UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

192 lines (172 loc) 5.16 kB
import { createMemoryHistory } from '@tanstack/history' import { _getRenderedMatches } from '../load-client' import { mergeHeaders } from './headers' import { attachRouterServerSsrUtils, getNormalizedURL, getOrigin, } from './ssr-server' import { bindSsrResponseToRequest, disposeSsrResponseDetached, } from './handlerCallback' import type { HandlerCallback } from './handlerCallback' import type { AnyHeaders } from './headers' import type { AnyRouter } from '../router' import type { ServerManifest } from '../manifest' export type RequestHandler<TRouter extends AnyRouter> = ( cb: HandlerCallback<TRouter>, ) => Promise<Response> type RequestWaiter = ((reason: unknown) => void) | undefined const requestWaiters = new WeakMap<AbortSignal, Array<RequestWaiter>>() function removeRequestWaiter( waiters: Array<RequestWaiter>, index: number, reject: (reason: unknown) => void, ) { if (waiters[index] !== reject) { return } if (index !== waiters.length - 1) { waiters[index] = undefined return } waiters.pop() while (waiters.length && waiters[waiters.length - 1] === undefined) { waiters.pop() } } export function waitForRequest<T>( value: T | PromiseLike<T>, signal: AbortSignal, onLate?: (value: T) => void, ): Promise<T> { const promise = Promise.resolve(value) if (signal.aborted) { void promise.then(onLate, () => {}) return Promise.reject(signal.reason) } return new Promise<T>((resolve, reject) => { let waiters = requestWaiters.get(signal) let index: number if (waiters) { index = waiters.push(reject) - 1 } else { const newWaiters: Array<RequestWaiter> = [reject] waiters = newWaiters index = 0 requestWaiters.set(signal, newWaiters) signal.addEventListener( 'abort', () => { requestWaiters.delete(signal) for (const rejectWaiter of newWaiters) { rejectWaiter?.(signal.reason) } newWaiters.length = 0 }, { once: true }, ) } void promise.then( (result) => { removeRequestWaiter(waiters, index, reject) if (signal.aborted) { onLate?.(result) } else { resolve(result) } }, (error) => { removeRequestWaiter(waiters, index, reject) reject(error) }, ) }) } export function createRequestHandler<TRouter extends AnyRouter>({ createRouter, request, getRouterManifest, }: { createRouter: () => TRouter request: Request getRouterManifest?: () => ServerManifest | Promise<ServerManifest> }): RequestHandler<TRouter> { return async (cb) => { request.signal.throwIfAborted() const router = createRouter() let responseOwnsCleanup = false try { attachRouterServerSsrUtils({ router, manifest: await waitForRequest(getRouterManifest?.(), request.signal), }) // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR. const { url } = getNormalizedURL(request.url, 'http://localhost') const origin = getOrigin(request) const href = url.href.replace(url.origin, '') // Create a history for the router const history = createMemoryHistory({ initialEntries: [href], }) // Update the router with the history and context router.update({ history, origin: router.options.origin ?? origin, }) await router.load({ _signal: request.signal, }) request.signal.throwIfAborted() const result = router._serverResult if (result?.type === 'redirect') { return result.redirect } await waitForRequest(router.serverSsr?.dehydrate(), request.signal) request.signal.throwIfAborted() const responseHeaders = getRequestHeaders({ router, }) request.signal.throwIfAborted() const response = await waitForRequest( cb({ request, router, responseHeaders, }), request.signal, (late) => { disposeSsrResponseDetached(late, request.signal.reason) }, ) const ssrResponse = bindSsrResponseToRequest( router, response, request.signal, ) request.signal.throwIfAborted() responseOwnsCleanup = ssrResponse.serverSsrCleanup === 'stream' return ssrResponse.response } finally { if (!responseOwnsCleanup) { // Clean up router SSR state if the callback won't handle it // (e.g., if an error occurred before the callback was invoked). // Transformed streaming response bodies clean up when consumed/cancelled. router.serverSsr?.cleanup() } } } } function getRequestHeaders(opts: { router: AnyRouter }): Headers { const matchHeaders: Array<AnyHeaders> = [] for (const match of _getRenderedMatches(opts.router.stores.matches.get())) { matchHeaders.push(match.headers) } return mergeHeaders( { 'Content-Type': 'text/html; charset=UTF-8', }, ...matchHeaders, ) }