Skip to content

Commit 6a82137

Browse files
Enhance error handling and memory management in RSC payload processing
1 parent 6b2f6a2 commit 6a82137

File tree

2 files changed

+59
-8
lines changed

2 files changed

+59
-8
lines changed

node_package/src/RSCPayloadGenerator.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,61 @@ declare global {
1414
}
1515

1616
const rscPayloadStreams = new Map<string, RSCPayloadStreamInfo[]>();
17-
1817
const rscPayloadCallbacks = new Map<string, RSCPayloadCallback[]>();
1918

19+
/**
20+
* TTL (Time To Live) tracking for RSC payload cleanup.
21+
* This Map stores timeout IDs for automatic cleanup of RSC payload data.
22+
* The TTL mechanism serves as a safety net to prevent memory leaks in case
23+
* the normal cleanup path (via clearRSCPayloadStreams) is not called.
24+
*/
25+
const rscPayloadTTLs = new Map<string, NodeJS.Timeout>();
26+
27+
/**
28+
* Default TTL duration of 5 minutes (300000 ms).
29+
* This duration should be long enough to accommodate normal request processing
30+
* while preventing long-term memory leaks if cleanup is missed.
31+
*/
32+
const DEFAULT_TTL = 300000;
33+
34+
export const clearRSCPayloadStreams = (railsContext: RailsContextWithServerComponentCapabilities) => {
35+
const { renderRequestId } = railsContext.componentSpecificMetadata;
36+
rscPayloadStreams.delete(renderRequestId);
37+
rscPayloadCallbacks.delete(renderRequestId);
38+
39+
// Clear TTL if it exists
40+
const ttl = rscPayloadTTLs.get(renderRequestId);
41+
if (ttl) {
42+
clearTimeout(ttl);
43+
rscPayloadTTLs.delete(renderRequestId);
44+
}
45+
};
46+
47+
/**
48+
* Schedules automatic cleanup of RSC payload data after a TTL period.
49+
* The TTL mechanism is necessary because:
50+
* - It prevents memory leaks if clearRSCPayloadStreams is not called (e.g., due to errors)
51+
* - It ensures cleanup happens even if the request is abandoned or times out
52+
* - It provides a safety net for edge cases where the normal cleanup path might be missed
53+
*
54+
* @param railsContext - The Rails context containing the renderRequestId to schedule cleanup for
55+
*/
56+
function scheduleCleanup(railsContext: RailsContextWithServerComponentCapabilities) {
57+
const { renderRequestId } = railsContext.componentSpecificMetadata;
58+
// Clear any existing TTL to prevent multiple cleanup timers
59+
const existingTTL = rscPayloadTTLs.get(renderRequestId);
60+
if (existingTTL) {
61+
clearTimeout(existingTTL);
62+
}
63+
64+
// Set new TTL that will trigger cleanup after DEFAULT_TTL milliseconds
65+
const ttl = setTimeout(() => {
66+
clearRSCPayloadStreams(railsContext);
67+
}, DEFAULT_TTL);
68+
69+
rscPayloadTTLs.set(renderRequestId, ttl);
70+
}
71+
2072
/**
2173
* Registers a callback to be executed when RSC payloads are generated.
2274
*
@@ -43,6 +95,9 @@ export const onRSCPayloadGenerated = (
4395
rscPayloadCallbacks.set(renderRequestId, [callback]);
4496
}
4597

98+
// This ensures we have a safety net even if the normal cleanup path fails
99+
scheduleCleanup(railsContext);
100+
46101
// Call callback for any existing streams for this context
47102
const existingStreams = rscPayloadStreams.get(renderRequestId);
48103
if (existingStreams) {
@@ -122,9 +177,3 @@ export const getRSCPayloadStreams = (
122177
const { renderRequestId } = railsContext.componentSpecificMetadata;
123178
return rscPayloadStreams.get(renderRequestId) ?? [];
124179
};
125-
126-
export const clearRSCPayloadStreams = (railsContext: RailsContextWithServerComponentCapabilities) => {
127-
const { renderRequestId } = railsContext.componentSpecificMetadata;
128-
rscPayloadStreams.delete(renderRequestId);
129-
rscPayloadCallbacks.delete(renderRequestId);
130-
};

node_package/src/injectRSCPayload.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,11 @@ export default function injectRSCPayload(
123123
rscPromise = startRSC();
124124
}
125125
rscPromise
126+
.finally(() => {
127+
ReactOnRails.clearRSCPayloadStreams?.(railsContext);
128+
})
126129
.then(() => {
127130
resultStream.end();
128-
ReactOnRails.clearRSCPayloadStreams?.(railsContext);
129131
})
130132
.catch((err: unknown) => resultStream.emit('error', err));
131133
});

0 commit comments

Comments
 (0)