Skip to content

Commit e1949c4

Browse files
refactor: update RailsContext types to include server component capabilities and streamline related functions
1 parent b69bb79 commit e1949c4

File tree

8 files changed

+86
-58
lines changed

8 files changed

+86
-58
lines changed

eslint.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ const config = tsEslint.config([
162162
parserOptions: {
163163
projectService: {
164164
allowDefaultProject: ['eslint.config.ts', 'knip.ts', 'node_package/tests/*.test.{ts,tsx}'],
165+
// Needed because `import * as ... from` instead of `import ... from` doesn't work in this file
166+
// for some imports.
165167
defaultProject: 'tsconfig.eslint.json',
166168
},
167169
},

node_package/src/RSCPayloadGenerator.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import { PassThrough } from 'stream';
2-
import { RailsContext, RSCPayloadStreamInfo, RSCPayloadCallback } from './types/index.ts';
2+
import {
3+
RailsContextWithServerComponentCapabilities,
4+
RSCPayloadStreamInfo,
5+
RSCPayloadCallback,
6+
} from './types/index.ts';
37

48
declare global {
59
function generateRSCPayload(
610
componentName: string,
711
props: unknown,
8-
railsContext: RailsContext,
12+
railsContext: RailsContextWithServerComponentCapabilities,
913
): Promise<NodeJS.ReadableStream>;
1014
}
1115

12-
const mapRailsContextToRSCPayloadStreams = new Map<RailsContext, RSCPayloadStreamInfo[]>();
16+
const mapRailsContextToRSCPayloadStreams = new Map<string, RSCPayloadStreamInfo[]>();
1317

14-
const rscPayloadCallbacks = new Map<RailsContext, Array<RSCPayloadCallback>>();
18+
const rscPayloadCallbacks = new Map<string, Array<RSCPayloadCallback>>();
1519

1620
/**
1721
* Registers a callback to be executed when RSC payloads are generated.
@@ -27,13 +31,17 @@ const rscPayloadCallbacks = new Map<RailsContext, Array<RSCPayloadCallback>>();
2731
* @param railsContext - Context for the current request
2832
* @param callback - Function to call when an RSC payload is generated
2933
*/
30-
export const onRSCPayloadGenerated = (railsContext: RailsContext, callback: RSCPayloadCallback) => {
31-
const callbacks = rscPayloadCallbacks.get(railsContext) || [];
34+
export const onRSCPayloadGenerated = (
35+
railsContext: RailsContextWithServerComponentCapabilities,
36+
callback: RSCPayloadCallback,
37+
) => {
38+
const { renderRequestId } = railsContext.componentSpecificMetadata;
39+
const callbacks = rscPayloadCallbacks.get(renderRequestId) || [];
3240
callbacks.push(callback);
33-
rscPayloadCallbacks.set(railsContext, callbacks);
41+
rscPayloadCallbacks.set(renderRequestId, callbacks);
3442

3543
// Call callback for any existing streams for this context
36-
const existingStreams = mapRailsContextToRSCPayloadStreams.get(railsContext) || [];
44+
const existingStreams = mapRailsContextToRSCPayloadStreams.get(renderRequestId) || [];
3745
existingStreams.forEach((streamInfo) => callback(streamInfo));
3846
};
3947

@@ -56,7 +64,7 @@ export const onRSCPayloadGenerated = (railsContext: RailsContext, callback: RSCP
5664
export const getRSCPayloadStream = async (
5765
componentName: string,
5866
props: unknown,
59-
railsContext: RailsContext,
67+
railsContext: RailsContextWithServerComponentCapabilities,
6068
): Promise<NodeJS.ReadableStream> => {
6169
if (typeof generateRSCPayload !== 'function') {
6270
throw new Error(
@@ -66,8 +74,9 @@ export const getRSCPayloadStream = async (
6674
);
6775
}
6876

77+
const { renderRequestId } = railsContext.componentSpecificMetadata;
6978
const stream = await generateRSCPayload(componentName, props, railsContext);
70-
const streams = mapRailsContextToRSCPayloadStreams.get(railsContext) ?? [];
79+
const streams = mapRailsContextToRSCPayloadStreams.get(renderRequestId) ?? [];
7180
const stream1 = new PassThrough();
7281
stream.pipe(stream1);
7382
const stream2 = new PassThrough();
@@ -79,25 +88,29 @@ export const getRSCPayloadStream = async (
7988
stream: stream2,
8089
};
8190
streams.push(streamInfo);
82-
mapRailsContextToRSCPayloadStreams.set(railsContext, streams);
91+
mapRailsContextToRSCPayloadStreams.set(renderRequestId, streams);
8392

8493
// Notify callbacks about the new stream in a sync manner to maintain proper hydration timing
8594
// as described in the comment above onRSCPayloadGenerated
86-
const callbacks = rscPayloadCallbacks.get(railsContext) || [];
95+
const callbacks = rscPayloadCallbacks.get(renderRequestId) || [];
8796
callbacks.forEach((callback) => callback(streamInfo));
8897

8998
return stream1;
9099
};
91100

92101
export const getRSCPayloadStreams = (
93-
railsContext: RailsContext,
102+
railsContext: RailsContextWithServerComponentCapabilities,
94103
): {
95104
componentName: string;
96105
props: unknown;
97106
stream: NodeJS.ReadableStream;
98-
}[] => mapRailsContextToRSCPayloadStreams.get(railsContext) ?? [];
107+
}[] => {
108+
const { renderRequestId } = railsContext.componentSpecificMetadata;
109+
return mapRailsContextToRSCPayloadStreams.get(renderRequestId) ?? [];
110+
};
99111

100-
export const clearRSCPayloadStreams = (railsContext: RailsContext) => {
101-
mapRailsContextToRSCPayloadStreams.delete(railsContext);
102-
rscPayloadCallbacks.delete(railsContext);
112+
export const clearRSCPayloadStreams = (railsContext: RailsContextWithServerComponentCapabilities) => {
113+
const { renderRequestId } = railsContext.componentSpecificMetadata;
114+
mapRailsContextToRSCPayloadStreams.delete(renderRequestId);
115+
rscPayloadCallbacks.delete(renderRequestId);
103116
};

node_package/src/ReactOnRailsRSC.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { PassThrough, Readable } from 'stream';
44

55
import {
66
RSCRenderParams,
7-
RailsContextWithComponentSpecificMetadata,
7+
assertRailsContextWithServerComponentCapabilities,
88
StreamRenderState,
99
StreamableComponentResult,
1010
} from './types/index.ts';
@@ -34,11 +34,10 @@ const streamRenderRSCComponent = (
3434
options: RSCRenderParams,
3535
): Readable => {
3636
const { throwJsErrors } = options;
37-
if (!options.railsContext?.serverSide || !options.railsContext.reactClientManifestFileName) {
38-
throw new Error('Rails context is not available');
39-
}
37+
const { railsContext } = options;
38+
assertRailsContextWithServerComponentCapabilities(railsContext);
4039

41-
const { reactClientManifestFileName } = options.railsContext;
40+
const { reactClientManifestFileName } = railsContext;
4241
const renderState: StreamRenderState = {
4342
result: null,
4443
hasErrors: false,
@@ -78,9 +77,7 @@ const streamRenderRSCComponent = (
7877
});
7978

8079
readableStream.on('end', () => {
81-
if (options.railsContext?.componentSpecificMetadata) {
82-
notifySSREnd(options.railsContext as RailsContextWithComponentSpecificMetadata);
83-
}
80+
notifySSREnd(railsContext);
8481
});
8582
return readableStream;
8683
};

node_package/src/getReactServerComponent.server.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BundleManifest } from 'react-on-rails-rsc';
22
import { buildClientRenderer } from 'react-on-rails-rsc/client.node';
33
import transformRSCStream from './transformRSCNodeStream.ts';
44
import loadJsonFile from './loadJsonFile.ts';
5-
import { RailsContext } from './types/index.ts';
5+
import { assertRailsContextWithServerComponentCapabilities, RailsContext } from './types/index.ts';
66

77
type RSCServerRootProps = {
88
componentName: string;
@@ -58,18 +58,7 @@ const getReactServerComponent = async ({
5858
componentProps,
5959
railsContext,
6060
}: RSCServerRootProps) => {
61-
if (
62-
!railsContext?.serverSide ||
63-
!railsContext?.reactClientManifestFileName ||
64-
!railsContext?.reactServerClientManifestFileName
65-
) {
66-
throw new Error(
67-
'serverClientManifestFileName and reactServerClientManifestFileName are required. ' +
68-
'Please ensure that React Server Component webpack configurations are properly set ' +
69-
'as stated in the React Server Component tutorial. ' +
70-
'Make sure to use "stream_react_component" instead of "react_component" to SSR a server component.',
71-
);
72-
}
61+
assertRailsContextWithServerComponentCapabilities(railsContext);
7362

7463
if (typeof ReactOnRails.getRSCPayloadStream !== 'function') {
7564
throw new Error(

node_package/src/injectRSCPayload.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PipeableStream } from 'react-dom/server';
22
import { PassThrough, Transform } from 'stream';
3-
import { RailsContext } from './types/index.ts';
3+
import { RailsContextWithServerComponentCapabilities } from './types/index.ts';
44

55
// In JavaScript, when an escape sequence with a backslash (\) is followed by a character
66
// that isn't a recognized escape character, the backslash is ignored, and the character
@@ -50,7 +50,7 @@ function writeChunk(chunk: string, transform: Transform, cacheKey: string) {
5050
*/
5151
export default function injectRSCPayload(
5252
pipeableHtmlStream: NodeJS.ReadableStream | PipeableStream,
53-
railsContext: RailsContext,
53+
railsContext: RailsContextWithServerComponentCapabilities,
5454
) {
5555
const htmlStream = new PassThrough();
5656
pipeableHtmlStream.pipe(htmlStream);

node_package/src/postSSRHooks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import { RailsContextWithComponentSpecificMetadata } from './types/index.ts';
1+
import { RailsContextWithServerComponentCapabilities } from './types/index.ts';
22

33
type PostSSRHook = () => void;
44
const postSSRHooks = new Map<string, PostSSRHook[]>();
55

66
export const addPostSSRHook = (
7-
railsContext: RailsContextWithComponentSpecificMetadata,
7+
railsContext: RailsContextWithServerComponentCapabilities,
88
hook: PostSSRHook,
99
) => {
1010
const hooks = postSSRHooks.get(railsContext.componentSpecificMetadata.renderRequestId) || [];
1111
hooks.push(hook);
1212
postSSRHooks.set(railsContext.componentSpecificMetadata.renderRequestId, hooks);
1313
};
1414

15-
export const notifySSREnd = (railsContext: RailsContextWithComponentSpecificMetadata) => {
15+
export const notifySSREnd = (railsContext: RailsContextWithServerComponentCapabilities) => {
1616
const hooks = postSSRHooks.get(railsContext.componentSpecificMetadata.renderRequestId);
1717
if (hooks) {
1818
hooks.forEach((hook) => hook());

node_package/src/streamServerRenderedReactComponent.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import buildConsoleReplay from './buildConsoleReplay.ts';
88
import handleError from './handleError.ts';
99
import { renderToPipeableStream, PipeableStream } from './ReactDOMServer.cts';
1010
import { createResultObject, convertToError, validateComponent } from './serverRenderUtils.ts';
11-
import type {
12-
RailsContextWithComponentSpecificMetadata,
11+
import {
12+
assertRailsContextWithServerComponentCapabilities,
1313
RenderParams,
1414
StreamRenderState,
1515
StreamableComponentResult,
@@ -138,7 +138,7 @@ const streamRenderReactComponent = (
138138
reactRenderingResult: StreamableComponentResult,
139139
options: RenderParams,
140140
) => {
141-
const { name: componentName, throwJsErrors, domNodeId } = options;
141+
const { name: componentName, throwJsErrors, domNodeId, railsContext } = options;
142142
const renderState: StreamRenderState = {
143143
result: null,
144144
hasErrors: false,
@@ -163,10 +163,7 @@ const streamRenderReactComponent = (
163163
endStream();
164164
};
165165

166-
const { railsContext } = options;
167-
if (!railsContext) {
168-
throw new Error('railsContext is required to stream a React component');
169-
}
166+
assertRailsContextWithServerComponentCapabilities(railsContext);
170167

171168
Promise.resolve(reactRenderingResult)
172169
.then((reactRenderedElement) => {
@@ -195,7 +192,7 @@ const streamRenderReactComponent = (
195192
},
196193
onAllReady() {
197194
if (railsContext.componentSpecificMetadata?.renderRequestId) {
198-
notifySSREnd(railsContext as RailsContextWithComponentSpecificMetadata);
195+
notifySSREnd(railsContext);
199196
}
200197
},
201198
identifierPrefix: domNodeId,

node_package/src/types/index.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,38 @@ export type RailsContext = {
6262
}
6363
);
6464

65-
export type RailsContextWithComponentSpecificMetadata = RailsContext & {
65+
export type RailsContextWithServerComponentCapabilities = RailsContext & {
66+
serverSide: true;
67+
serverSideRSCPayloadParameters: unknown;
68+
reactClientManifestFileName: string;
69+
reactServerClientManifestFileName: string;
6670
componentSpecificMetadata: {
6771
renderRequestId: string;
6872
};
6973
};
7074

75+
export const assertRailsContextWithServerComponentCapabilities: (
76+
context: RailsContext | undefined,
77+
) => asserts context is RailsContextWithServerComponentCapabilities = (
78+
context: RailsContext | undefined,
79+
): asserts context is RailsContextWithServerComponentCapabilities => {
80+
if (
81+
!context ||
82+
!('serverSideRSCPayloadParameters' in context) ||
83+
!('reactClientManifestFileName' in context) ||
84+
!('reactServerClientManifestFileName' in context) ||
85+
!('componentSpecificMetadata' in context)
86+
) {
87+
throw new Error(
88+
'Rails context does not have server side RSC payload parameters.\n\n' +
89+
'Please ensure:\n' +
90+
'1. You are using a compatible version of react_on_rails_pro\n' +
91+
'2. Server components support is enabled by setting:\n' +
92+
' ReactOnRailsPro.configuration.enable_rsc_support = true',
93+
);
94+
}
95+
};
96+
7197
// not strictly what we want, see https://github.com/microsoft/TypeScript/issues/17867#issuecomment-323164375
7298
type AuthenticityHeaders = Record<string, string> & {
7399
'X-CSRF-Token': string | null;
@@ -183,7 +209,8 @@ export interface RenderParams extends Params {
183209
renderingReturnsPromises: boolean;
184210
}
185211

186-
export interface RSCRenderParams extends RenderParams {
212+
export interface RSCRenderParams extends Omit<RenderParams, 'railsContext'> {
213+
railsContext: RailsContextWithServerComponentCapabilities;
187214
reactClientManifestFileName: string;
188215
}
189216

@@ -301,7 +328,7 @@ export interface ReactOnRails {
301328
* Adds a post SSR hook to be called after the SSR has completed.
302329
* @param hook - The hook to be called after the SSR has completed.
303330
*/
304-
addPostSSRHook(railsContext: RailsContextWithComponentSpecificMetadata, hook: () => void): void;
331+
addPostSSRHook(railsContext: RailsContextWithServerComponentCapabilities, hook: () => void): void;
305332
}
306333

307334
export type RSCPayloadStreamInfo = {
@@ -432,7 +459,7 @@ export interface ReactOnRailsInternal extends ReactOnRails {
432459
getRSCPayloadStream?: (
433460
componentName: string,
434461
props: unknown,
435-
railsContext: RailsContext,
462+
railsContext: RailsContextWithServerComponentCapabilities,
436463
) => Promise<NodeJS.ReadableStream>;
437464

438465
/**
@@ -441,7 +468,7 @@ export interface ReactOnRailsInternal extends ReactOnRails {
441468
* @param railsContext - The Rails context of the current rendering request.
442469
* @returns An array of objects, each containing the component name and its corresponding NodeJS.ReadableStream.
443470
*/
444-
getRSCPayloadStreams?: (railsContext: RailsContext) => {
471+
getRSCPayloadStreams?: (railsContext: RailsContextWithServerComponentCapabilities) => {
445472
componentName: string;
446473
props: unknown;
447474
stream: NodeJS.ReadableStream;
@@ -452,13 +479,16 @@ export interface ReactOnRailsInternal extends ReactOnRails {
452479
* @param railsContext - The Rails context of the current rendering request.
453480
* @param callback - The callback to be called when an RSC payload stream is generated.
454481
*/
455-
onRSCPayloadGenerated?: (railsContext: RailsContext, callback: RSCPayloadCallback) => void;
482+
onRSCPayloadGenerated?: (
483+
railsContext: RailsContextWithServerComponentCapabilities,
484+
callback: RSCPayloadCallback,
485+
) => void;
456486

457487
/**
458488
* Clears all RSC payload streams generated for the rendering request of the given Rails context.
459489
* @param railsContext - The Rails context of the current rendering request.
460490
*/
461-
clearRSCPayloadStreams?: (railsContext: RailsContext) => void;
491+
clearRSCPayloadStreams?: (railsContext: RailsContextWithServerComponentCapabilities) => void;
462492
}
463493

464494
export type RenderStateHtml = FinalHtmlResult | Promise<FinalHtmlResult>;

0 commit comments

Comments
 (0)