Skip to content

fix(router): location state belongs to the entry on screen; evicted same-route back/forward restores its page (#1029, #1030) - #1033

Merged
ivogt merged 8 commits into
mainfrom
fix/1029-location-state-with-tree
Oct 4, 2026
Merged

ivogt merged 8 commits into
mainfrom
fix/1029-location-state-with-tree

Conversation

@ivogt

@ivogt ivogt commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #1029. Closes #1030. References #1031.

Two fixes. useLocationState returns a history entry's state only together with that entry's page, for a reader that is mounted across a navigation and for one that mounts while the navigation is pending (#1029). Back/forward to an entry of the same route that the history cache dropped now restores that entry's page instead of keeping the page being left under the entry's URL (#1030).

Problem

#1029. On a client navigation the router pushes the history entry once the response has arrived, and React commits the destination afterwards. A navigation that waits on a loader keeps the current page on screen until then. useLocationState did not wait: it read history.state, which has already moved, so the page being left rendered with the next entry's state. The visible case is a "load more" list: ?page=N loads a page through a loader, and the items on screen travel to the next page as location state on the <Link>. After the click, every item of the current page was listed twice until the next page's data landed, and React logged duplicate-key warnings in development. Back/forward had the same shape. A reader that mounted during the wait had it too, whichever way mounted readers were delivered to.

#1030. Pressing Back to ?page=3 from ?page=24 of the same list, once page 3's entry had left the 20-entry history cache, changed the URL and the entry's location state and left page 24 on screen.

Before vs after

The same component, the same click on "Load more" on page 5, while page 6's loader is still streaming:

const carried = useLocationState(CarriedProducts) ?? []; // pages 1..N-1
const { data } = useLoader(ProductsLoader); // page N, from ?page=N
const products = [...carried, ...data.products];
While ?page=6 is pending carried loader data On screen
Before pages 1..5 (the destination's) page 5 pages 1..5 plus page 5 again
After pages 1..4 (the current entry's) page 5 pages 1..5
After, a reader mounted during the wait pages 1..4 page 5 agrees with the list
Back to ?page=3 from ?page=24, entry no longer cached URL Page on screen
Before ?page=3 page 24
After ?page=3 page 3 (refetched)

End-to-end example

// src/location-states.ts
import { createLocationState } from "@rangojs/router";

export const CarriedProducts = createLocationState<Product[]>({
  clearOnReload: true, // a refresh shows what the server rendered
});
// src/loaders.ts
import { createLoader } from "@rangojs/router";

export const ProductsLoader = createLoader(async (ctx) => {
  const page = Number(ctx.searchParams.get("page") ?? "1");
  return { page, products: await listProducts({ page, size: 50 }) };
});
// src/components/ProductList.tsx
"use client";
import { Link, useLoader, useLocationState } from "@rangojs/router/client";
import { ProductsLoader } from "../loaders";
import { CarriedProducts } from "../location-states";

export function ProductList() {
  const { data, isLoading } = useLoader(ProductsLoader);
  const carried = useLocationState(CarriedProducts) ?? [];
  const products = [...carried, ...data.products];
  return (
    <>
      <ul aria-busy={isLoading}>
        {products.map((product) => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
      <Link
        to={`/products?page=${data.page + 1}`}
        state={[CarriedProducts(products)]}
        scroll={false}
      >
        Load more
      </Link>
    </>
  );
}

No duplicate filter, and Back through a list longer than the history cache lands on the page the URL names.

The delivery model

The location state of the entry on screen is React state owned by NavigationProvider, next to the payload, set in the same update and delivered by context:

// src/browser/react/NavigationProvider.tsx
const [payload, setPayload] = useState(initialPayload);
const [locationState, setLocationState] = useState(eventController.getLocationState);
// store.onUpdate subscriber:
setPayload({ root: update.root, metadata: update.metadata });
setLocationState(eventController.getLocationState());
// render:
<LocationStateContext.Provider value={locationState}>{content}</LocationStateContext.Provider>
// src/browser/react/location-state.ts: the whole hook
const entry = useContext(LocationStateContext);
const optimistic = useContext(OptimisticLocationContext);
const shown = (optimistic ? readableLocationState(optimistic) : entry)?.[key || "state"];
const state = useSyncExternalStore(subscribeToNothing, () => shown, getServerSnapshot);
// + the flash clear effect

A transition React holds renders the destination's value; an urgent render of the content on screen, a reader mounting there included, renders the value of the entry being left. That is React's own state semantics, so no ordering between a notification and a payload is relied on.

  • One source. Each commit site hands the controller the entry state it pushed, restored or merged: EventController.commitLocationState(entryState). The controller stores a snapshot (locationStateSnapshot: the typed slots and plain state, only when recorded under the client's version) and notifies nobody; the provider takes it next to setPayload. It is never a later read of history.state.
  • Commits without a payload pass treeless: a revalidate: false navigation, a commit whose response kept every segment, and a server action's state. The provider takes those with the commit's own state notification, in the batch that moves usePathname. A tree commit never sets the mark, so nothing can deliver it early.
  • No second delivery path. The __rsc_locationstate window event is gone, and so are the commit identity and per-reader subscription the first three commits of this branch replaced it with. The hook has no state, ref, subscription or frozen first read. useSyncExternalStore stays for its server snapshot only, per reader, so every hydrating render returns undefined.
  • Identity. The snapshot is a context value, so identity decides who re-renders. A slot keeps its value, and the snapshot its identity, when a commit passes back what readers already hold. Equal content in a new object is a new value on purpose: a link clicked twice announces its flash message or scroll target twice. I tried deep equality first; it made a repeated flash invisible and left it uncleared in history. New values are structured clones, so a reader never shares an object with the navigation's caller.

When a reader sees an entry's state

What changes the entry's state A reader gets it Tree
push / replace, urgent commit in the commit that shows the destination yes
push / replace, held (same-route, transition(), held loader) in the commit that shows the destination; the page being left keeps its own yes
back/forward, cached in the commit that restores the entry (the state found at popstate) yes
back/forward, refetched in the commit that shows the refetched entry yes
server-set state on a navigation or redirect in the commit that shows the destination yes
intercept (modal over the page) with the modal yes
a navigation or back/forward the server answers with nothing to render with the new location (same batch as usePathname) no
revalidate: false with the new location no
a server action's setLocationState when the response is processed, added to what readers show no
Def.write() / Def.delete(), the flash clear not until the entry is restored (back/forward, reload) none
a reader that mounts while a navigation is pending the state of the page it mounts in n/a
a clientUrls() optimistic destination the state the navigation carries, from its first render n/a
superseded, cancelled or failed navigation nothing n/a
document load / hydration undefined while hydrating, then the entry's state n/a

What an existing app can notice

Listed in full in the CHANGELOG entry. In short:

  • A reader in content that stays on screen no longer previews the next entry.
  • A reader that mounts after Def.write() / Def.delete() no longer sees the change; it does when the entry is restored. Def.read() returns it at once. The JSDoc promised "the next mount" before.
  • A flash value is shown until the next navigation commits, by every reader: a reader mounted across a later navigation no longer keeps it, and one that mounts later in the same visit now gets it.
  • Every reader renders once when a navigation changes the entry's location state, in the commit that renders the page; none when it does not.
  • A server action's state is added to what readers show and no longer reveals an earlier write().
  • A reader outside the router's provider returns undefined (it read history.state before).

Code changes

src/browser/react/NavigationProvider.tsx, context.ts, location-state.ts, location-state-shared.ts (the snapshot); src/browser/event-controller.ts (the holder); the commit sites navigation-transaction.ts, navigation-bridge.ts, server-action-bridge.ts, partial-update.ts, rsc-router.tsx; src/ssr/ssr-root.tsx (same provider nesting, value undefined); src/client-urls/navigation.ts (the optimistic entry state is built only when the group presents the target); src/testing/render-route.tsx.

#1030 is one expression in src/browser/partial-update.ts:

const previousUrl =
  mode.type === "leave-intercept" || (tx.traversal && !interceptSourceUrl)
    ? segmentState.currentUrl || tx.currentUrl
    : interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;

tx.currentUrl is window.location when the transaction is created; on a back/forward the popstate event has already moved it to the target, so the server was told the client is on the target, diffed the target against itself, and on the same route found nothing to re-render. The store's URL is the committed entry's. The leave-intercept path already used it for the same reason.

renderRoute: its popstate listener is registered through the bridge's registerLinkInterception slot and commits with the same controller call as production. router.navigate() writes a history entry only for state (push) or replace: true; navigate(url) and navigate(url, { replace: false }) leave history alone.

The test-app's /load-more and /app-version routes moved into their own urls() module, included at the same paths and names; location-state.tsx differs from main by 4 insertions and 36 deletions. No *.gen.ts changed.

Bundle

The "use no memo" directive is removed. Router client chunk, gzip: main 47077B, the first three commits of this branch 47100B (with the directive), now 47270B (47262B for #1029, 8B for #1030). With the directive restored the reworked hook measured 47167B, so it would not fit the old 47104B limit either. ROUTER_CHUNK_GZIP_MAX is raised 46KB -> 47KB (48128B) with a dated note in tools/check-bundle-guards.mjs: a whole KB, like the earlier steps in that file, because main sat 27B under the old limit and a smaller step would trip on the next change. The hook itself shrank; the snapshot, its holder on the controller and the provider's context are what grew.

Test plan

Red before green:

  • Mount during a held navigation, unit (render-route-location-state-commit.test.tsx), on 27a41ffb: AssertionError: expected 'page 1: p1-1+p1-2+p1-3' to be 'page 1: none'. E2e (router app, dev), same source: Expected: "1:0" Received: "1:3", 3 of 3 attempts. Green on this branch.
  • Back/forward to an evicted same-route entry that differs only in search params leaves the wrong page on screen #1030, unit (partial-update.test.ts), without the fix: AssertionError: expected 'http://localhost/list?page=3' to be 'http://localhost/list?page=6'. E2e (router app, dev and production), without the fix: the screen held page: "23" with items p1-1, p1-2, p1-3, p23-1, p23-2, p23-3 and late: "23:3" (page 23's tree under page 2's URL and state). Green with it in both modes.

Unit, through renderRoute (src/testing/__tests__/render-route-location-state-commit.test.tsx):

  • a held load-more navigation shows no carried item twice (push, replace, hydrated, StrictMode)
  • a reader that mounts while a navigation is held sees the state of the entry on screen
  • a navigation renders readers once when the entry's state changes, and not at all when it does not
  • an object-valued slot keeps its identity when another slot changes: its reader's effects and memoized children do not run again
  • a reader's value is a copy
  • flash: shown with the destination, cleared, shown until the entry's state next changes; a reader mounted after the clear shows it
  • write() / delete() change the entry, not what readers show
  • router.navigate() writes a history entry only for state or replace: true
  • a popstate event is a back/forward onto the entry

Unit, internal: event-controller.test.ts (recording, the treeless mark, identity, copy), navigation-transaction.test.ts (the object pushed / restored / merged is the one recorded; treeless), navigation-bridge*.test.ts (shallow commit, cached restore order, navigations that do not commit), server-action-bridge.test.ts (an action's slots on top of what readers hold; a flash value survives), partial-update.test.ts (treeless on the no-changes path; the three previousUrl cases for #1030), location-state-hydration.test.tsx, location-state-version.test.tsx and location-state-version-surface.test.tsx (now through the real NavigationProvider), client-root.test.tsx.

Dogfood: tests/cloudflare-basic/test/location-state-options.test.tsx (held load-more with a late reader, on the app's own list).

E2e, both apps, dev and (production):

  • a load-more navigation held by its loader shows no item twice
  • a reader that mounts during a held navigation reads the entry on screen
  • back/forward restores an entry's carried items with its page, cached and refetched
  • back/forward to an evicted entry of the same route restores that entry's page (Back/forward to an evicted same-route entry that differs only in search params leaves the wrong page on screen #1030; the refetch is held, and a reader mounted during it reads the page being left). The entry is reached through a link nothing prefetches (lm-more-cold): see the first note.

Results:

  • Gate, from the worktree root: pnpm run typecheck 16/16; pnpm run test:unit:all green (router 7208 passed + 445 RSC; mini 25 + 3; cloudflare-basic 69 + 17; vite-rsc-demo 25 + 1; shared-e2e 8); pnpm run lint 0 warnings; pnpm run format clean; pnpm check:e2e-bucketing, pnpm check:e2e-parity --strict, pnpm run check:docs-api (266 identifiers) and pnpm check:bundle-guards green.
  • Router app e2e: dev project 1363 passed, 0 failed. Production project 1155 passed, 1 failed: the new Back/forward to an evicted same-route entry that differs only in search params leaves the wrong page on screen #1030 case, for the prefetch-cache reason in the first note; after the fixture change location-state.test.ts is 37 of 37 in production and 46 of 46 in dev. Semantic matrix 25 dev / 28 production. Hook render stability 8 dev / 8 production, no pin changed.
  • The held load-more and the mount-during-hold cases, --repeat-each=3 --retries=0, router app: 6 of 6 in dev, 6 of 6 in production.
  • cloudflare-basic e2e: production project 537 passed, 0 failed; the location-state, action-location-state, hook-stability and client-urls-slow suites again after the fixture change, 32 of 32. Dev project on the final tree: 475 passed, 2 skipped, 0 failed.
  • Second run on the final commit, --no-deps, of location-state, client-urls-slow and hook-render-stability (plus default-prefetch-none on cloudflare-basic): router app 61 passed in dev and 52 in production; cloudflare-basic 31 passed in dev and 31 in production. The gate was run again on the final commit with the same counts.
  • Flakes, cloudflare-basic dev only:
    • default-prefetch-none › an offscreen viewport override waits for intersection failed in 2 of 2 full dev runs before the last commit (Expected: > 920 Received: 0), passed 4 of 4 alone, and passed 2 of 2 full dev runs on origin/main (eed288b1). Cause and fix are in the last commit: the test measured a link inside a streamed segment React had not revealed yet (179ms later it had), and the three client-urls-slow tests this branch adds run next to it. With the wait: 0 of 1 full run.
    • same-route-nav › same-route nav keeps old content and never flashes the skeleton failed in 1 of 3 full dev runs on this branch and 0 of 2 on origin/main in this round. With --repeat-each=5, measured earlier on this branch, it failed 2 of 5 on the branch and 2, 2 and 3 of 5 on origin/main. Not touched.

Notes

  • Back/forward to an evicted same-route entry that differs only in search params leaves the wrong page on screen #1030 and the prefetch cache. A prefetched response is kept for prefetchCacheTTL under a key that does not include the source page, and a navigation fetch consumes it before asking the server (navigation-client.ts, consumePrefetch(wildcardKey)). So with link prefetching on (the production default is "viewport"), a return to an evicted entry whose URL a link had prefetched within the TTL was served from that cache and showed the right page before this fix too. The bug needs the server to be asked: prefetch off, the TTL expired, the cache rotated by an action, or an entry no link prefetched. The first production run of the new e2e failed for that reason (the page came back at once, with no hold to observe), so the fixture has a second load-more link to a URL of its own with prefetch="none", and the test goes through it. Without the fix that test is red in production as well.
  • usePathname / useSearchParams on back/forward (usePathname / useSearchParams change at popstate, before the destination tree commits; one 'committed entry' for URL, params and location state #1031). They still change at popstate. For the window of a refetched traversal the page on screen reads the destination's URL from them and its own entry's state from useLocationState. Documented in the hooks skill, the docs page and the CHANGELOG; not changed here.
  • A commit without a payload while another navigation is held. A shallow navigation or a nothing-to-render commit that lands while React still holds an earlier navigation applies to the tree on screen at once, which is then the page being left. An action cannot do this: its state is dropped once the user has navigated away. Not pinned by a test.
  • Cached restore that fails to render and falls through to a fetch (navigation-bridge.ts, "Failed to render from cache"): the store has already moved to the target there, so that fetch still names the target as the source. Not reachable by a test I could write without faking a render failure; left as is.
  • Cross-route evicted traversals now also tell the server the page actually being left. By reading, shared segments are revalidated against it under the normal rules; before, they were compared with the target. Not pinned by a test.
  • renderRoute seeds are copied like any entry state. A locationState seed that history could not store (a function, say) now fails the render with DataCloneError, where the hook used to hand it back as is.
  • The last commit changes a test of another feature (tests/cloudflare-basic/e2e/default-prefetch-none.test.ts): one await expect(link).toBeVisible() before the measurement, assertion unchanged. It is its own commit so it can be dropped.
  • The optimistic clientUrls() value is not cloned; the difference is documented in docs/client-urls.md and the client-urls skill. Cloning there would turn a value history cannot store into an error before the fetch.
  • Review. The first version (commit identity plus a per-reader subscription) went through the four-angle simplify review and a design review. The design review is why the delivery moved into NavigationProvider: a reader that mounted during a held navigation still read the destination's state. Applied from the simplify angles: a 600-line re-indentation of location-state.tsx undone (43 real lines), no clientUrls() work on a navigation that no group presents, renderRoute's back/forward on the production controller call, "use no memo" removed. Skipped: nothing.

ivogt added 8 commits October 4, 2026 00:31
…re it (#1029)

A mounted useLocationState reader received the destination entry's state
as soon as the router pushed the entry (on back/forward, at the popstate
event), in a render of its own. For as long as a navigation was held, the
page being left rendered with the next entry's state: a "load more" list
showed the current page's items twice until the next page landed.

The hook read history.state through useSyncExternalStore, and tx.commit
told it with a window event. A store snapshot is the latest value in every
tree at once, so it cannot stay behind in the tree a transition is holding.

Location state now rides the route-state notification. A commit site moves
an identity on the event controller (commitLocationState); the hook keeps
its slot in React state and re-reads history.state when the identity
moves. NavigationProvider already flushes that notification inside the
payload update, so the read takes the payload's lane and commits with the
destination's tree. useSyncExternalStore stays for its server snapshot.

- push, replace, back/forward (cached restore and refetch) and server-set
  state on a navigation commit with the tree.
- A revalidate:false navigation and a server action's state commit no
  tree and are delivered at once, as before.
- clientUrls(): OptimisticLocation carries the entry the navigation will
  push, so the optimistic destination reads its own state on its first
  render instead of the entry being left.
- The __rsc_locationstate window event and its dispatch sites are gone.
- A reader whose slot a commit leaves as it was sets no state.
- renderRoute: navigations commit through the controller,
  router.navigate() takes state/replace, and a popstate event is a
  back/forward onto the current history.state.

The hook carries "use no memo": compiled by the React Compiler it grows a
29-slot memo cache (296B gzip in the router chunk). Router chunk on
cloudflare-basic: 47077B -> 47100B gzip (limit 47104B).
…#1029)

Browser coverage for #1029 in the router test-app and cloudflare-basic,
dev and production, with the bodies shared in tests/shared-e2e.

- The load-more fixture loads its page through a loader (`?hold=<ms>`
  delays it) and lists carried items plus the loader's page with no
  duplicate filter. `location-state.commit` holds a navigation for 2.5s
  and checks the screen during the hold, every DOM state in between and
  every list the fixture committed; then back/forward across cached
  entries and a return to an evicted entry, with a reader in a layout the
  two #994 fixtures now share.
- The slow clientUrls group gets a reader in chrome outside the group, in
  the group layout and in each page: the optimistic destination reads the
  state its navigation carries, held content keeps its entry's state until
  the canonical commit, and a superseded or cancelled navigation leaves
  none behind.
- The render-stability pages get a useLocationState probe: same render and
  commit counts as on main.
- cloudflare-basic's dogfood unit test seeds the list's loader and pins the
  pending window through renderRoute.

No route was added or renamed.
…e duplicate filter (#1029)

- skills/hooks/state.md: the contract ("When a reader sees an entry's
  state") with a table per kind of change, and the load-more example as
  the plain concatenation. The paragraph that described the duplicate
  filter as a workaround is gone.
- skills/client-urls, docs/client-urls.md: inside the optimistic branch
  useLocationState reads the state the navigation carries; outside it and
  in held content, the committed entry's.
- skills/testing, docs/testing.md: router.navigate(url, { state, loaders })
  for a component across a pending navigation, and a popstate event as a
  back/forward.
- docs/internal/feature-map.md, feature-file-map.md: location state on the
  route-state notification, the commit sites, and why the hook is React
  state with useSyncExternalStore kept for the server snapshot only.
- docs site hooks reference, shallow-navigation design note: same rule.
…ext (#1029)

A reader that mounted while a navigation was pending read the destination
entry's state in the tree still on screen: useLocationState took its first
value from history.state, which has already moved by then. The first fix
covered readers that were mounted and left this one open.

The location state of the entry on screen is now React state owned by
NavigationProvider, set in the same update as the payload and delivered by
LocationStateContext. A transition React holds renders the destination's
value; an urgent render of the content on screen, a reader mounting there
included, renders the value of the entry being left. The hook is a context
read behind the per-reader hydration snapshot: no identity, no mirror ref, no
subscription, no frozen first read.

- Every commit site hands the controller the entry state it pushed, restored
  or merged (EventController.commitLocationState(entryState)); the snapshot
  is built from that object, under the version and clearOnReload rules, not
  from a later read of history.state. The provider takes it next to
  setPayload. A commit no payload follows (shallow navigation, a commit that
  keeps every segment, an action's state) is marked treeless and taken with
  that commit's state notification, in the batch of the location.
- A value keeps its identity when a navigation carries the reader's own
  value forward or an action changes another slot; any other object is a new
  value, and a structured clone of what the caller passed.
- An action's state is added to what readers hold instead of re-reading the
  entry, so a flash value on screen survives it.
- write()/delete() and the flash clear change history only: readers see them
  when the entry is next restored. A flash value is shown until the entry's
  state is next committed.
- Outside a NavigationProvider a reader has no location state.
- The "use no memo" directive is gone. Router chunk 47077B -> 47262B gzip;
  limit raised 46KB -> 47KB with a dated note (47167B with the directive, so
  it would not have fit either).
- renderRoute commits through the same calls: its popstate listener is the
  bridge's registerLinkInterception, and router.navigate() writes history
  only for `state` or `replace: true`.
- The optimistic clientUrls() entry state is built only when the group
  presents the target.
…on screen (#1029)

Both apps, dev and production: the load-more fixture mounts a second reader
of the carried items on demand. Pressed while the next page's loader holds
the navigation, it mounts in the page still on screen and must read that
page's entry, then change with the list. The DOM sampler flags any state in
which the late reader disagrees with the list it is in.

The cloudflare-basic dogfood test covers the same through renderRoute.

The test-app's load-more and app-version routes move to their own urls()
module, included at the same paths and names, so location-state.tsx keeps
its existing patterns untouched.
…t entry's page (#1030)

Going back or forward to an entry of the same route that differs only in
search params, after the history cache dropped it, changed the URL and the
entry's location state and left the page being left on screen: a load-more
list past 20 pages showed the wrong page's items on Back.

Cause: the partial fetch tells the server which page the client is on
(X-RSC-Router-Client-Path) from the transaction's URL, window.location at the
time the transaction is created. On a back/forward the popstate event has
already moved it to the target, so the server diffed the target against
itself: on the same route nothing revalidates, the diff is empty, and the
client commits without a tree. A different route still got its segments,
which is why only the same-route case showed.

A traversal now names the store's URL, the committed entry's, as the
leave-intercept path already did for the same reason. An intercept restore
keeps naming the intercept's source; push and replace keep the transaction's
URL.

Tests: partial-update unit tests for the three cases, and an e2e in both
apps, dev and production, that evicts through the same route and returns
with the refetch held by its loader.
…opstate limit (#1029, #1030, #1031)

The hooks skill and the docs-site page state the contract as built: a reader
gets an entry's location state with that entry's page, including a reader
that mounts while a navigation is pending, and what follows from the provider
holding it (identity of values, write()/delete(), flash, a reader outside the
provider). Both name the limit that remains: usePathname and useSearchParams
still change at popstate on back/forward (#1031).

CHANGELOG: the #1029 entry is rewritten around the same contract and lists
every change in when a reader updates that an existing app could notice; a
#1030 entry is added.

Also: the optimistic clientUrls() value is the object the navigation passed
and becomes the entry's copy at the commit (documented, not cloned);
renderRoute's navigate() history options and its popstate listener; the
internal feature maps and the shallow-navigation design note follow the
provider-owned delivery.
…before measuring

cloudflare-basic's `default-prefetch-none › an offscreen viewport override
waits for intersection` read the link's position right after the shell
hydrated. When the document streams the layout as its own segment, React
reveals it a moment later (measured: 179ms); until then the link sits in a
hidden `S:1` node and has no box: `Expected: > 920  Received: 0`.

The three client-urls-slow tests this branch adds run next to it in a local
two-worker dev run, and with them it lost that race in 2 of 2 full runs and 3
of 3 runs of the nine files around it. Alone it passed 4 of 4, and on the same
source without those three tests the nine files pass. On origin/main it passed
2 of 2 full dev runs.

Wait for the link to be visible, then measure. The assertion is unchanged.
@pkg-pr-new

pkg-pr-new Bot commented Oct 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@1033

commit: 08e88f4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant