Skip to content

Dev-warn when a streamed Suspense resolve is dropped (absent boundary) #1051

Description

@vivek7405

Problem

applyStreamedResolve (the client router's streamed-Suspense applier) silently drops a resolve when its boundary placeholder #sN is not in the DOM (if (!boundary) return;). The drop itself is the CORRECT designed posture: every legitimately-reachable absent case is benign (a superseded nav fenced by isCurrent(), a discarded revalidation, a degrading full page load, a frame-missing no-swap). But the silence has a real cost: when a BUG wrongly makes a valid boundary absent, the only symptom is a mysteriously stuck skeleton with zero signal. That is exactly how the #1048 view-transition race stayed hidden and took a full live-repro investigation to diagnose. A one-line dev console warning would have surfaced it on the first glance.

This is an observability follow-up from the PR #1049 review discussion, NOT a behavior change. Do not add a retry or apply-anyway path: a retry would run outside the streamer's nav-token fence and could splice a superseded nav's content into a recycled boundary id (boundary ids are per-render sequential).

Design / approach

Mirror the router's existing degradation-diagnostics pattern, devWarnFallback (packages/core/src/router-client.js ~L775): dev-only (process.env.NODE_ENV === 'production' early-return), warnOnce-keyed so a burst of dropped boundaries does not spam, message naming the dropped boundary id and the likely causes, e.g. [webjs] dropped streamed Suspense resolve "s1": no #s1 boundary in the DOM. Benign if this navigation was superseded or degraded; a stuck skeleton here means the shell swap did not place the boundary.

Keep the drop itself byte-identical (return unchanged). The warning goes in the !boundary branch only.

Implementation notes (for the implementing agent)

  • Where to edit: packages/core/src/router-client.js applyStreamedResolve (~L4093, the if (!boundary) return; branch, which already carries a comment block explaining why the drop is deliberate; extend it rather than contradicting it). devWarnFallback (~L775) and warnOnce are in the same file and show the exact dev-gating + once-keying idiom; key the warn on the boundary id (stream-drop:${id}).
  • Landmines:
    • Do NOT warn on the isCurrent()-fenced skips in streamBoundariesProgressively (~L4110): a superseded nav's skipped applies are routine, not diagnostic. Only the reached-the-DOM-and-found-no-boundary case warns.
    • _resetWarnOnce is exported for tests (packages/core/test/routing/router-client.test.js uses it); reuse it so the new warn is testable deterministically.
    • The function also runs during background revalidation paths; the warn must never throw and never run in production (the devWarnFallback gate handles both).
  • Invariants: .js + JSDoc in packages/; no behavior change to the drop; client-router change, so a browser assertion is preferred, but a pure console-warn addition is observable in the node/linkedom harness too (stub console.warn), which matches how other warn-once tests in router-client.test.js assert.
  • Tests: node test asserting (a) a dropped resolve emits exactly one dev warning naming the id, (b) a second drop of the same id does not re-warn, (c) NODE_ENV=production emits nothing, and (d) a successful resolve emits nothing. Counterfactual: the test fails when the warn line is removed.
  • Docs: none (dev-only diagnostics; no public surface change). No scaffold/MCP/editor surface.

Acceptance criteria

  • A dropped streamed resolve logs one dev-only warning naming the boundary id and pointing at the stuck-skeleton implication
  • No warning in production, on superseded-nav fenced skips, or on successful resolves; no repeat spam for the same id
  • The drop behavior itself is byte-identical (no retry, no apply-anyway)
  • A counterfactual proves the test fires (removing the warn reds it)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions