Skip to content

Loading states 11 28 - #3

Merged
ivogt merged 4 commits into
mainfrom
loading-states-11-28
Nov 28, 2025
Merged

ivogt merged 4 commits into
mainfrom
loading-states-11-28

Conversation

@ivogt

@ivogt ivogt commented Nov 28, 2025

Copy link
Copy Markdown
Collaborator

No description provided.

ivogt and others added 4 commits November 28, 2025 18:46
…treaming

Implements Next.js-like loading() support for route definitions. When a route
defines a loading component, async handlers are wrapped in React Suspense with
the loading component as the fallback, enabling RSC streaming.

Key changes:
- Added loading() builder method to route definitions (route-definition.ts)
- Modified router to wrap async handlers in Suspense when loading is defined (router.ts)
- OutletProvider passes loading component to context (outlet-context.ts)
- Outlet component wraps content in Suspense when loading exists (client.tsx)
- Added ProductDetailSkeleton example in shop demo

The loading skeleton appears immediately while the route content streams in,
providing a better user experience for slow async operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Merges origin/main which adds errorBoundary() and notFoundBoundary() helpers
into this branch which has loading() support for RSC streaming.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@ivogt
ivogt merged commit 4ac9013 into main Nov 28, 2025
ivogt added a commit that referenced this pull request Feb 2, 2026
Analyzes the fundamental tension between lazy loading and type safety:
- What current lazy loading actually does
- Whether it matters for RSC/Cloudflare Workers
- Four options to preserve type safety (A-D)
- Recommendation to benchmark before deciding

Added lazy loading strategy as open question #3.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
ivogt added a commit that referenced this pull request Jun 13, 2026
… decisions, #570 cleanup (#573)

* test(router): dual-mode e2e for prerender+loading()+passthrough (M16) and redirect under loading() (M9)

Adds the dev + production e2e fixtures the M16/M9 unit-tested fixes still
lacked (CLAUDE.md mandates dual-mode coverage). Deferred from the stabilization
PR; addresses the two main gaps in #567.

- prerender-loading: Prerender + loading() + ctx.passthrough(). The loading()
  boundary defers the build handler so the passthrough sentinel arrives
  Promise-wrapped; detectPrerenderPassthrough awaits it. getParams lists
  [baked, skip]: baked is prerendered, skip passes through to the live handler.
  Production asserts the manifest holds baked but NOT skip (M16 regression
  guard: count === 1; without the fix skip would bake a corrupt artifact).
- loading-redirect: a synchronous redirect() return under loading()
  short-circuits to a real HTTP redirect. Verified for hard + soft navigation
  in both modes.

Regenerated router.named-routes.gen.ts for the two new includes (urls.gen.ts is
legacy/unreferenced and no longer auto-generated, so left untouched).

Lower-priority #567 gaps (build-time intercept _currentSegmentId handle drop,
server-action-bridge pre-dispatch over-invalidation, document-cache
private+s-maxage) need fixes first and are deferred; #567 stays open for them.

