Skip to content

Scaffold gallery: add streaming-action, view-transition, and <webjs-suspense> demos #990

Description

@vivek7405

Problem

A gallery coverage audit (against the full @webjsdev/core + @webjsdev/server surface) found three shipped, browser-facing features with zero demo anywhere in the scaffold gallery. This issue combines all three into one gallery-expansion effort (was #990 streaming, #991 view transitions, #992 <webjs-suspense>; #991 and #992 are folded here):

  1. Streaming server-action results (feat: streaming RPC results (return a ReadableStream/AsyncIterable from an action) #489) - an action returning a ReadableStream / async iterable / async generator, consumed at the call site with for await.
  2. View Transitions - the opt-in <meta name="view-transition"> + data-webjs-permanent.
  3. Component-level <webjs-suspense> streaming boundary (feat: <webjs-suspense> streaming SSR boundary + per-component streaming (follow-up to #469) #471) - a first-paint fallback that streams the resolved content in.

They can ship as one PR with three commits, or be split; keep them one tracked item.

Shared implementation notes (all three)

  • Each is one single-concept demo: packages/cli/templates/gallery/app/features/<name>/page.ts + packages/cli/templates/gallery/modules/<name>/, registered in the FEATURES array in packages/cli/lib/create.js (around L1340), and added to the galleryModules prune list in packages/cli/templates/scripts/clear-gallery.mjs.
  • The gallery ships in every UI template; generators emit strings, so an escaping bug only shows in a freshly generated app (follow webjs-scaffold-sync). No backticks inside html bodies (invariant 9). app/ is routing-only, logic in modules/, server-only code behind .server.ts, light DOM + Tailwind, one action per file.
  • Verify each with generate + boot + webjs check. These features' exports are already classified/exempt in test/scaffolds/gallery-coverage.json, so no manifest change is expected; confirm test/scaffolds/gallery-coverage.test.js and the other scaffold-*.test.js stay green.

1. Streaming server-action results (#489)

No demo shows an action returning a stream/async-generator consumed with for await (const chunk of await streamTokens()). The api-template stream route (packages/cli/lib/api-gallery.js) returns a raw HTTP ReadableStream from a hand-written route.ts, a DIFFERENT mechanism, so the gap is real. This is a marquee use case (token-by-token AI output over the normal action call site, back-pressured, cancelled on client disconnect).

  • Add: modules/streaming/actions/streamTokens.server.ts (an async function* action), modules/streaming/components/token-stream.ts (a component doing for await, appending to a signal), app/features/streaming/page.ts.
  • Detection is on the return value (no config export), per @webjsdev/server/src/action-stream.js (isStreamable). A streamed result is never cached / ETagged / seeded.

2. View Transitions

view-transition and data-webjs-permanent appear nowhere in the scaffold. The client-router demo covers soft nav / prefetch / navigate() / revalidate() / data-no-router / clientRouter:false, but not View Transitions (a headline client-router feature).

  • Reference: .agents/skills/webjs/references/client-router-and-streaming.md (View Transitions section). Opt in with <meta name="view-transition"> (value same-origin), which wraps all three swap paths; data-webjs-permanent + an id persists a live element across a swap.
  • Add either a scoped app/features/view-transitions/ page pair or extend the client-router demo; include one data-webjs-permanent element. Decide where the meta opt-in scopes (a root-layout meta turns it on app-wide).
  • Landmine: startViewTransition is not universal; when unavailable the swap runs synchronously with no flash and no throw, so the demo must degrade cleanly. iOS WebKit repaint quirks around client-router nav are documented (dogfood: mobile navbar flickers on forward nav (backdrop-blur sticky header) #610); keep the transition simple.

3. <webjs-suspense> component streaming (#471)

Page-level Suspense is demoed (app/features/async-render/page.ts) but the component-level <webjs-suspense .fallback=...> streaming boundary element is not shown. It is the only way to show a first-paint fallback for a SLOW component and stream the content in.

  • Add either a second, <webjs-suspense>-wrapped slow region to the async-render demo, or a dedicated app/features/suspense/. Contrast it with plain async-render (which blocks SSR so the data is in the first paint with no fallback): <webjs-suspense> is the deliberate choice for SLOW data.
  • <webjs-suspense> is a core element re-exported from @webjsdev/core; .fallback=${...} MUST be an unquoted property hole (invariant 4). Needs genuinely slow data (an artificial delay in the action) to be visible; a throwing component inside a boundary is isolated.

Acceptance criteria

  • A streaming-action demo shows an async-generator action consumed with for await, rendering chunks as they arrive
  • A View Transitions demo shows a transition on a soft navigation, with the <meta> opt-in and one data-webjs-permanent persisted element, degrading cleanly where startViewTransition is unavailable
  • A <webjs-suspense> demo wraps a slow region in .fallback=... and streams the resolved content in, with the contrast to page-level Suspense clear in the comments
  • All three cards are registered in FEATURES and pruned by gallery:clear
  • All three ship in every UI template; generate + boot + webjs check pass; the scaffold coverage + gallery suites stay green

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions