@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
134 lines • 5.46 kB
JSX
import * as Solid from 'solid-js/web';
import { isbot } from 'isbot';
import { createSsrStreamResponse, transformReadableStreamWithRouter, } from '@tanstack/router-core/ssr/server';
import { makeSsrSerovalPlugin } from '@tanstack/router-core';
const noop = () => { };
// Bot responses wait for the server renderer before streaming. If the request
// disconnects during that wait, unblock so the pipe can abort and clean up.
async function waitForReadyOrAbort(ready, signal) {
let cleanup = noop;
try {
await Promise.race([
ready,
new Promise((resolve) => {
const onAbort = () => resolve();
cleanup = () => signal.removeEventListener('abort', onAbort);
signal.addEventListener('abort', onAbort, { once: true });
if (signal.aborted)
resolve();
}),
]);
}
finally {
cleanup();
}
}
export const renderRouterToStream = async ({ request, router, responseHeaders, children, }) => {
const { writable, readable } = new TransformStream();
const docType = Solid.ssr('<!DOCTYPE html>');
const serializationAdapters = router.options?.serializationAdapters ||
router.options.ssr?.serializationAdapters;
const serovalPlugins = serializationAdapters?.map((adapter) => {
const plugin = makeSsrSerovalPlugin(adapter, { didRun: false });
return plugin;
});
const stream = Solid.renderToStream(() => (<>
{docType}
{children()}
</>), {
nonce: router.options.ssr?.nonce,
plugins: serovalPlugins,
});
// Solid's `pipeTo(w)` takes a single arg (no signal overload) and locks
// `w` via `w.getWriter()`. To still own the lifecycle we hand Solid a
// proxy WritableStream that forwards into an inner writer we control on
// the real TransformStream writable. Aborting the inner writer errors
// the underlying readable (which our router transform reads from),
// surfacing the cancel through the response pipeline.
//
// RESIDUAL RISK: solid-js@1.x does NOT expose a disposal hook on
// `renderToStream`, and its internal write loop swallows writer
// rejections (`writer.write(...).catch(() => {})` in
// solid-js/web/dist/server.js). So aborting the inner writer stops
// outbound bytes but does not terminate Solid's render continuation
// if a Suspense/resource never resolves — those pending promise
// continuations remain scheduled and can retain children/context/
// request references via captured closures until natural completion
// or process exit. The request-scoped router graph itself is released
// by ServerSsr.cleanup() through the router stream lifecycle, so the leak
// is bounded to whatever the user's Suspense/resource closures capture.
// A hard upstream-abort guarantee would require a disposal API in solid-js.
const innerWriter = writable.getWriter();
let writerDone = false;
const releaseWriter = () => {
try {
innerWriter.releaseLock();
}
catch {
// already released / errored
}
};
const abortSolidPipe = (reason) => {
if (writerDone)
return;
writerDone = true;
void innerWriter
.abort(reason)
.catch(() => { })
.finally(releaseWriter);
};
const onRequestAbort = () => {
abortSolidPipe(request.signal.reason);
};
// Wire request abort before the bot all-ready wait. Otherwise a disconnect
// during `await stream` can leave this callback pending forever.
if (request.signal.aborted) {
onRequestAbort();
}
else {
request.signal.addEventListener('abort', onRequestAbort, { once: true });
router.serverSsr?.onCleanup(() => {
request.signal.removeEventListener('abort', onRequestAbort);
});
}
if (isbot(request.headers.get('User-Agent'))) {
await waitForReadyOrAbort(Promise.resolve(stream), request.signal);
}
const solidWritable = new WritableStream({
write(chunk) {
return innerWriter.write(chunk);
},
close() {
writerDone = true;
return innerWriter.close().finally(releaseWriter);
},
abort(reason) {
writerDone = true;
return innerWriter.abort(reason).finally(releaseWriter);
},
});
if (!request.signal.aborted) {
try {
void Promise.resolve(stream.pipeTo(solidWritable)).catch((err) => {
if (writerDone ||
err?.name === 'AbortError' ||
err?.code === 'ABORT_ERR')
return;
console.error('Error in Solid render stream:', err);
abortSolidPipe(err);
});
}
catch (err) {
if (err?.name !== 'AbortError' && err?.code !== 'ABORT_ERR') {
console.error('Error in Solid render stream:', err);
}
abortSolidPipe(err);
}
}
const responseStream = transformReadableStreamWithRouter(router, readable, { onAbort: abortSolidPipe });
return createSsrStreamResponse(router, new Response(responseStream, {
status: router.stores.statusCode.get(),
headers: responseHeaders,
}));
};
//# sourceMappingURL=renderRouterToStream.jsx.map