275 lines
11 KiB
JavaScript
275 lines
11 KiB
JavaScript
import { ACTION_REFRESH, ACTION_SERVER_ACTION, ACTION_NAVIGATE, ACTION_RESTORE, ACTION_HMR_REFRESH, PrefetchKind, ACTION_PREFETCH } from './router-reducer/router-reducer-types';
|
|
import { reducer } from './router-reducer/router-reducer';
|
|
import { startTransition } from 'react';
|
|
import { isThenable } from '../../shared/lib/is-thenable';
|
|
import { prefetch as prefetchWithSegmentCache } from './segment-cache';
|
|
import { dispatchAppRouterAction } from './use-action-queue';
|
|
import { addBasePath } from '../add-base-path';
|
|
import { createPrefetchURL, isExternalURL } from './app-router';
|
|
import { prefetchReducer } from './router-reducer/reducers/prefetch-reducer';
|
|
import { setLinkForCurrentNavigation } from './links';
|
|
function runRemainingActions(actionQueue, setState) {
|
|
if (actionQueue.pending !== null) {
|
|
actionQueue.pending = actionQueue.pending.next;
|
|
if (actionQueue.pending !== null) {
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
runAction({
|
|
actionQueue,
|
|
action: actionQueue.pending,
|
|
setState
|
|
});
|
|
} else {
|
|
// No more actions are pending, check if a refresh is needed
|
|
if (actionQueue.needsRefresh) {
|
|
actionQueue.needsRefresh = false;
|
|
actionQueue.dispatch({
|
|
type: ACTION_REFRESH,
|
|
origin: window.location.origin
|
|
}, setState);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
async function runAction(param) {
|
|
let { actionQueue, action, setState } = param;
|
|
const prevState = actionQueue.state;
|
|
actionQueue.pending = action;
|
|
const payload = action.payload;
|
|
const actionResult = actionQueue.action(prevState, payload);
|
|
function handleResult(nextState) {
|
|
// if we discarded this action, the state should also be discarded
|
|
if (action.discarded) {
|
|
return;
|
|
}
|
|
actionQueue.state = nextState;
|
|
runRemainingActions(actionQueue, setState);
|
|
action.resolve(nextState);
|
|
}
|
|
// if the action is a promise, set up a callback to resolve it
|
|
if (isThenable(actionResult)) {
|
|
actionResult.then(handleResult, (err)=>{
|
|
runRemainingActions(actionQueue, setState);
|
|
action.reject(err);
|
|
});
|
|
} else {
|
|
handleResult(actionResult);
|
|
}
|
|
}
|
|
function dispatchAction(actionQueue, payload, setState) {
|
|
let resolvers = {
|
|
resolve: setState,
|
|
reject: ()=>{}
|
|
};
|
|
// most of the action types are async with the exception of restore
|
|
// it's important that restore is handled quickly since it's fired on the popstate event
|
|
// and we don't want to add any delay on a back/forward nav
|
|
// this only creates a promise for the async actions
|
|
if (payload.type !== ACTION_RESTORE) {
|
|
// Create the promise and assign the resolvers to the object.
|
|
const deferredPromise = new Promise((resolve, reject)=>{
|
|
resolvers = {
|
|
resolve,
|
|
reject
|
|
};
|
|
});
|
|
startTransition(()=>{
|
|
// we immediately notify React of the pending promise -- the resolver is attached to the action node
|
|
// and will be called when the associated action promise resolves
|
|
setState(deferredPromise);
|
|
});
|
|
}
|
|
const newAction = {
|
|
payload,
|
|
next: null,
|
|
resolve: resolvers.resolve,
|
|
reject: resolvers.reject
|
|
};
|
|
// Check if the queue is empty
|
|
if (actionQueue.pending === null) {
|
|
// The queue is empty, so add the action and start it immediately
|
|
// Mark this action as the last in the queue
|
|
actionQueue.last = newAction;
|
|
runAction({
|
|
actionQueue,
|
|
action: newAction,
|
|
setState
|
|
});
|
|
} else if (payload.type === ACTION_NAVIGATE || payload.type === ACTION_RESTORE) {
|
|
// Navigations (including back/forward) take priority over any pending actions.
|
|
// Mark the pending action as discarded (so the state is never applied) and start the navigation action immediately.
|
|
actionQueue.pending.discarded = true;
|
|
// The rest of the current queue should still execute after this navigation.
|
|
// (Note that it can't contain any earlier navigations, because we always put those into `actionQueue.pending` by calling `runAction`)
|
|
newAction.next = actionQueue.pending.next;
|
|
// if the pending action was a server action, mark the queue as needing a refresh once events are processed
|
|
if (actionQueue.pending.payload.type === ACTION_SERVER_ACTION) {
|
|
actionQueue.needsRefresh = true;
|
|
}
|
|
runAction({
|
|
actionQueue,
|
|
action: newAction,
|
|
setState
|
|
});
|
|
} else {
|
|
// The queue is not empty, so add the action to the end of the queue
|
|
// It will be started by runRemainingActions after the previous action finishes
|
|
if (actionQueue.last !== null) {
|
|
actionQueue.last.next = newAction;
|
|
}
|
|
actionQueue.last = newAction;
|
|
}
|
|
}
|
|
let globalActionQueue = null;
|
|
export function createMutableActionQueue(initialState, instrumentationHooks) {
|
|
const actionQueue = {
|
|
state: initialState,
|
|
dispatch: (payload, setState)=>dispatchAction(actionQueue, payload, setState),
|
|
action: async (state, action)=>{
|
|
const result = reducer(state, action);
|
|
return result;
|
|
},
|
|
pending: null,
|
|
last: null,
|
|
onRouterTransitionStart: instrumentationHooks !== null && typeof instrumentationHooks.onRouterTransitionStart === 'function' ? instrumentationHooks.onRouterTransitionStart : null
|
|
};
|
|
if (typeof window !== 'undefined') {
|
|
// The action queue is lazily created on hydration, but after that point
|
|
// it doesn't change. So we can store it in a global rather than pass
|
|
// it around everywhere via props/context.
|
|
if (globalActionQueue !== null) {
|
|
throw Object.defineProperty(new Error('Internal Next.js Error: createMutableActionQueue was called more ' + 'than once'), "__NEXT_ERROR_CODE", {
|
|
value: "E624",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
globalActionQueue = actionQueue;
|
|
}
|
|
return actionQueue;
|
|
}
|
|
export function getCurrentAppRouterState() {
|
|
return globalActionQueue !== null ? globalActionQueue.state : null;
|
|
}
|
|
function getAppRouterActionQueue() {
|
|
if (globalActionQueue === null) {
|
|
throw Object.defineProperty(new Error('Internal Next.js error: Router action dispatched before initialization.'), "__NEXT_ERROR_CODE", {
|
|
value: "E668",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
return globalActionQueue;
|
|
}
|
|
function getProfilingHookForOnNavigationStart() {
|
|
if (globalActionQueue !== null) {
|
|
return globalActionQueue.onRouterTransitionStart;
|
|
}
|
|
return null;
|
|
}
|
|
export function dispatchNavigateAction(href, navigateType, shouldScroll, linkInstanceRef) {
|
|
// TODO: This stuff could just go into the reducer. Leaving as-is for now
|
|
// since we're about to rewrite all the router reducer stuff anyway.
|
|
const url = new URL(addBasePath(href), location.href);
|
|
if (process.env.__NEXT_APP_NAV_FAIL_HANDLING) {
|
|
window.next.__pendingUrl = url;
|
|
}
|
|
setLinkForCurrentNavigation(linkInstanceRef);
|
|
const onRouterTransitionStart = getProfilingHookForOnNavigationStart();
|
|
if (onRouterTransitionStart !== null) {
|
|
onRouterTransitionStart(href, navigateType);
|
|
}
|
|
dispatchAppRouterAction({
|
|
type: ACTION_NAVIGATE,
|
|
url,
|
|
isExternalUrl: isExternalURL(url),
|
|
locationSearch: location.search,
|
|
shouldScroll,
|
|
navigateType,
|
|
allowAliasing: true
|
|
});
|
|
}
|
|
export function dispatchTraverseAction(href, tree) {
|
|
const onRouterTransitionStart = getProfilingHookForOnNavigationStart();
|
|
if (onRouterTransitionStart !== null) {
|
|
onRouterTransitionStart(href, 'traverse');
|
|
}
|
|
dispatchAppRouterAction({
|
|
type: ACTION_RESTORE,
|
|
url: new URL(href),
|
|
tree
|
|
});
|
|
}
|
|
/**
|
|
* The app router that is exposed through `useRouter`. These are public API
|
|
* methods. Internal Next.js code should call the lower level methods directly
|
|
* (although there's lots of existing code that doesn't do that).
|
|
*/ export const publicAppRouterInstance = {
|
|
back: ()=>window.history.back(),
|
|
forward: ()=>window.history.forward(),
|
|
prefetch: process.env.__NEXT_CLIENT_SEGMENT_CACHE ? // data in the router reducer state; it writes into a global mutable
|
|
// cache. So we don't need to dispatch an action.
|
|
(href, options)=>{
|
|
const actionQueue = getAppRouterActionQueue();
|
|
prefetchWithSegmentCache(href, actionQueue.state.nextUrl, actionQueue.state.tree, (options == null ? void 0 : options.kind) === PrefetchKind.FULL);
|
|
} : (href, options)=>{
|
|
// Use the old prefetch implementation.
|
|
const actionQueue = getAppRouterActionQueue();
|
|
const url = createPrefetchURL(href);
|
|
if (url !== null) {
|
|
var _options_kind;
|
|
// The prefetch reducer doesn't actually update any state or
|
|
// trigger a rerender. It just writes to a mutable cache. So we
|
|
// shouldn't bother calling setState/dispatch; we can just re-run
|
|
// the reducer directly using the current state.
|
|
// TODO: Refactor this away from a "reducer" so it's
|
|
// less confusing.
|
|
prefetchReducer(actionQueue.state, {
|
|
type: ACTION_PREFETCH,
|
|
url,
|
|
kind: (_options_kind = options == null ? void 0 : options.kind) != null ? _options_kind : PrefetchKind.FULL
|
|
});
|
|
}
|
|
},
|
|
replace: (href, options)=>{
|
|
startTransition(()=>{
|
|
var _options_scroll;
|
|
dispatchNavigateAction(href, 'replace', (_options_scroll = options == null ? void 0 : options.scroll) != null ? _options_scroll : true, null);
|
|
});
|
|
},
|
|
push: (href, options)=>{
|
|
startTransition(()=>{
|
|
var _options_scroll;
|
|
dispatchNavigateAction(href, 'push', (_options_scroll = options == null ? void 0 : options.scroll) != null ? _options_scroll : true, null);
|
|
});
|
|
},
|
|
refresh: ()=>{
|
|
startTransition(()=>{
|
|
dispatchAppRouterAction({
|
|
type: ACTION_REFRESH,
|
|
origin: window.location.origin
|
|
});
|
|
});
|
|
},
|
|
hmrRefresh: ()=>{
|
|
if (process.env.NODE_ENV !== 'development') {
|
|
throw Object.defineProperty(new Error('hmrRefresh can only be used in development mode. Please use refresh instead.'), "__NEXT_ERROR_CODE", {
|
|
value: "E485",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
} else {
|
|
startTransition(()=>{
|
|
dispatchAppRouterAction({
|
|
type: ACTION_HMR_REFRESH,
|
|
origin: window.location.origin
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
// Exists for debugging purposes. Don't use in application code.
|
|
if (typeof window !== 'undefined' && window.next) {
|
|
window.next.router = publicAppRouterInstance;
|
|
}
|
|
|
|
//# sourceMappingURL=app-router-instance.js.map
|