Skip to content

losos/html: skip patch shortcut when cached DOM was wiped (#15)#16

Merged
melvincarvalho merged 1 commit into
gh-pagesfrom
fix-issue-15-stale-parts-cache
May 3, 2026
Merged

losos/html: skip patch shortcut when cached DOM was wiped (#15)#16
melvincarvalho merged 1 commit into
gh-pagesfrom
fix-issue-15-stale-parts-cache

Conversation

@melvincarvalho
Copy link
Copy Markdown
Contributor

@melvincarvalho melvincarvalho commented May 3, 2026

Closes #15.

Summary

  • Tagged-template strings arrays are interned, so prev.strings === template.strings matches the cache forever — even when the container was wiped externally between renders. patch() then mutates orphan nodes in prev.parts and silently no-ops.
  • Add a container.firstChild guard before taking the patch shortcut. Falls through to the existing fresh-build path on cache miss / stale cache.

2 lines changed (one comment, one extra condition on the existing if). +90 bytes.

Why container.firstChild and not container.contains(prev.parts[0].node)

The earlier draft of this PR used prev.parts[0]?.node for the validity check. As Copilot pointed out, that breaks the patch fast-path for static templates with zero ${} holes — parts is empty, the check fails, every rerender does a full rebuild and resets element state (input focus, selection, etc).

container.firstChild works for both shapes: it's truthy when the container has any rendered DOM at all, falsy after innerHTML = ''. No regression on static templates.

Why only losos/html.js and not the root html.js

package.json exports ./html./losos/html.js, and files only includes losos/. The duplicate html.js at the repo root isn't published and isn't what consumers import. Fixing it doesn't reach anyone. Filed separately as #17 — eliminating that duplicate is a different problem.

Test plan

  • node -c losos/html.js syntax passes.
  • Manual: served via the LOSOS shell at nosdav/browser's clone — switching tabs no longer blanks source-pane (the originating real-world hit).
  • Repro from html.js render() blanks container after external innerHTML clear (stale-parts cache) #15 (render, clear innerHTML, render again with same template) returns the expected DOM after the fix.
  • Static template (html\

    static

    `) rerendered twice still hits the patch fast-path (parts is empty but container.firstChild` is truthy → no full rebuild).

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a stale-cache bug in the lightweight html.js renderer where rerendering the same tagged-template after external DOM clearing could hit the patch fast-path and silently update detached/orphan nodes (issue #15).

Changes:

  • Extend the render cache hit condition to ensure the cached DOM is still attached to the container before using the patch shortcut.
  • Update the inline comment to document the new stale-cache guard.

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

Comment thread html.js Outdated
Comment on lines +46 to +47
// Same template — patch holes. Skip if cached DOM was wiped (#15).
if (prev && prev.strings === template.strings && prev.parts[0]?.node && container.contains(prev.parts[0].node)) {
Comment thread html.js Outdated
Comment on lines +46 to +47
// Same template — patch holes. Skip if cached DOM was wiped (#15).
if (prev && prev.strings === template.strings && prev.parts[0]?.node && container.contains(prev.parts[0].node)) {
Tagged-template `strings` arrays are interned by the engine, so the
cache's `prev.strings === template.strings` matches forever — even
when the container's children were cleared externally between
renders. patch() then mutates detached nodes in `prev.parts` and
silently no-ops, leaving the container blank.

Add a `container.firstChild` guard before taking the patch
shortcut. Falls through to the existing fresh-build path when the
cache is stale. Using `container.firstChild` rather than checking a
specific part's node also keeps the fast-path live for static
templates with zero `${}` holes (empty parts array).

Patched in `losos/html.js`, the file actually exported by
package.json (`exports["./html"] → ./losos/html.js`); the
duplicate copy at the repo root is unpublished and is its own
problem (separate issue).
@melvincarvalho melvincarvalho force-pushed the fix-issue-15-stale-parts-cache branch from 20547f7 to 92db250 Compare May 3, 2026 04:40
@melvincarvalho melvincarvalho changed the title html: skip patch shortcut when cached DOM was wiped (#15) losos/html: skip patch shortcut when cached DOM was wiped (#15) May 3, 2026
@melvincarvalho melvincarvalho requested a review from Copilot May 3, 2026 04:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


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

Comment thread losos/html.js
Comment on lines +46 to +47
// Same template — patch holes. Skip if cached DOM was wiped (#15).
if (prev && prev.strings === template.strings && container.firstChild) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Declining the anchor approach. The foreign-DOM case requires a consumer to interleave render() with direct container.appendChild(foreign) between renders on the same container — an unusual pattern; either you manage that container with render() or you do direct DOM ops, not both. The actual #15 consumer (the LOSOS shell) does content.innerHTML = '' immediately before each pane.render(...), so when our render() runs, firstChild is null and the check correctly falls through. Keeping container.firstChild for the lean ethic — +0 cache fields, +0 lines on the build path.

@melvincarvalho melvincarvalho merged commit 43c8adb into gh-pages May 3, 2026
4 checks passed
@melvincarvalho melvincarvalho deleted the fix-issue-15-stale-parts-cache branch May 3, 2026 04:49
melvincarvalho added a commit to nosdav/browser that referenced this pull request May 3, 2026
linkedobjects/losos#16 added a `container.firstChild` guard to the
patch fast-path in `render()`, so a stale cache after the container
was wiped externally falls through to a fresh build instead of
silently no-op'ing on detached nodes. Mirror the upstream change
into the vendored copy at `losos/html.js`.

Net diff: 2 lines, +90 bytes. Fixes source-pane going blank after
tab-switching in the LOSOS shell.
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.

html.js render() blanks container after external innerHTML clear (stale-parts cache)

2 participants