@@ -14,9 +14,61 @@ declare global {
1414}
1515
1616const rscPayloadStreams = new Map < string , RSCPayloadStreamInfo [ ] > ( ) ;
17-
1817const 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- } ;
0 commit comments