Skip to content

Commit 8345920

Browse files
Refactor RSC handling to improve performance and error management
1 parent b9954a2 commit 8345920

File tree

7 files changed

+42
-18
lines changed

7 files changed

+42
-18
lines changed

node_package/src/RSCPayloadGenerator.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PassThrough } from 'stream';
1+
import { PassThrough, Readable } from 'stream';
22
import {
33
RailsContextWithServerComponentCapabilities,
44
RSCPayloadStreamInfo,
@@ -33,6 +33,15 @@ const DEFAULT_TTL = 300000;
3333

3434
export const clearRSCPayloadStreams = (railsContext: RailsContextWithServerComponentCapabilities) => {
3535
const { renderRequestId } = railsContext.componentSpecificMetadata;
36+
// Close any active streams before clearing
37+
const streams = rscPayloadStreams.get(renderRequestId);
38+
if (streams) {
39+
streams.forEach(({ stream }) => {
40+
if (typeof (stream as Readable).destroy === 'function') {
41+
(stream as Readable).destroy();
42+
}
43+
});
44+
}
3645
rscPayloadStreams.delete(renderRequestId);
3746
rscPayloadCallbacks.delete(renderRequestId);
3847

node_package/src/ReactOnRailsRSC.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from './streamServerRenderedReactComponent.ts';
2020
import loadJsonFile from './loadJsonFile.ts';
2121

22-
let serverRenderer: ReturnType<typeof buildServerRenderer> | undefined;
22+
let serverRendererPromise: Promise<ReturnType<typeof buildServerRenderer>> | undefined;
2323

2424
const streamRenderRSCComponent = (
2525
reactRenderingResult: StreamableComponentResult,
@@ -49,12 +49,13 @@ const streamRenderRSCComponent = (
4949
};
5050

5151
const initializeAndRender = async () => {
52-
if (!serverRenderer) {
53-
const reactClientManifest = await loadJsonFile<BundleManifest>(reactClientManifestFileName);
54-
serverRenderer = buildServerRenderer(reactClientManifest);
52+
if (!serverRendererPromise) {
53+
serverRendererPromise = loadJsonFile<BundleManifest>(reactClientManifestFileName).then(
54+
(reactClientManifest) => buildServerRenderer(reactClientManifest),
55+
);
5556
}
5657

57-
const { renderToPipeableStream } = serverRenderer;
58+
const { renderToPipeableStream } = await serverRendererPromise;
5859
const rscStream = renderToPipeableStream(await reactRenderingResult, {
5960
onError: (err) => {
6061
const error = convertToError(err);

node_package/src/getReactServerComponent.client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ const fetchRSC = ({ componentName, componentProps, railsContext }: ClientGetReac
4646
const propsString = JSON.stringify(componentProps);
4747
const { rscPayloadGenerationUrlPath } = railsContext;
4848
const strippedUrlPath = rscPayloadGenerationUrlPath?.replace(/^\/|\/$/g, '');
49-
return createFromFetch(fetch(`/${strippedUrlPath}/${componentName}?props=${propsString}`));
49+
const encodedParams = new URLSearchParams({ props: propsString }).toString();
50+
return createFromFetch(fetch(`/${strippedUrlPath}/${componentName}?${encodedParams}`));
5051
};
5152

5253
const createRSCStreamFromArray = (payloads: string[]) => {

node_package/src/getReactServerComponent.server.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,23 @@ type GetReactServerComponentOnServerProps = {
1010
railsContext: RailsContext;
1111
};
1212

13-
let clientRenderer: ReturnType<typeof buildClientRenderer> | undefined;
13+
let clientRendererPromise: Promise<ReturnType<typeof buildClientRenderer>> | undefined;
1414

1515
const createFromReactOnRailsNodeStream = async (
1616
stream: NodeJS.ReadableStream,
1717
reactServerManifestFileName: string,
1818
reactClientManifestFileName: string,
1919
) => {
20-
if (!clientRenderer) {
21-
const [reactServerManifest, reactClientManifest] = await Promise.all([
20+
if (!clientRendererPromise) {
21+
clientRendererPromise = Promise.all([
2222
loadJsonFile<BundleManifest>(reactServerManifestFileName),
2323
loadJsonFile<BundleManifest>(reactClientManifestFileName),
24-
]);
25-
clientRenderer = buildClientRenderer(reactClientManifest, reactServerManifest);
24+
]).then(([reactServerManifest, reactClientManifest]) =>
25+
buildClientRenderer(reactClientManifest, reactServerManifest),
26+
);
2627
}
2728

28-
const { createFromNodeStream } = clientRenderer;
29+
const { createFromNodeStream } = await clientRendererPromise;
2930
const transformedStream = transformRSCStream(stream);
3031
return createFromNodeStream<React.ReactNode>(transformedStream);
3132
};

node_package/src/injectRSCPayload.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ export default function injectRSCPayload(
6565
try {
6666
const rscPromises: Promise<void>[] = [];
6767

68+
if (!ReactOnRails.onRSCPayloadGenerated) {
69+
// This should never occur in normal operation and indicates a bug in react_on_rails that needs to be fixed.
70+
// While not fatal, missing RSC payload injection will degrade performance by forcing
71+
console.error(
72+
'ReactOnRails Error: ReactOnRails.onRSCPayloadGenerated is not defined. RSC payloads cannot be injected.',
73+
);
74+
}
75+
6876
ReactOnRails.onRSCPayloadGenerated?.(railsContext, (streamInfo) => {
6977
const { stream, props, componentName } = streamInfo;
7078
const cacheKey = createRSCPayloadKey(componentName, props, railsContext);
@@ -123,12 +131,17 @@ export default function injectRSCPayload(
123131
rscPromise = startRSC();
124132
}
125133
rscPromise
126-
.finally(() => {
127-
ReactOnRails.clearRSCPayloadStreams?.(railsContext);
128-
})
129134
.then(() => {
130135
resultStream.end();
131136
})
137+
.finally(() => {
138+
if (!ReactOnRails.clearRSCPayloadStreams) {
139+
console.error('ReactOnRails Error: clearRSCPayloadStreams is not a function');
140+
return;
141+
}
142+
143+
ReactOnRails.clearRSCPayloadStreams(railsContext);
144+
})
132145
.catch((err: unknown) => resultStream.emit('error', err));
133146
});
134147

node_package/src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export type RailsContext = {
5757
// The react-on-rails package uses 'unknown' for these parameters to avoid direct dependency.
5858
// This ensures that if the communication protocol between the node renderer and the Rails server changes,
5959
// we don't need to update this type or introduce a breaking change.
60-
serverSideRSCPayloadParameters: unknown;
60+
serverSideRSCPayloadParameters?: unknown;
6161
reactClientManifestFileName?: string;
6262
reactServerClientManifestFileName?: string;
6363
}

node_package/tests/SuspenseHydration.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ async function renderAndHydrate() {
196196
await act(async () => {
197197
const secondChunk = await writeSecondChunk();
198198
expect(secondChunk).toContain('script');
199-
console.log(secondChunk);
200199
});
201200
await waitFor(() => {
202201
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();

0 commit comments

Comments
 (0)