Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ the same output in all three.
| `directives.js` | the lit-html-parity directive set (`unsafeHTML`, `live`, `keyed`, `guard`, `templateContent`, `ref` / `createRef`, `cache`, `until`, `asyncAppend` / `asyncReplace`, `watch`, plus each `is*` guard). `repeat` lives in `repeat.js`. All are re-exported from `index.js` / `index-browser.js` so the bare specifier and the `/directives` subpath (which collapses onto the dist browser bundle) expose the full set |
| `repeat.js` | `repeat(items, keyFn, templateFn)` for keyed list reconciliation |
| `suspense.js` | `Suspense()` page/region-level boundary primitive |
| `webjs-suspense.js` | The `<webjs-suspense>` component-level streaming boundary element (#471). SSR (`render-server.js`) does the work: `injectDSD`'s `processSuspenseElements` pre-pass reads `.fallback` (carried as `data-webjs-fallback` by `renderTemplate`, since a TemplateResult is not serializer-safe) and, in a streaming context, flushes the fallback as `<webjs-suspense id="sN">` while pushing the children to `ctx.pending` for out-of-order streaming (concurrent across boundaries via `Promise.all`); without a streaming context the children render inline (blocking). This client element is layout-neutral (`display:contents`) and the registration home for the soft-nav apply; first-load streaming needs no client runtime (the inline swap script `replaceWith`s the boundary element with the resolved children, which then upgrade). Every swap path (the inline script, the boot `__webjsResolve`, and the soft-nav `applyStreamedResolve`) removes the transient wrapper, so a boundary settles to the same DOM however the page was reached. SSR-inert (defined client-side only) |
| `webjs-suspense.js` | The `<webjs-suspense>` component-level streaming boundary element (#471). SSR (`render-server.js`) does the work: `injectDSD`'s `processSuspenseElements` pre-pass reads `.fallback` (carried as `data-webjs-fallback` by `renderTemplate`, because this element is browser-defined, so the walk below finds no class for it and no server-side instance runs `consumePropAttrs`, which leaves `connectedCallback` as the only consumer of a normal `data-webjs-prop-*` binding, far too late for a placeholder that has to be in the first flushed bytes) and, in a streaming context, flushes the fallback as `<webjs-suspense id="sN">` while pushing the children to `ctx.pending` for out-of-order streaming (concurrent across boundaries via `Promise.all`); without a streaming context the children render inline (blocking). This client element is layout-neutral (`display:contents`) and the registration home for the soft-nav apply; first-load streaming needs no client runtime (the inline swap script `replaceWith`s the boundary element with the resolved children, which then upgrade). Every swap path (the inline script, the boot `__webjsResolve`, and the soft-nav `applyStreamedResolve`) removes the transient wrapper, so a boundary settles to the same DOM however the page was reached. SSR-inert (defined client-side only) |
| `context.js` | Context Protocol: `createContext`, `ContextProvider`, `ContextConsumer`, `ContextRequestEvent` |
| `task.js` | `Task` / `TaskStatus` controller for async data in components |
| `router-client.js` | Turbo Drive–style client router; entry: `enableClientRouter` / `navigate`. Also exports `loadFrame(frameEl, url)` (#253), the reusable frame self-load `webjs-frame.js` calls: it fetches `url` as a frame nav (the `x-webjs-frame` header) and applies the matched subtree through the SAME `fetchAndApply` frame-swap path a click uses (no history push / snapshot / optimistic skeleton, since it swaps one region). Post-swap activation of a boundary range goes through `activateSwappedRange` (#1102), the ONE place both tiers (`replaceBoundaryRange`, `swapMarkerRange`) reactivate scripts and upgrade custom elements. Two things it owns and a new call site must keep: it SNAPSHOTS the range before iterating, because `reactivateScripts` replaces a top-level script and a detached node cuts a live `nextSibling` walk (every later node in the range is then silently skipped); and `reactivateScripts` handles container-IS-a-script itself, since `querySelectorAll` never matches the node it is called on. A top-level script therefore re-executes on every swap of its range, INCLUDING one the keyed differ reused by `id`, matching what a descendant script in a reused container has always done. `data-webjs-permanent` splits into two cases and they must NOT be unified (#1252). The marked element IS a script: NEVER exempt, whether the walk reaches it as the container or as a descendant of one (the regraft selector has no tag filter, so a marked script IS preserved by identity and does land in the WeakSet, which is why the exemption is STRICT containment and never reflexive). The regraft also has a both-exist guard, so on the swap that first mounts a route there is no live node to preserve and exempting the inert parsed copy would leave a script that runs on a cold load and never on a soft nav, which is #1102 itself. Script INSIDE a preserved marked element: exempt, because the attribute is subtree-scoped (`diffElementInPlace` already returns early rather than recursing into one) and re-emitting an init script against an instance the author kept alive is a double-initialization. The filter keys on the `regraftedPermanents` WeakSet, which the two regrafts populate on every successful path, so it means ACTUALLY preserved by identity rather than merely carrying the attribute; an attribute-only filter would leave a first-mount permanent element's scripts never running at all |
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/render-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,12 +400,15 @@ async function renderTemplate(tr, ctx) {
// page-level `.prop` on a native element could have set the
// property to begin with.
out = out.slice(0, attrStart);
// `<webjs-suspense .fallback=${html`...`}>` (#471). A TemplateResult
// is not serializer-safe (the normal data-webjs-prop-* path would
// drop it) and a normal custom-element prop applies only at
// hydration, too late for the streaming placeholder. So render the
// fallback to HTML now and carry it as data-webjs-fallback, which the
// injectDSD streaming pre-pass reads as the boundary placeholder.
// `<webjs-suspense .fallback=${html`...`}>` (#471). This element is
// defined only in the browser, so the injectDSD walk skips it
// (`lookup(tag)` finds no class) and no server-side instance runs
// consumePropAttrs. A normal data-webjs-prop-* binding would then
// land at connectedCallback, too late for the streaming placeholder.
// So render the fallback to HTML now and carry it as
// data-webjs-fallback, which the injectDSD streaming pre-pass reads
// as the boundary placeholder. (The value itself would serialize
// fine: a TemplateResult is a plain {strings, values} object.)
if (currentTag === 'webjs-suspense' && name === 'fallback') {
const fbHtml = await render(val, ctx);
out += `data-webjs-fallback="${escapeAttr(fbHtml)}"`;
Expand Down
2 changes: 1 addition & 1 deletion website/app/docs/suspense/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ With Suspense: TTFB = shell render = ~40ms
<li><strong>Error-isolated.</strong> A throwing component inside a boundary renders its own error state while siblings stream.</li>
<li><strong>Progressive on soft navigation.</strong> A client-router navigation to a streamed page applies the shell (with fallbacks) immediately, advances the URL, then streams each boundary in, matching the initial-load experience.</li>
</ul>
<p>The <code>.fallback</code> is read at SSR as the inline placeholder (never through the <code>data-webjs-prop-*</code> path, since a <code>TemplateResult</code> is not serializer-safe) and must be an unquoted property hole. <code>renderFallback()</code> on a component is a DIFFERENT concern (the client re-fetch loading state, never the first paint); see <a href="/docs/loading-states">Loading States</a>.</p>
<p>The <code>.fallback</code> is read at SSR as the inline placeholder (never through the <code>data-webjs-prop-*</code> path, because <code>&lt;webjs-suspense&gt;</code> is defined only in the browser, so no server-side instance consumes that attribute and the property would not land until <code>connectedCallback</code>, far too late for a placeholder that has to be in the first flushed bytes) and must be an unquoted property hole. <code>renderFallback()</code> on a component is a DIFFERENT concern (the client re-fetch loading state, never the first paint); see <a href="/docs/loading-states">Loading States</a>.</p>
Comment thread
vivek7405 marked this conversation as resolved.
Comment thread
vivek7405 marked this conversation as resolved.

<h2>When to Use Suspense</h2>
<ul>
Expand Down
Loading