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
9 changes: 8 additions & 1 deletion packages/vinext/src/server/app-browser-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ async function renderNavigationPayload(
async function commitSameUrlNavigatePayload(
nextElements: Promise<AppElements>,
returnValue?: ServerActionResult["returnValue"],
actionInitiationState?: AppRouterState,
): Promise<unknown> {
const navigationSnapshot = createClientNavigationRenderSnapshot(
window.location.href,
Expand All @@ -300,6 +301,7 @@ async function commitSameUrlNavigatePayload(
nextElements,
navigationSnapshot,
returnValue,
actionInitiationState,
);
}

Expand Down Expand Up @@ -853,10 +855,15 @@ function registerServerActionCallback(): void {
return commitSameUrlNavigatePayload(
Promise.resolve(AppElementsWire.decode(result.root)),
result.returnValue,
currentState,
);
}

return commitSameUrlNavigatePayload(Promise.resolve(AppElementsWire.decode(result)));
return commitSameUrlNavigatePayload(
Promise.resolve(AppElementsWire.decode(result)),
undefined,
currentState,
);
});
}

Expand Down
12 changes: 5 additions & 7 deletions packages/vinext/src/server/app-browser-navigation-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type BrowserNavigationController = {
nextElements: Promise<AppElements>,
navigationSnapshot: ClientNavigationRenderSnapshot,
returnValue?: { ok: boolean; data: unknown },
actionInitiationState?: AppRouterState,
): Promise<unknown>;
hmrReplaceTree(
nextElements: Promise<AppElements>,
Expand Down Expand Up @@ -417,7 +418,7 @@ export function createAppBrowserNavigationController(

const approval = approvePendingNavigationCommit({
activeNavigationId,
currentState,
currentState: getBrowserRouterState(),
pending,
startedNavigationId: options.navId,
});
Expand Down Expand Up @@ -476,14 +477,10 @@ export function createAppBrowserNavigationController(
nextElements: Promise<AppElements>,
navigationSnapshot: ClientNavigationRenderSnapshot,
returnValue?: { ok: boolean; data: unknown },
actionInitiationState?: AppRouterState,
): Promise<unknown> {
const currentState = getBrowserRouterState();
const currentState = actionInitiationState ?? getBrowserRouterState();
const startedNavigationId = activeNavigationId;
// Known limitation: if a same-URL navigation fully commits while this
// server action is awaiting resolveAndClassifyNavigationCommit(), the action
// can still dispatch its older payload afterward. The old pre-2c code had
// the same race, and Next.js has similar behavior. Tightening this would
// need a stronger commit-version gate than activeNavigationId alone.
const {
approvedCommit,
decision,
Expand All @@ -496,6 +493,7 @@ export function createAppBrowserNavigationController(
currentState,
navigationSnapshot,
nextElements,
getCurrentStateForApproval: getBrowserRouterState,
renderId: allocateRenderId(),
operationLane: "server-action",
startedNavigationId,
Expand Down
10 changes: 10 additions & 0 deletions packages/vinext/src/server/app-browser-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,20 @@ export function shouldHardNavigate(

export function resolvePendingNavigationCommitDisposition(options: {
activeNavigationId: number;
currentVisibleCommitVersion: number;
currentRootLayoutTreePath: string | null;
nextRootLayoutTreePath: string | null;
startedNavigationId: number;
startedVisibleCommitVersion: number;
}): PendingNavigationCommitDisposition {
if (options.startedNavigationId !== options.activeNavigationId) {
return "skip";
}

if (options.startedVisibleCommitVersion !== options.currentVisibleCommitVersion) {
return "skip";
}

if (shouldHardNavigate(options.currentRootLayoutTreePath, options.nextRootLayoutTreePath)) {
return "hard-navigate";
}
Expand All @@ -294,16 +300,20 @@ export function resolvePendingNavigationCommitDisposition(options: {

export function resolvePendingNavigationCommitDispositionDecision(options: {
activeNavigationId: number;
currentVisibleCommitVersion: number;
currentRootLayoutTreePath: string | null;
nextRootLayoutTreePath: string | null;
startedNavigationId: number;
startedVisibleCommitVersion: number;
}): PendingNavigationCommitDispositionDecision {
const disposition = resolvePendingNavigationCommitDisposition(options);
const traceFields = {
activeNavigationId: options.activeNavigationId,
currentVisibleCommitVersion: options.currentVisibleCommitVersion,
currentRootLayoutTreePath: options.currentRootLayoutTreePath,
nextRootLayoutTreePath: options.nextRootLayoutTreePath,
startedNavigationId: options.startedNavigationId,
startedVisibleCommitVersion: options.startedVisibleCommitVersion,
};

return {
Expand Down
8 changes: 7 additions & 1 deletion packages/vinext/src/server/app-browser-visible-commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ export function applyApprovedVisibleCommit(

function resolvePendingNavigationCommitDecision(options: {
activeNavigationId: number;
currentVisibleCommitVersion: number;
currentRootLayoutTreePath: string | null;
nextRootLayoutTreePath: string | null;
startedNavigationId: number;
startedVisibleCommitVersion: number;
}): CommitDecision {
const { disposition, trace } = resolvePendingNavigationCommitDispositionDecision(options);

Expand Down Expand Up @@ -123,9 +125,11 @@ export function approvePendingNavigationCommit(options: {
}): CommitApproval {
const decision = resolvePendingNavigationCommitDecision({
activeNavigationId: options.activeNavigationId,
currentVisibleCommitVersion: options.currentState.visibleCommitVersion,
currentRootLayoutTreePath: options.currentState.rootLayoutTreePath,
nextRootLayoutTreePath: options.pending.rootLayoutTreePath,
startedNavigationId: options.startedNavigationId,
startedVisibleCommitVersion: options.pending.action.operation.startedVisibleCommitVersion,
});

switch (decision.disposition) {
Expand Down Expand Up @@ -155,6 +159,7 @@ export async function resolveAndClassifyNavigationCommit(options: {
currentState: AppRouterState;
navigationSnapshot: ClientNavigationRenderSnapshot;
nextElements: Promise<AppElements>;
getCurrentStateForApproval?: () => AppRouterState;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The ? makes this optional, which means callers that don't pass it silently fall back to using the stale captured currentState for approval. That's fine for renderNavigationPayload (which calls approvePendingNavigationCommit directly and never goes through this function), but if a future caller forgets to pass it for an async path, the version gate becomes ineffective without any signal.

Consider making it required for async callers or at least adding a doc comment that explains the fallback semantics — something like "If omitted, approval uses the captured currentState, which is only safe for synchronous commit paths."

operationLane: OperationLane;
previousNextUrl?: string | null;
renderId: number;
Expand All @@ -171,9 +176,10 @@ export async function resolveAndClassifyNavigationCommit(options: {
type: options.type,
});

const approvalState = options.getCurrentStateForApproval?.() ?? options.currentState;
const approval = approvePendingNavigationCommit({
activeNavigationId: options.activeNavigationId,
currentState: options.currentState,
currentState: approvalState,
pending,
startedNavigationId: options.startedNavigationId,
});
Expand Down
Loading
Loading