Skip to content

Commit 2b1517c

Browse files
refactor: enhance RSC component handling by introducing promise wrapper and improving stream processing for better error management
1 parent f961a3b commit 2b1517c

File tree

5 files changed

+56
-33
lines changed

5 files changed

+56
-33
lines changed

node_package/src/RSCProvider.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,7 @@ export const createRSCProvider = ({
4545
return cachedComponents[key];
4646
};
4747

48-
const getComponent = async (componentName: string, componentProps: unknown) => {
49-
const cachedComponent = getCachedComponent(componentName, componentProps);
50-
if (cachedComponent) {
51-
return cachedComponent;
52-
}
48+
const getComponent = (componentName: string, componentProps: unknown) => {
5349
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
5450
if (key in fetchRSCPromises) {
5551
return fetchRSCPromises[key];

node_package/src/RSCRoute.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ export type RSCRouteProps = {
3030

3131
const RSCRoute = ({ componentName, componentProps }: RSCRouteProps) => {
3232
const { getComponent, getCachedComponent } = useRSC();
33-
const component =
34-
getCachedComponent(componentName, componentProps) ??
35-
React.use(getComponent(componentName, componentProps));
36-
return component;
33+
const cachedComponent = getCachedComponent(componentName, componentProps);
34+
if (cachedComponent) {
35+
return cachedComponent;
36+
}
37+
38+
const componentPromise = getComponent(componentName, componentProps);
39+
return React.use(componentPromise);
3740
};
3841

3942
export default RSCRoute;

node_package/src/getReactServerComponent.client.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { createFromReadableStream } from 'react-on-rails-rsc/client.browser';
3-
import { createRSCPayloadKey, fetch } from './utils.ts';
3+
import { createRSCPayloadKey, fetch, wrapInNewPromise } from './utils.ts';
44
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs.ts';
55
import { assertRailsContextWithComponentSpecificMetadata, RailsContext } from './types/index.ts';
66

@@ -23,7 +23,8 @@ const createFromFetch = async (fetchPromise: Promise<Response>) => {
2323
throw new Error('No stream found in response');
2424
}
2525
const transformedStream = transformRSCStreamAndReplayConsoleLogs(stream);
26-
return createFromReadableStream<React.ReactNode>(transformedStream);
26+
const renderPromise = createFromReadableStream<React.ReactNode>(transformedStream);
27+
return wrapInNewPromise(renderPromise);
2728
};
2829

2930
/**
@@ -96,7 +97,8 @@ const createRSCStreamFromArray = (payloads: string[]) => {
9697
const createFromPreloadedPayloads = (payloads: string[]) => {
9798
const stream = createRSCStreamFromArray(payloads);
9899
const transformedStream = transformRSCStreamAndReplayConsoleLogs(stream);
99-
return createFromReadableStream<React.ReactNode>(transformedStream);
100+
const renderPromise = createFromReadableStream<React.ReactNode>(transformedStream);
101+
return wrapInNewPromise(renderPromise);
100102
};
101103

102104
/**

node_package/src/transformRSCStreamAndReplayConsoleLogs.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,31 +39,37 @@ export default function transformRSCStreamAndReplayConsoleLogs(stream: ReadableS
3939
}
4040
};
4141

42-
while (!done) {
43-
const decodedValue = typeof value === 'string' ? value : decoder.decode(value);
44-
const decodedChunks = lastIncompleteChunk + decodedValue;
45-
const chunks = decodedChunks.split('\n');
46-
lastIncompleteChunk = chunks.pop() ?? '';
42+
try {
43+
while (!done) {
44+
const decodedValue = typeof value === 'string' ? value : decoder.decode(value);
45+
const decodedChunks = lastIncompleteChunk + decodedValue;
46+
const chunks = decodedChunks.split('\n');
47+
lastIncompleteChunk = chunks.pop() ?? '';
4748

48-
const jsonChunks = chunks
49-
.filter((line) => line.trim() !== '')
50-
.map((line) => {
51-
try {
52-
return JSON.parse(line) as RSCPayloadChunk;
53-
} catch (error) {
54-
console.error('Error parsing JSON:', line, error);
55-
throw error;
56-
}
57-
});
49+
const jsonChunks = chunks
50+
.filter((line) => line.trim() !== '')
51+
.map((line) => {
52+
try {
53+
return JSON.parse(line) as RSCPayloadChunk;
54+
} catch (error) {
55+
console.error('Error parsing JSON:', line, error);
56+
throw error;
57+
}
58+
});
5859

59-
for (const jsonChunk of jsonChunks) {
60-
handleJsonChunk(jsonChunk);
61-
}
60+
for (const jsonChunk of jsonChunks) {
61+
handleJsonChunk(jsonChunk);
62+
}
6263

63-
// eslint-disable-next-line no-await-in-loop
64-
({ value, done } = await reader.read());
64+
// eslint-disable-next-line no-await-in-loop
65+
({ value, done } = await reader.read());
66+
}
67+
} catch (error) {
68+
console.error('Error transforming RSC stream:', error);
69+
controller.error(error);
70+
} finally {
71+
controller.close();
6572
}
66-
controller.close();
6773
},
6874
});
6975
}

node_package/src/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,19 @@ export const createRSCPayloadKey = (
1919
) => {
2020
return `${componentName}-${JSON.stringify(componentProps)}-${railsContext.componentSpecificMetadata.renderRequestId}`;
2121
};
22+
23+
/**
24+
* Wraps a promise from react-server-dom-webpack in a standard JavaScript Promise.
25+
*
26+
* This is necessary because promises returned by react-server-dom-webpack's methods
27+
* (like `createFromReadableStream` and `createFromNodeStream`) have non-standard behavior:
28+
* their `then()` method returns `null` instead of the promise itself, which breaks
29+
* promise chaining. This wrapper creates a new standard Promise that properly
30+
* forwards the resolution/rejection of the original promise.
31+
*/
32+
export const wrapInNewPromise = <T>(promise: Promise<T>) => {
33+
return new Promise<T>((resolve, reject) => {
34+
void promise.then(resolve);
35+
void promise.catch(reject);
36+
});
37+
};

0 commit comments

Comments
 (0)