Skip to content

Commit 4f7f773

Browse files
Move files to prepare for Removing react-dom/server from the RSC bundle PR (#1889)
- Moves `packages/react-on-rails/handleError.ts` to `packages/react-on-rails/src/generateRenderingErrorMessage.ts`. - Move some utility functions from `packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts` to `packages/react-on-rails-pro/src/streamingUtils.ts`
1 parent 5e02c7a commit 4f7f773

File tree

7 files changed

+262
-234
lines changed

7 files changed

+262
-234
lines changed

packages/react-on-rails-pro/src/ReactOnRailsRSC.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
streamServerRenderedComponent,
3131
StreamingTrackers,
3232
transformRenderStreamChunksToResultObject,
33-
} from './streamServerRenderedReactComponent.ts';
33+
} from './streamingUtils.ts';
3434
import loadJsonFile from './loadJsonFile.ts';
3535

3636
let serverRendererPromise: Promise<ReturnType<typeof buildServerRenderer>> | undefined;

packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts

Lines changed: 7 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -12,150 +12,23 @@
1212
* https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
1313
*/
1414

15-
import * as React from 'react';
16-
import { PassThrough, Readable } from 'stream';
15+
import { Readable } from 'stream';
1716

18-
import createReactOutput from 'react-on-rails/createReactOutput';
19-
import { isPromise, isServerRenderHash } from 'react-on-rails/isServerRenderResult';
20-
import buildConsoleReplay from 'react-on-rails/buildConsoleReplay';
2117
import handleError from 'react-on-rails/handleError';
2218
import { renderToPipeableStream } from 'react-on-rails/ReactDOMServer';
23-
import { createResultObject, convertToError, validateComponent } from 'react-on-rails/serverRenderUtils';
19+
import { convertToError } from 'react-on-rails/serverRenderUtils';
2420
import {
2521
assertRailsContextWithServerStreamingCapabilities,
2622
RenderParams,
2723
StreamRenderState,
2824
StreamableComponentResult,
29-
PipeableOrReadableStream,
30-
RailsContextWithServerStreamingCapabilities,
31-
assertRailsContextWithServerComponentMetadata,
3225
} from 'react-on-rails/types';
33-
import * as ComponentRegistry from './ComponentRegistry.ts';
3426
import injectRSCPayload from './injectRSCPayload.ts';
35-
import PostSSRHookTracker from './PostSSRHookTracker.ts';
36-
import RSCRequestTracker from './RSCRequestTracker.ts';
37-
38-
type BufferedEvent = {
39-
event: 'data' | 'error' | 'end';
40-
data: unknown;
41-
};
42-
43-
/**
44-
* Creates a new Readable stream that safely buffers all events from the input stream until reading begins.
45-
*
46-
* This function solves two important problems:
47-
* 1. Error handling: If an error occurs on the source stream before error listeners are attached,
48-
* it would normally crash the process. This wrapper buffers error events until reading begins,
49-
* ensuring errors are properly handled once listeners are ready.
50-
* 2. Event ordering: All events (data, error, end) are buffered and replayed in the exact order
51-
* they were received, maintaining the correct sequence even if events occur before reading starts.
52-
*
53-
* @param stream - The source Readable stream to buffer
54-
* @returns {Object} An object containing:
55-
* - stream: A new Readable stream that will buffer and replay all events
56-
* - emitError: A function to manually emit errors into the stream
57-
*/
58-
const bufferStream = (stream: Readable) => {
59-
const bufferedEvents: BufferedEvent[] = [];
60-
let startedReading = false;
61-
62-
const listeners = (['data', 'error', 'end'] as const).map((event) => {
63-
const listener = (data: unknown) => {
64-
if (!startedReading) {
65-
bufferedEvents.push({ event, data });
66-
}
67-
};
68-
stream.on(event, listener);
69-
return { event, listener };
70-
});
71-
72-
const bufferedStream = new Readable({
73-
read() {
74-
if (startedReading) return;
75-
startedReading = true;
76-
77-
// Remove initial listeners
78-
listeners.forEach(({ event, listener }) => stream.off(event, listener));
79-
const handleEvent = ({ event, data }: BufferedEvent) => {
80-
if (event === 'data') {
81-
this.push(data);
82-
} else if (event === 'error') {
83-
this.emit('error', data);
84-
} else {
85-
this.push(null);
86-
}
87-
};
88-
89-
// Replay buffered events
90-
bufferedEvents.forEach(handleEvent);
91-
92-
// Attach new listeners for future events
93-
(['data', 'error', 'end'] as const).forEach((event) => {
94-
stream.on(event, (data: unknown) => handleEvent({ event, data }));
95-
});
96-
},
97-
});
98-
99-
return {
100-
stream: bufferedStream,
101-
emitError: (error: unknown) => {
102-
if (startedReading) {
103-
bufferedStream.emit('error', error);
104-
} else {
105-
bufferedEvents.push({ event: 'error', data: error });
106-
}
107-
},
108-
};
109-
};
110-
111-
export const transformRenderStreamChunksToResultObject = (renderState: StreamRenderState) => {
112-
const consoleHistory = console.history;
113-
let previouslyReplayedConsoleMessages = 0;
114-
115-
const transformStream = new PassThrough({
116-
transform(chunk: Buffer, _, callback) {
117-
const htmlChunk = chunk.toString();
118-
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages);
119-
previouslyReplayedConsoleMessages = consoleHistory?.length || 0;
120-
const jsonChunk = JSON.stringify(createResultObject(htmlChunk, consoleReplayScript, renderState));
121-
this.push(`${jsonChunk}\n`);
122-
123-
// Reset the render state to ensure that the error is not carried over to the next chunk
124-
// eslint-disable-next-line no-param-reassign
125-
renderState.error = undefined;
126-
// eslint-disable-next-line no-param-reassign
127-
renderState.hasErrors = false;
128-
129-
callback();
130-
},
131-
});
132-
133-
let pipedStream: PipeableOrReadableStream | null = null;
134-
const pipeToTransform = (pipeableStream: PipeableOrReadableStream) => {
135-
pipeableStream.pipe(transformStream);
136-
pipedStream = pipeableStream;
137-
};
138-
// We need to wrap the transformStream in a Readable stream to properly handle errors:
139-
// 1. If we returned transformStream directly, we couldn't emit errors into it externally
140-
// 2. If an error is emitted into the transformStream, it would cause the render to fail
141-
// 3. By wrapping in Readable.from(), we can explicitly emit errors into the readableStream without affecting the transformStream
142-
// Note: Readable.from can merge multiple chunks into a single chunk, so we need to ensure that we can separate them later
143-
const { stream: readableStream, emitError } = bufferStream(transformStream);
144-
145-
const writeChunk = (chunk: string) => transformStream.write(chunk);
146-
const endStream = () => {
147-
transformStream.end();
148-
if (pipedStream && 'abort' in pipedStream) {
149-
pipedStream.abort();
150-
}
151-
};
152-
return { readableStream, pipeToTransform, writeChunk, emitError, endStream };
153-
};
154-
155-
export type StreamingTrackers = {
156-
postSSRHookTracker: PostSSRHookTracker;
157-
rscRequestTracker: RSCRequestTracker;
158-
};
27+
import {
28+
StreamingTrackers,
29+
transformRenderStreamChunksToResultObject,
30+
streamServerRenderedComponent,
31+
} from './streamingUtils.ts';
15932

16033
const streamRenderReactComponent = (
16134
reactRenderingResult: StreamableComponentResult,
@@ -228,102 +101,6 @@ const streamRenderReactComponent = (
228101
return readableStream;
229102
};
230103

231-
type StreamRenderer<T, P extends RenderParams> = (
232-
reactElement: StreamableComponentResult,
233-
options: P,
234-
streamingTrackers: StreamingTrackers,
235-
) => T;
236-
237-
/**
238-
* This module implements request-scoped tracking for React Server Components (RSC)
239-
* and post-SSR hooks using local tracker instances per request.
240-
*
241-
* DESIGN PRINCIPLES:
242-
* - Each request gets its own PostSSRHookTracker and RSCRequestTracker instances
243-
* - State is automatically garbage collected when request completes
244-
* - No shared state between concurrent requests
245-
* - Simple, predictable cleanup lifecycle
246-
*
247-
* TRACKER RESPONSIBILITIES:
248-
* - PostSSRHookTracker: Manages hooks that run after SSR completes
249-
* - RSCRequestTracker: Handles RSC payload generation and stream tracking
250-
* - Both inject their capabilities into the Rails context for component access
251-
*/
252-
253-
export const streamServerRenderedComponent = <T, P extends RenderParams>(
254-
options: P,
255-
renderStrategy: StreamRenderer<T, P>,
256-
): T => {
257-
const { name: componentName, domNodeId, trace, props, railsContext, throwJsErrors } = options;
258-
259-
assertRailsContextWithServerComponentMetadata(railsContext);
260-
const postSSRHookTracker = new PostSSRHookTracker();
261-
const rscRequestTracker = new RSCRequestTracker(railsContext);
262-
const streamingTrackers = {
263-
postSSRHookTracker,
264-
rscRequestTracker,
265-
};
266-
267-
const railsContextWithStreamingCapabilities: RailsContextWithServerStreamingCapabilities = {
268-
...railsContext,
269-
addPostSSRHook: postSSRHookTracker.addPostSSRHook.bind(postSSRHookTracker),
270-
getRSCPayloadStream: rscRequestTracker.getRSCPayloadStream.bind(rscRequestTracker),
271-
};
272-
273-
const optionsWithStreamingCapabilities = {
274-
...options,
275-
railsContext: railsContextWithStreamingCapabilities,
276-
};
277-
278-
try {
279-
const componentObj = ComponentRegistry.get(componentName);
280-
validateComponent(componentObj, componentName);
281-
282-
const reactRenderingResult = createReactOutput({
283-
componentObj,
284-
domNodeId,
285-
trace,
286-
props,
287-
railsContext: railsContextWithStreamingCapabilities,
288-
});
289-
290-
if (isServerRenderHash(reactRenderingResult)) {
291-
throw new Error('Server rendering of streams is not supported for server render hashes.');
292-
}
293-
294-
if (isPromise(reactRenderingResult)) {
295-
const promiseAfterRejectingHash = reactRenderingResult.then((result) => {
296-
if (!React.isValidElement(result)) {
297-
throw new Error(
298-
`Invalid React element detected while rendering ${componentName}. If you are trying to stream a component registered as a render function, ` +
299-
`please ensure that the render function returns a valid React component, not a server render hash. ` +
300-
`This error typically occurs when the render function does not return a React element or returns an incorrect type.`,
301-
);
302-
}
303-
return result;
304-
});
305-
return renderStrategy(promiseAfterRejectingHash, optionsWithStreamingCapabilities, streamingTrackers);
306-
}
307-
308-
return renderStrategy(reactRenderingResult, optionsWithStreamingCapabilities, streamingTrackers);
309-
} catch (e) {
310-
const { readableStream, writeChunk, emitError, endStream } = transformRenderStreamChunksToResultObject({
311-
hasErrors: true,
312-
isShellReady: false,
313-
result: null,
314-
});
315-
if (throwJsErrors) {
316-
emitError(e);
317-
}
318-
319-
const error = convertToError(e);
320-
const htmlResult = handleError({ e: error, name: componentName, serverSide: true });
321-
writeChunk(htmlResult);
322-
endStream();
323-
return readableStream as T;
324-
}
325-
};
326-
327104
const streamServerRenderedReactComponent = (options: RenderParams): Readable =>
328105
streamServerRenderedComponent(options, streamRenderReactComponent);
329106

0 commit comments

Comments
 (0)