* chore(router): safe cleanup batch from review (#570)

Internal-only, behavior-preserving cleanups; grep-verified safe and gate-green
(typecheck, full unit suite, lint, format).

- Remove write-only MiddlewareEntry.mountPrefix (read nowhere; scoping folds
  into fullPattern before the entry is built). Dropped 3 production assignments
  plus the test fixtures.
- Dedupe cookie helpers: delete the test-only middleware-cookies.ts (byte-for-byte
  dup of request-context's parseCookiesFromHeader/serializeCookieValue), export
  the canonical copies from request-context for the unit tests, drop the
  middleware.ts re-export.
- Delete the zero-importer src/handles/index.ts barrel (Meta/MetaTags/Breadcrumbs
  are exported directly from the client/index entries); repoint collect-handle.test.
- Drop the MiddlewareCollectableEntry re-export from middleware.ts; its one
  importer now imports from middleware-types.
- Extract hasContextVars() into context-var.ts and use it in the two prerender
  entry points that duplicated the keys-or-symbols predicate.
- renderStaticSegment: throw a TypeError when a Static handler returns a Response
  (was silently serialized as a corrupt build artifact); the fresh/revalidation
  paths already throw via handleHandlerResult. Adds a unit test.
- ctx.reverse() middleware fallback: widen the signature to (name, params?, search?)
  to match ScopedReverseFunction and make the error message accurate.
- docs: tree-structure.md inTransitionScope also covers error/notFound segments.

Deferred, still open in #570: the optionalParams all-or-nothing removal and the
loaderPromises param removal both sit on matrix-critical matching/segment-resolution
paths and warrant a semantic-matrix run rather than a cleanup sweep. The
document-cache "timing-gap caveat" cited at document-cache.ts:45-53 does not exist
there (those lines are Cache-Control parsing), so that doc item is a no-op.

* feat(router): pre-1.0 public-API surface decisions from review (#569 part 1)

Type/naming/export-surface decisions (no behavior change). See #569.

- #1 router.use(pattern, mw): add a pattern-generic overload so
  `.use("/users/:id", (ctx) => ...)` types `ctx.params.id` via ExtractParams.
  Bare `.use(mw)` stays generic. Added on both Rango and RangoInternal. Pins it
  with a type-level test (router-use-types.test.ts).
- #2 useAction: export a stable public `ActionState` (and `ActionLifecycleState`)
  from ./client so consumers can name the hook's return type; TrackedActionState
  stays the @internal impl name (ActionState aliases it).
- #4 buildEnv: move off the shared base onto per-preset interfaces. Cloudflare
  keeps `"auto"` (wrangler getPlatformProxy); Node now rejects `"auto"` at the
  type level (Exclude<BuildEnvOption,"auto">) instead of only at build time.
- #5 poke plugin: rename name "vite-plugin-poke" -> "@rangojs/router:poke" so it
  matches the @rangojs/router:* family for consumers ordering/filtering by name.
- #6 ./build: move realm-crossing pipeline internals (buildRouteTrie,
  buildPerRouterTrie, TrieNode, TrieLeaf, collectFallbackClientRefs) to a new
  ./build/internal subpath; ./build keeps the documented public API
  (generateManifest*, route-type generation, createScanFilter, hashParams).
- #8 mergeHandlerUse: document the intentional validation asymmetry (explicit
  use() items bypass the mount-site allow-list); chosen over enforcing it.

Deferred to part 2 (behavior changes): #3 prefetch options, #7 scanFilter
wiring, #9 profile-registry trim, #10 redirect-in-action idiom + examples.

* feat(router): prefetch source-scope option + redirect-in-action ergonomics (#569 part 2a)

- #3 router.prefetch(url, { key: ":source" }): add an options arg so imperative
  prefetch can source-scope its cache entry, at parity with
  <Link prefetchKey=":source">. Wired through to prefetchDirect.
- #10 redirect() in actions: drop the `as any` on non-form actions by typing
  them Promise<Response> (saveAndRedirect, actionSimpleRedirect). The form-bound
  peReturnRedirect keeps a documented intrinsic cast (React's form-action type is
  () => void | Promise<void>, which can't express a Response return); consumers
  should prefer `throw redirect(...)`. Strengthened the server-actions skill.

Deferred to part 2b: #7 (complete scanFilter — needs a new public plugin option)
and #9 (trim profile-registry — runtime cache-resolution refactor).

* feat(router): complete scanFilter discovery option + trim profile registry (#569 part 2b)

- #7 complete the scanFilter feature. Adds a public rango() option
  `discovery: { include, exclude }` (glob, root-relative; mirrors the CLI's
  --include/--exclude). The consume side was already wired
  (findRouterFiles(state.projectRoot, state.scanFilter) at 3 sites); this
  threads the option through PluginOptions and compiles it via createScanFilter
  into state.scanFilter in configResolved, once projectRoot is known. Filters
  which files route discovery / static type-gen scan. Tests in
  generate-route-types.test.ts (include restricts, exclude removes).
- #9 trim the profile registry. Removed the global mutable `_profiles`,
  `setCacheProfiles`, and `getCacheProfile` — all dead: `getCacheProfile` had no
  production reader (its own JSDoc said so), and `setCacheProfiles` only
  populated the unread global. Kept the load-bearing `resolveCacheProfiles`,
  which createRouter() already uses to thread a per-request profile map (the
  "use cache: <profile>" path reads it request-scoped via ctx._cacheProfiles).
  Dropped the router.ts setCacheProfiles() call; repointed the tests to
  resolveCacheProfiles (and reframed the "global replace" test as per-router
  map independence).

#569 is now fully implemented (all 10 decisions).

* fix(router): keep build internals on ./build (revert #569.6) + throw in return-form redirect actions (#569.10)

CI on #573 caught a regression from #569 part 1: the Vite discovery pipeline
imports buildRouteTrie / buildPerRouterTrie / collectFallbackClientRefs at build
time via runner.import("@rangojs/router/build") (discover-routers.ts), and the
RSC runner does not resolve a ./build/internal subpath in that realm. Moving them
left collectFallbackClientRefs undefined, so the router-level default error /
notFound boundary was never collected into the app-fallback chunk
(mini.test.ts clientChunks: "mini-default-error must be in app-fallback").

Revert #569.6: restore buildRouteTrie/buildPerRouterTrie/collectFallbackClientRefs
+ TrieNode/TrieLeaf on ./build; delete ./build/internal + its package.json export.
#569.6 is re-deferred — internalizing these needs runner subpath resolution or a
different seam (noted in build/index.ts).

#569.10 revision: per owner, the return-form redirect test actions
(saveAndRedirect, actionSimpleRedirect) now `throw redirect()` instead of being
typed Promise<Response>. The form-bound peReturnRedirect keeps its documented
intrinsic cast.

Verified: mini clientChunks 6/6, location-state e2e 47/47 (dev+prod), typecheck,
3866 unit, lint, format.
ivogt added a commit that referenced this pull request Jun 23, 2026
…minism

Implements the verified review findings on the automatic fullyPrefetched/complete
prefetch-driven transition (separate from transition({ when })), plus hardening.

- #2 honesty: DecodedPrefetch.complete flips only on normal stream EOF. teeWithCompletion
  now reports "eof" | "aborted" | "error"; complete is gated on "eof", so an aborted or
  errored prefetch is no longer treated as fully warmed.
- #3 coverage: unit tests for teeWithCompletion reasons, navigation-client fullyPrefetched
  propagation, and the partial-update fully-prefetched startTransition branch (asserting
  NO addTransitionType), with a beforeEach mock-clear so the assertion is non-vacuous.
- #4a determinism: e2e waits on a gated client-complete signal (window.__rangoPrefetchComplete,
  a no-op in production unless a test creates it) instead of network-body timing; the partial
  row stays headers-only. Removes the under-load flake.
- #4c parity: cloudflare-basic dev+prod prefetch-transition e2e (slow route + hover-prefetch
  link); the helper derives label from mode (drift-proof bucketing) and a negative-control
  test keeps the no-flash assertion honest.
- Docs: internal feature map/file map describe the automatic fully-drained prefetch content-hold.
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.

1 participant