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
15 changes: 13 additions & 2 deletions packages/react/src/runtime/pages-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ function App({
// with the browser URL, so one that renders the query falls back to a client render.
React.startTransition(() => setHydrated(true));
}
return true;
},
dispose() {},
}),
Expand All @@ -459,7 +460,7 @@ function App({
committedResources.add(resource.id);
resource
.commit()
.then(() => {
.then((committed) => {
const resolveViewTransition = viewTransitionWaiters.get(resource.id);
if (resolveViewTransition) {
viewTransitionWaiters.delete(resource.id);
Expand All @@ -468,7 +469,17 @@ function App({
const visitSettler = pendingVisitSettlers.get(resource.id);
if (visitSettler) {
pendingVisitSettlers.delete(resource.id);
visitSettler.resolve(result.pageData);
if (committed) {
visitSettler.resolve(result.pageData);
} else if (resource.method.toUpperCase() === 'GET') {
// A newer visit committed first. Settle like any abandoned visit.
visitSettler.resolve(undefined);
} else {
visitSettler.reject(new DOMException('Navigation aborted', 'AbortError'));
}
}
if (!committed) {
return;
}
if (activePendingNavigationId === resource.id) {
activePendingNavigationId = null;
Expand Down
15 changes: 13 additions & 2 deletions packages/void/src/pages/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,26 @@ export async function createVoidRouter(config: VoidRouterConfig): Promise<{
preserveState: resource.options.preserveState ?? resource.method !== 'GET',
});

let applied = true;
const useTransition = resource.options.viewTransition ?? config.viewTransitions;
if (useTransition && document.startViewTransition) {
const transition = document.startViewTransition(() => applyUpdate());
// The callback runs on a later frame. A newer visit started by then renders instead.
const transition = document.startViewTransition(() => {
if (activeNavigationId !== resource.id) {
applied = false;
return;
}
return applyUpdate();
});
await transition.updateCallbackDone;
} else {
await applyUpdate();
}

await resource.commit();
// A newer visit can commit while this page is being applied; its URL stays.
if (!applied || !(await resource.commit())) {
throw new DOMException('Navigation aborted', 'AbortError');
}
if (activeNavigationId === resource.id) {
setRouteState(currentUrl(), result.pageData.params ?? {});
}
Expand Down
33 changes: 27 additions & 6 deletions packages/void/src/pages/navigation-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export type PageNavigationResource = {
immediate: boolean;
shell: Promise<PageNavigationResult | undefined>;
abort(): void;
commit(): Promise<void>;
/** Resolves false when a newer visit has already set the URL, so nothing was committed. */
commit(): Promise<boolean>;
dispose(): void;
};

Expand Down Expand Up @@ -252,6 +253,9 @@ export async function createPageNavigationEngine(

const _prefetchCache = new PrefetchCache();
let _nextNavigationId = 0;
// The newest visit whose URL is in the address bar: the last commit, or a back/forward
// visit, which the browser has already moved to. An older visit must not commit over it.
let _urlNavigationId = 0;
let _pendingResource: PageNavigationResource | null = null;
let _committedResource: PageNavigationResource | null = null;
let _committedDeferredRegistry: DeferredRegistry | null = null;
Expand Down Expand Up @@ -853,17 +857,23 @@ export async function createPageNavigationEngine(
deferredRegistry: DeferredRegistry,
resourceId: number,
getResource: () => PageNavigationResource,
): () => Promise<void> {
): () => Promise<boolean> {
let committed = false;
return async () => {
if (committed) {
return;
return false;
}
committed = true;
const result = await shell;
if (!result) {
return;
return false;
}
// A newer visit committed while this one was being applied. Committing now would put
// back this page's URL, head, scroll and Deferred ownership over the newer page.
if (resourceId < _urlNavigationId) {
return false;
}
_urlNavigationId = resourceId;
if (_pendingResource?.id === resourceId) {
_pendingResource = null;
}
Expand Down Expand Up @@ -922,6 +932,7 @@ export async function createPageNavigationEngine(
restoreScroll(options._scrollPosition, commitTarget.hash);
}
});
return true;
};
}

Expand Down Expand Up @@ -993,11 +1004,21 @@ export async function createPageNavigationEngine(
immediate: true,
shell: Promise.resolve(undefined),
abort() {},
async commit() {},
// Same-page hash visits commit right away. They do not stop an older visit
// whose page is already on screen from committing its URL.
async commit() {
return true;
},
dispose() {},
};
}

if (options._popstate) {
// The browser has already moved to this entry. A same-page hash entry is left out
// above: it renders nothing, so an older page on screen still commits its URL.
_urlNavigationId = id;
}

const controller = new AbortController();
const deferredRegistry = new DeferredRegistry();
const commitTarget = { fetchUrl: effectiveFetchUrl, hash };
Expand All @@ -1012,7 +1033,7 @@ export async function createPageNavigationEngine(
deferredRegistry,
);

let commitResource: () => Promise<void> = async () => {};
let commitResource: () => Promise<boolean> = async () => false;
const stop = () => {
controller.abort(abortError());
deferredRegistry.dispose();
Expand Down
Loading
Loading