From 424dd6c188a61a5085c8696da665a6010ca76944 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 9 Dec 2025 14:14:59 +0200 Subject: [PATCH 01/26] use v19.0.3 of react-on-rails-rsc as git dep --- packages/react-on-rails-pro/package.json | 2 +- pnpm-lock.yaml | 15 ++++++++------- react_on_rails_pro/spec/dummy/package.json | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/react-on-rails-pro/package.json b/packages/react-on-rails-pro/package.json index 37d266e9a2..29c6dc0b72 100644 --- a/packages/react-on-rails-pro/package.json +++ b/packages/react-on-rails-pro/package.json @@ -80,6 +80,6 @@ "mock-fs": "^5.5.0", "react": "^19.0.1", "react-dom": "^19.0.1", - "react-on-rails-rsc": "^19.0.3" + "react-on-rails-rsc": "git+https://github.com/shakacode/react_on_rails_rsc#upgrade-to-react-v19.2.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7db392c03..be2822c11e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,8 +209,8 @@ importers: specifier: ^19.0.1 version: 19.2.0(react@19.2.0) react-on-rails-rsc: - specifier: ^19.0.3 - version: 19.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) + specifier: git+https://github.com/shakacode/react_on_rails_rsc#upgrade-to-react-v19.2.1 + version: https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) packages/react-on-rails-pro-node-renderer: dependencies: @@ -4421,11 +4421,12 @@ packages: react-dom: ^19.0.0 webpack: ^5.59.0 - react-on-rails-rsc@19.0.3: - resolution: {integrity: sha512-g+89U83+WjZDbtLvYQbjld0pWdUXpKageSoeKsX8cj1SkmULMAzbxgvH6vdzOuQUSwchkbDgwFO9umlHDhiyug==} + react-on-rails-rsc@https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261: + resolution: {tarball: https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261} + version: 19.0.3 peerDependencies: - react: ^19.0.1 - react-dom: ^19.0.1 + react: ^19.2.1 + react-dom: ^19.2.1 webpack: ^5.59.0 react@19.2.0: @@ -10321,7 +10322,7 @@ snapshots: webpack: 5.103.0(@swc/core@1.15.3) webpack-sources: 3.3.3 - react-on-rails-rsc@19.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)): + react-on-rails-rsc@https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 diff --git a/react_on_rails_pro/spec/dummy/package.json b/react_on_rails_pro/spec/dummy/package.json index 629f9bd370..1e5f2cd32e 100644 --- a/react_on_rails_pro/spec/dummy/package.json +++ b/react_on_rails_pro/spec/dummy/package.json @@ -52,7 +52,7 @@ "@dr.pogodin/react-helmet": "^3.0.4", "react-on-rails-pro": "link:.yalc/react-on-rails-pro", "react-on-rails-pro-node-renderer": "link:.yalc/react-on-rails-pro-node-renderer", - "react-on-rails-rsc": "^19.0.3", + "react-on-rails-rsc": "git+https://github.com/shakacode/react_on_rails_rsc#upgrade-to-react-v19.2.1", "react-proptypes": "^1.0.0", "react-redux": "^9.2.0", "react-refresh": "^0.11.0", From 641893ed7bfa3e1e33acb5bac0351b4f60cbedfe Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Wed, 10 Dec 2025 18:24:05 +0200 Subject: [PATCH 02/26] upgrade to react 19.2.1 --- packages/react-on-rails-pro/package.json | 4 +- .../react-on-rails-pro/tests/AsyncQueue.ts | 5 +- ...oncurrentRSCPayloadGeneration.rsc.test.tsx | 18 ++- .../tests/utils/removeRSCChunkStack.ts | 17 ++- pnpm-lock.yaml | 117 +++++++++++++----- .../spec/dummy/client/node-renderer.js | 2 +- react_on_rails_pro/spec/dummy/package.json | 4 +- react_on_rails_pro/spec/dummy/pnpm-lock.yaml | 19 +-- 8 files changed, 136 insertions(+), 50 deletions(-) diff --git a/packages/react-on-rails-pro/package.json b/packages/react-on-rails-pro/package.json index 29c6dc0b72..231f7052f6 100644 --- a/packages/react-on-rails-pro/package.json +++ b/packages/react-on-rails-pro/package.json @@ -78,8 +78,8 @@ "devDependencies": { "@types/mock-fs": "^4.13.4", "mock-fs": "^5.5.0", - "react": "^19.0.1", - "react-dom": "^19.0.1", + "react": "19.2.1", + "react-dom": "19.2.1", "react-on-rails-rsc": "git+https://github.com/shakacode/react_on_rails_rsc#upgrade-to-react-v19.2.1" } } diff --git a/packages/react-on-rails-pro/tests/AsyncQueue.ts b/packages/react-on-rails-pro/tests/AsyncQueue.ts index d6f8b7b204..0cf6c843b8 100644 --- a/packages/react-on-rails-pro/tests/AsyncQueue.ts +++ b/packages/react-on-rails-pro/tests/AsyncQueue.ts @@ -26,9 +26,10 @@ class AsyncQueue { dequeue() { return new Promise((resolve, reject) => { - const bufferValueIfExist = this.buffer.shift(); + const bufferValueIfExist = this.buffer.length > 0 ? this.buffer.join('') : undefined; + this.buffer.length = 0; if (bufferValueIfExist) { - resolve(bufferValueIfExist); + resolve(bufferValueIfExist as T); } else if (this.isEnded) { reject(new Error('Queue Ended')); } else { diff --git a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx index 353b19d6f8..502c6193ef 100644 --- a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx @@ -100,13 +100,20 @@ const createParallelRenders = (size: number) => { return { enqueue, expectNextChunk, expectEndOfStream }; }; +const delay = (ms: number) => new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); +}); + test('Renders concurrent rsc streams as single rsc stream', async () => { - expect.assertions(258); + // expect.assertions(258); const asyncQueue = new AsyncQueue(); const stream = renderComponent({ asyncQueue }); const reader = new StreamReader(stream); const chunks: string[] = []; + await delay(100); let chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Async Queue'); @@ -114,16 +121,20 @@ test('Renders concurrent rsc streams as single rsc stream', async () => { expect(chunk).not.toContain('Random Value'); asyncQueue.enqueue('Random Value1'); + + await delay(100); chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Random Value1'); asyncQueue.enqueue('Random Value2'); + await delay(100); chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Random Value2'); asyncQueue.enqueue('Random Value3'); + await delay(100); chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Random Value3'); @@ -133,12 +144,17 @@ test('Renders concurrent rsc streams as single rsc stream', async () => { const { enqueue, expectNextChunk, expectEndOfStream } = createParallelRenders(50); expect(chunks).toHaveLength(4); + await delay(100); await expectNextChunk(chunks[0]); enqueue('Random Value1'); + await delay(100); await expectNextChunk(chunks[1]); enqueue('Random Value2'); + await delay(100); await expectNextChunk(chunks[2]); enqueue('Random Value3'); + await delay(100); await expectNextChunk(chunks[3]); + await delay(100); await expectEndOfStream(); }); diff --git a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts index 4774feadfa..c5d2d0df4d 100644 --- a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts +++ b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts @@ -1,7 +1,16 @@ import { RSCPayloadChunk } from 'react-on-rails'; -const removeRSCChunkStack = (chunk: string) => { - const parsedJson = JSON.parse(chunk) as RSCPayloadChunk; +const removeRSCChunkStackInternal = (chunk: string) => { + if (chunk.trim().length === 0) { + return chunk; + } + + let parsedJson: RSCPayloadChunk; + try { + parsedJson = JSON.parse(chunk) as RSCPayloadChunk; + } catch (err) { + throw new Error(`Error while parsing the json: "${chunk}", ${err}`); + } const { html } = parsedJson; const santizedHtml = html.split('\n').map((chunkLine) => { if (!chunkLine.includes('"stack":')) { @@ -25,4 +34,8 @@ const removeRSCChunkStack = (chunk: string) => { }); }; +const removeRSCChunkStack = (chunk: string) => { + chunk.split('\n').map(removeRSCChunkStackInternal).join('\n'); +} + export default removeRSCChunkStack; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be2822c11e..ffe278899a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,7 +98,7 @@ importers: version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jest: specifier: ^28.11.0 - version: 28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + version: 28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.39.1(jiti@2.6.1)) @@ -119,7 +119,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.25) + version: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -173,7 +173,7 @@ importers: version: 0.2.6(@swc/core@1.15.3)(webpack@5.103.0(@swc/core@1.15.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3) typescript: specifier: ^5.8.3 version: 5.9.3 @@ -203,14 +203,14 @@ importers: specifier: ^5.5.0 version: 5.5.0 react: - specifier: ^19.0.1 - version: 19.2.0 + specifier: 19.2.1 + version: 19.2.1 react-dom: - specifier: ^19.0.1 - version: 19.2.0(react@19.2.0) + specifier: 19.2.1 + version: 19.2.1(react@19.2.1) react-on-rails-rsc: specifier: git+https://github.com/shakacode/react_on_rails_rsc#upgrade-to-react-v19.2.1 - version: https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) + version: https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(webpack@5.103.0(@swc/core@1.15.3)) packages/react-on-rails-pro-node-renderer: dependencies: @@ -1650,6 +1650,9 @@ packages: '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -2131,6 +2134,10 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: @@ -2396,6 +2403,10 @@ packages: core-js-compat@3.47.0: resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + cosmiconfig@9.0.0: resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} @@ -4405,6 +4416,11 @@ packages: peerDependencies: react: ^19.2.0 + react-dom@19.2.1: + resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} + peerDependencies: + react: ^19.2.1 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4433,6 +4449,10 @@ packages: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} + react@19.2.1: + resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} + engines: {node: '>=0.10.0'} + readline-sync@1.4.10: resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} engines: {node: '>= 0.8.0'} @@ -5340,6 +5360,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + yargs-parser@15.0.3: resolution: {integrity: sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==} @@ -6392,7 +6416,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -6406,7 +6430,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.25) + jest-config: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -7007,6 +7031,9 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/parse-json@4.0.2': + optional: true + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -7530,6 +7557,13 @@ snapshots: '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.4 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + optional: true + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5): dependencies: '@babel/compat-data': 7.28.5 @@ -7804,6 +7838,15 @@ snapshots: dependencies: browserslist: 4.28.0 + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + optional: true + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -7813,13 +7856,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 - create-jest@29.7.0(@types/node@20.19.25): + create-jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.19.25) + jest-config: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -7936,7 +7979,9 @@ snapshots: decimal.js@10.6.0: {} - dedent@1.7.0: {} + dedent@1.7.0(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 deep-is@0.1.4: {} @@ -8283,13 +8328,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3): + eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) optionalDependencies: '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - jest: 29.7.0(@types/node@20.19.25) + jest: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) transitivePeerDependencies: - supports-color - typescript @@ -9134,7 +9179,7 @@ snapshots: jest-util: 29.7.0 p-limit: 3.1.0 - jest-circus@29.7.0: + jest-circus@29.7.0(babel-plugin-macros@3.1.0): dependencies: '@jest/environment': 29.7.0 '@jest/expect': 29.7.0 @@ -9143,7 +9188,7 @@ snapshots: '@types/node': 20.19.25 chalk: 4.1.2 co: 4.6.0 - dedent: 1.7.0 + dedent: 1.7.0(babel-plugin-macros@3.1.0) is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -9160,16 +9205,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.19.25): + jest-cli@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.25) + create-jest: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.25) + jest-config: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -9179,7 +9224,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.19.25): + jest-config@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -9190,7 +9235,7 @@ snapshots: deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.7.0 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) jest-environment-node: 29.7.0 jest-get-type: 29.6.3 jest-regex-util: 29.6.3 @@ -9459,12 +9504,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.19.25): + jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.19.25) + jest-cli: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10307,6 +10352,11 @@ snapshots: react: 19.2.0 scheduler: 0.27.0 + react-dom@19.2.1(react@19.2.1): + dependencies: + react: 19.2.1 + scheduler: 0.27.0 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -10322,17 +10372,19 @@ snapshots: webpack: 5.103.0(@swc/core@1.15.3) webpack-sources: 3.3.3 - react-on-rails-rsc@https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)): + react-on-rails-rsc@https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(webpack@5.103.0(@swc/core@1.15.3)): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) webpack: 5.103.0(@swc/core@1.15.3) webpack-sources: 3.3.3 react@19.2.0: {} + react@19.2.1: {} + readline-sync@1.4.10: {} real-require@0.2.0: {} @@ -10991,12 +11043,12 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@20.19.25) + jest: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -11361,6 +11413,9 @@ snapshots: yallist@3.1.1: {} + yaml@1.10.2: + optional: true + yargs-parser@15.0.3: dependencies: camelcase: 5.3.1 diff --git a/react_on_rails_pro/spec/dummy/client/node-renderer.js b/react_on_rails_pro/spec/dummy/client/node-renderer.js index 67f390b45a..15d1ac7c3b 100644 --- a/react_on_rails_pro/spec/dummy/client/node-renderer.js +++ b/react_on_rails_pro/spec/dummy/client/node-renderer.js @@ -55,7 +55,7 @@ const config = { // additionalContext enables you to specify additional NodeJS modules to add to the VM context in // addition to our supportModules defaults. - additionalContext: { URL, AbortController }, + additionalContext: { URL, AbortController, performance }, // Required to use setTimeout, setInterval, & clearTimeout during server rendering stubTimers: false, diff --git a/react_on_rails_pro/spec/dummy/package.json b/react_on_rails_pro/spec/dummy/package.json index 1e5f2cd32e..a2a6bed227 100644 --- a/react_on_rails_pro/spec/dummy/package.json +++ b/react_on_rails_pro/spec/dummy/package.json @@ -46,8 +46,8 @@ "postcss": "^8.4.31", "postcss-loader": "^7.1.0", "prop-types": "^15.7.2", - "react": "^19.0.1", - "react-dom": "^19.0.1", + "react": "19.2.1", + "react-dom": "19.2.1", "react-error-boundary": "^4.1.2", "@dr.pogodin/react-helmet": "^3.0.4", "react-on-rails-pro": "link:.yalc/react-on-rails-pro", diff --git a/react_on_rails_pro/spec/dummy/pnpm-lock.yaml b/react_on_rails_pro/spec/dummy/pnpm-lock.yaml index 8c017555f4..0b4d5b9b93 100644 --- a/react_on_rails_pro/spec/dummy/pnpm-lock.yaml +++ b/react_on_rails_pro/spec/dummy/pnpm-lock.yaml @@ -138,10 +138,10 @@ importers: specifier: ^15.7.2 version: 15.8.1 react: - specifier: ^19.0.1 + specifier: 19.2.1 version: 19.2.1 react-dom: - specifier: ^19.0.1 + specifier: 19.2.1 version: 19.2.1(react@19.2.1) react-error-boundary: specifier: ^4.1.2 @@ -153,8 +153,8 @@ importers: specifier: link:.yalc/react-on-rails-pro-node-renderer version: link:.yalc/react-on-rails-pro-node-renderer react-on-rails-rsc: - specifier: ^19.0.3 - version: 19.0.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(webpack@5.103.0) + specifier: git+https://github.com/shakacode/react_on_rails_rsc#upgrade-to-react-v19.2.1 + version: https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(webpack@5.103.0) react-proptypes: specifier: ^1.0.0 version: 1.0.0 @@ -3466,11 +3466,12 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-on-rails-rsc@19.0.3: - resolution: {integrity: sha512-g+89U83+WjZDbtLvYQbjld0pWdUXpKageSoeKsX8cj1SkmULMAzbxgvH6vdzOuQUSwchkbDgwFO9umlHDhiyug==} + react-on-rails-rsc@https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261: + resolution: {tarball: https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261} + version: 19.0.3 peerDependencies: - react: ^19.0.1 - react-dom: ^19.0.1 + react: ^19.2.1 + react-dom: ^19.2.1 webpack: ^5.59.0 react-proptypes@1.0.0: @@ -7942,7 +7943,7 @@ snapshots: react-is@16.13.1: {} - react-on-rails-rsc@19.0.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(webpack@5.103.0): + react-on-rails-rsc@https://codeload.github.com/shakacode/react_on_rails_rsc/tar.gz/f00f4f96578b0de1224284f011e20817c4186261(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(webpack@5.103.0): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 From 56fe3584b1d974709f147745571c29cddaacaaaf Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 10:30:09 +0200 Subject: [PATCH 03/26] make vm adds the global `performance` object to the vm context --- packages/react-on-rails-pro-node-renderer/src/worker/vm.ts | 1 + react_on_rails_pro/spec/dummy/client/node-renderer.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts b/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts index 3567f9ea7c..aa3f997dcf 100644 --- a/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts +++ b/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts @@ -217,6 +217,7 @@ export async function buildVM(filePath: string) { TextEncoder, URLSearchParams, ReadableStream, + performance, process, setTimeout, setInterval, diff --git a/react_on_rails_pro/spec/dummy/client/node-renderer.js b/react_on_rails_pro/spec/dummy/client/node-renderer.js index 15d1ac7c3b..67f390b45a 100644 --- a/react_on_rails_pro/spec/dummy/client/node-renderer.js +++ b/react_on_rails_pro/spec/dummy/client/node-renderer.js @@ -55,7 +55,7 @@ const config = { // additionalContext enables you to specify additional NodeJS modules to add to the VM context in // addition to our supportModules defaults. - additionalContext: { URL, AbortController, performance }, + additionalContext: { URL, AbortController }, // Required to use setTimeout, setInterval, & clearTimeout during server rendering stubTimers: false, From df3407b138549d5c2c39ef401510f1443b34cf50 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 10:48:01 +0200 Subject: [PATCH 04/26] linting --- .../tests/concurrentRSCPayloadGeneration.rsc.test.tsx | 11 ++++++----- .../tests/utils/removeRSCChunkStack.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx index 502c6193ef..9f6a8b254d 100644 --- a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx @@ -100,11 +100,12 @@ const createParallelRenders = (size: number) => { return { enqueue, expectNextChunk, expectEndOfStream }; }; -const delay = (ms: number) => new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, ms); -}); +const delay = (ms: number) => + new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); test('Renders concurrent rsc streams as single rsc stream', async () => { // expect.assertions(258); diff --git a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts index c5d2d0df4d..57795540a6 100644 --- a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts +++ b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts @@ -36,6 +36,6 @@ const removeRSCChunkStackInternal = (chunk: string) => { const removeRSCChunkStack = (chunk: string) => { chunk.split('\n').map(removeRSCChunkStackInternal).join('\n'); -} +}; export default removeRSCChunkStack; From 48a4510d5dd3a8c19b94f9cca76f1b311a2e7649 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 10:49:07 +0200 Subject: [PATCH 05/26] add AbortController to the vm context --- packages/react-on-rails-pro-node-renderer/src/worker/vm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts b/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts index aa3f997dcf..daba69dc69 100644 --- a/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts +++ b/packages/react-on-rails-pro-node-renderer/src/worker/vm.ts @@ -212,6 +212,7 @@ export async function buildVM(filePath: string) { // 1. docs/node-renderer/js-configuration.md // 2. packages/node-renderer/src/shared/configBuilder.ts extendContext(contextObject, { + AbortController, Buffer, TextDecoder, TextEncoder, From 2a18b8e4e6f2ef93efb6c3557ff4a04875f12a4c Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 12:00:45 +0200 Subject: [PATCH 06/26] revert this: make htmlStreaming.test.js fail to debug the error --- .../tests/htmlStreaming.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js index 45b98e683d..b8ebfe52d4 100644 --- a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js +++ b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js @@ -197,6 +197,8 @@ describe('html streaming', () => { expect(fullBody).toContain('branch2 (level 1)'); expect(fullBody).toContain('branch2 (level 0)'); + // Fail to findout the chunks content on CI + expect(JSON.stringify(jsonChunks, undefined, 2)).toContain('nnnnnnnnnnnnnnnnnn'); expect(jsonChunks[0].isShellReady).toBeTruthy(); expect(jsonChunks[0].hasErrors).toBeTruthy(); expect(jsonChunks[0].renderingError).toMatchObject({ From 4dfb0dc4338e928e6869ebcf7548ab371e9db5ef Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 12:15:18 +0200 Subject: [PATCH 07/26] revert this: log chunk --- .../tests/htmlStreaming.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js index b8ebfe52d4..042b437889 100644 --- a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js +++ b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js @@ -54,7 +54,7 @@ const makeRequest = async (options = {}) => { try { return JSON.parse(chunk); } catch (e) { - return { hasErrors: true, error: `JSON parsing failed: ${e.message}` }; + return { hasErrors: true, error: `JSON parsing failed: ${e.message}`, chunk }; } }), ); From ee9464fe6517cf5503860c2f807805a4466bd52a Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 12:51:34 +0200 Subject: [PATCH 08/26] log chunks size --- .../tests/htmlStreaming.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js index 042b437889..684883849e 100644 --- a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js +++ b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js @@ -54,7 +54,7 @@ const makeRequest = async (options = {}) => { try { return JSON.parse(chunk); } catch (e) { - return { hasErrors: true, error: `JSON parsing failed: ${e.message}`, chunk }; + return { hasErrors: true, error: `JSON parsing failed: ${e.message}`, chunk, chunksSize: decodedChunksFromData.length }; } }), ); From 5f64386f0a164aecaedcb60a77c36d66ac945368 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 13:03:22 +0200 Subject: [PATCH 09/26] use buffer for received incomplete chunks --- .../tests/htmlStreaming.test.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js index 684883849e..e98c24012b 100644 --- a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js +++ b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js @@ -34,6 +34,7 @@ const makeRequest = async (options = {}) => { const jsonChunks = []; let firstByteTime; let status; + let buffer = ''; const decoder = new TextDecoder(); request.on('response', (headers) => { @@ -44,10 +45,17 @@ const makeRequest = async (options = {}) => { // Sometimes, multiple chunks are merged into one. // So, the server uses \n as a delimiter between chunks. const decodedData = typeof data === 'string' ? data : decoder.decode(data, { stream: false }); - const decodedChunksFromData = decodedData + const decodedChunksFromData = (buffer + decodedData) .split('\n') .map((chunk) => chunk.trim()) .filter((chunk) => chunk.length > 0); + + if (!decodedData.endsWith('\n')) { + buffer = decodedChunksFromData.pop() ?? ''; + } else { + buffer = ''; + } + chunks.push(...decodedChunksFromData); jsonChunks.push( ...decodedChunksFromData.map((chunk) => { From 7764069baf75fe272c8cb9d228cb5a2430671b9b Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 13:17:02 +0200 Subject: [PATCH 10/26] revert logging --- .../tests/htmlStreaming.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js index e98c24012b..2e05a736af 100644 --- a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js +++ b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js @@ -62,7 +62,7 @@ const makeRequest = async (options = {}) => { try { return JSON.parse(chunk); } catch (e) { - return { hasErrors: true, error: `JSON parsing failed: ${e.message}`, chunk, chunksSize: decodedChunksFromData.length }; + return { hasErrors: true, error: `JSON parsing failed: ${e.message}` }; } }), ); @@ -206,7 +206,6 @@ describe('html streaming', () => { expect(fullBody).toContain('branch2 (level 0)'); // Fail to findout the chunks content on CI - expect(JSON.stringify(jsonChunks, undefined, 2)).toContain('nnnnnnnnnnnnnnnnnn'); expect(jsonChunks[0].isShellReady).toBeTruthy(); expect(jsonChunks[0].hasErrors).toBeTruthy(); expect(jsonChunks[0].renderingError).toMatchObject({ From 8759ecd7df2cfb2ee3a05320718a6424befda7b9 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 14:32:57 +0200 Subject: [PATCH 11/26] linting --- .../tests/htmlStreaming.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js index 2e05a736af..6938344cab 100644 --- a/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js +++ b/packages/react-on-rails-pro-node-renderer/tests/htmlStreaming.test.js @@ -49,7 +49,7 @@ const makeRequest = async (options = {}) => { .split('\n') .map((chunk) => chunk.trim()) .filter((chunk) => chunk.length > 0); - + if (!decodedData.endsWith('\n')) { buffer = decodedChunksFromData.pop() ?? ''; } else { From 98c48f3e19e324a2c607bc0faa4f94df0c9302bd Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 15:19:09 +0200 Subject: [PATCH 12/26] increase the time of the test that reproduce the react condole replay bug --- .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 4053f8280e..59748e8038 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -28,7 +28,7 @@ const PromiseContainer = ({ name }: { name: string }) => { clearInterval(intervalId); resolve(`Value of name ${name}`); } - }, 1); + }, 20); }); return ( @@ -157,4 +157,4 @@ test('[bug] catches logs outside the component during reading the stream', async expect(content1).not.toContain('From Interval'); // Here's the bug expect(content1).toContain('Outside The Component'); -}); +}, 10000); From 062cf5123221f0dfc215f52272f6b233530c043c Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 15:26:55 +0200 Subject: [PATCH 13/26] increase delay at test --- .../serverRenderRSCReactComponent.rsc.test.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 59748e8038..3c148128a9 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -21,14 +21,16 @@ const PromiseWrapper = async ({ promise, name }: { promise: Promise; nam const PromiseContainer = ({ name }: { name: string }) => { const promise = new Promise((resolve) => { let i = 0; - const intervalId = setInterval(() => { - console.log(`Interval ${i} at [${name}]`); - i += 1; - if (i === 50) { - clearInterval(intervalId); - resolve(`Value of name ${name}`); - } - }, 20); + setTimeout(() => { + const intervalId = setInterval(() => { + console.log(`Interval ${i} at [${name}]`); + i += 1; + if (i === 50) { + clearInterval(intervalId); + resolve(`Value of name ${name}`); + } + }, 20); + }, 200); }); return ( From 9ffaac2a34ec7be67431296e3e865ffb4a3e2a9c Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 15:34:09 +0200 Subject: [PATCH 14/26] add a check to ensure that the buggy component is rendered as expected --- .../serverRenderRSCReactComponent.rsc.test.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 3c148128a9..3b78e165c2 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -11,14 +11,17 @@ import { finished } from 'stream/promises'; import { text } from 'stream/consumers'; import ReactOnRails, { RailsContextWithServerStreamingCapabilities } from '../src/ReactOnRailsRSC.ts'; -const PromiseWrapper = async ({ promise, name }: { promise: Promise; name: string }) => { +const PromiseWrapper = async ({ promise, name, onResolved }: { promise: Promise; name: string, onResolved?: () => {} }) => { console.log(`[${name}] Before awaitng`); const value = await promise; + if (onResolved) { + onResolved(); + } console.log(`[${name}] After awaitng`); return

Value: {value}

; }; -const PromiseContainer = ({ name }: { name: string }) => { +const PromiseContainer = ({ name, onResolved }: { name: string, onResolved?: () => {} }) => { const promise = new Promise((resolve) => { let i = 0; setTimeout(() => { @@ -125,6 +128,7 @@ test('no logs lekage from outside the component', async () => { }); test('[bug] catches logs outside the component during reading the stream', async () => { + let resolved = false; const readable1 = ReactOnRails.serverRenderRSCReactComponent({ railsContext: { reactClientManifestFileName: 'react-client-manifest.json', @@ -134,13 +138,16 @@ test('[bug] catches logs outside the component during reading the stream', async renderingReturnsPromises: true, throwJsErrors: true, domNodeId: 'dom-id', - props: { name: 'First Unique Name' }, + props: { name: 'First Unique Name', onResolved: () => { resolved = true; } }, }); let content1 = ''; let i = 0; readable1.on('data', (chunk: Buffer) => { i += 1; + if (i === 1) { + expect(resolved).toBe(false); + } // To avoid infinite loop if (i < 5) { console.log('Outside The Component'); From 6faf9122eb9b3d3eec78344870341b1e178babd3 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 11 Dec 2025 15:41:50 +0200 Subject: [PATCH 15/26] add a check to ensure that the buggy component is rendered as expected --- .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 3b78e165c2..389784ddf2 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -40,7 +40,7 @@ const PromiseContainer = ({ name, onResolved }: { name: string, onResolved?: ()

Initial Header

Loading Promise

}> - +
); @@ -161,6 +161,7 @@ test('[bug] catches logs outside the component during reading the stream', async }, 2); await finished(readable1); clearInterval(intervalId); + expect(resolved).toBe(true); expect(content1).toContain('First Unique Name'); expect(content1).not.toContain('From Interval'); From 78f17702b0e85a8b97fae519793fb3350139dcd4 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 10:49:32 +0200 Subject: [PATCH 16/26] revert this: run only the failing test on CI --- .github/workflows/package-js-tests.yml | 2 +- packages/react-on-rails-pro/package.json | 1 + .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 9 ++++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/package-js-tests.yml b/.github/workflows/package-js-tests.yml index ec5406123b..79fc3c0466 100644 --- a/.github/workflows/package-js-tests.yml +++ b/.github/workflows/package-js-tests.yml @@ -129,4 +129,4 @@ jobs: - name: Run JS unit tests for react-on-rails package run: pnpm --filter react-on-rails test - name: Run JS unit tests for react-on-rails-pro package - run: pnpm --filter react-on-rails-pro test + run: ENABLE_JEST_CONSOLE=y NODE_CONDITIONS=react-server pnpm --filter react-on-rails-pro jest tests/serverRenderRSCReactComponent.rsc.test.tsx diff --git a/packages/react-on-rails-pro/package.json b/packages/react-on-rails-pro/package.json index 231f7052f6..c3fa6614f7 100644 --- a/packages/react-on-rails-pro/package.json +++ b/packages/react-on-rails-pro/package.json @@ -8,6 +8,7 @@ "build-watch": "pnpm run clean && tsc --watch", "clean": "rm -rf ./lib", "test": "pnpm run test:non-rsc && pnpm run test:rsc", + "jest": "jest", "test:non-rsc": "jest tests --testPathIgnorePatterns=\".*(RSC|stream|registerServerComponent|serverRenderReactComponent|SuspenseHydration).*\"", "test:rsc": "node scripts/check-react-version.cjs || NODE_CONDITIONS=react-server jest tests/*.rsc.test.*", "type-check": "tsc --noEmit --noErrorTruncation", diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 389784ddf2..095a171da5 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -21,13 +21,14 @@ const PromiseWrapper = async ({ promise, name, onResolved }: { promise: Promise< return

Value: {value}

; }; -const PromiseContainer = ({ name, onResolved }: { name: string, onResolved?: () => {} }) => { +const PromiseContainer = ({ name, onResolved, tick }: { name: string, onResolved?: () => {}, tick?: () => {} }) => { const promise = new Promise((resolve) => { let i = 0; setTimeout(() => { const intervalId = setInterval(() => { console.log(`Interval ${i} at [${name}]`); i += 1; + tick?.(); if (i === 50) { clearInterval(intervalId); resolve(`Value of name ${name}`); @@ -127,8 +128,9 @@ test('no logs lekage from outside the component', async () => { expect(content1).not.toContain('Outside The Component'); }); -test('[bug] catches logs outside the component during reading the stream', async () => { +test.only('[bug] catches logs outside the component during reading the stream', async () => { let resolved = false; + let executedIntervals = 0; const readable1 = ReactOnRails.serverRenderRSCReactComponent({ railsContext: { reactClientManifestFileName: 'react-client-manifest.json', @@ -138,7 +140,7 @@ test('[bug] catches logs outside the component during reading the stream', async renderingReturnsPromises: true, throwJsErrors: true, domNodeId: 'dom-id', - props: { name: 'First Unique Name', onResolved: () => { resolved = true; } }, + props: { name: 'First Unique Name', onResolved: () => { resolved = true; }, tick: () => { executedIntervals += 1 } }, }); let content1 = ''; @@ -151,6 +153,7 @@ test('[bug] catches logs outside the component during reading the stream', async // To avoid infinite loop if (i < 5) { console.log('Outside The Component'); + console.log(`Interval Count: ${executedIntervals}`); } content1 += chunk.toString(); }); From 0bd359b971a81b9d64dc98eee8ddd46535d26398 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 10:56:18 +0200 Subject: [PATCH 17/26] revert this: rerun tests with the pnpm test script --- .github/workflows/package-js-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package-js-tests.yml b/.github/workflows/package-js-tests.yml index 79fc3c0466..ec5406123b 100644 --- a/.github/workflows/package-js-tests.yml +++ b/.github/workflows/package-js-tests.yml @@ -129,4 +129,4 @@ jobs: - name: Run JS unit tests for react-on-rails package run: pnpm --filter react-on-rails test - name: Run JS unit tests for react-on-rails-pro package - run: ENABLE_JEST_CONSOLE=y NODE_CONDITIONS=react-server pnpm --filter react-on-rails-pro jest tests/serverRenderRSCReactComponent.rsc.test.tsx + run: pnpm --filter react-on-rails-pro test From 1b8f55f5ec50e482581b8a731d13480e568079f4 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 11:01:11 +0200 Subject: [PATCH 18/26] run all tests at serverRenderRSCReactComponent --- .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 095a171da5..8e76569d2d 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -128,7 +128,7 @@ test('no logs lekage from outside the component', async () => { expect(content1).not.toContain('Outside The Component'); }); -test.only('[bug] catches logs outside the component during reading the stream', async () => { +test('[bug] catches logs outside the component during reading the stream', async () => { let resolved = false; let executedIntervals = 0; const readable1 = ReactOnRails.serverRenderRSCReactComponent({ From 9dee2391d9fbea76c624362e759b8df892b6dc10 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 11:03:41 +0200 Subject: [PATCH 19/26] ENABLE_JEST_CONSOLE=y for jest tests --- .github/workflows/package-js-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package-js-tests.yml b/.github/workflows/package-js-tests.yml index ec5406123b..1e4242d823 100644 --- a/.github/workflows/package-js-tests.yml +++ b/.github/workflows/package-js-tests.yml @@ -129,4 +129,4 @@ jobs: - name: Run JS unit tests for react-on-rails package run: pnpm --filter react-on-rails test - name: Run JS unit tests for react-on-rails-pro package - run: pnpm --filter react-on-rails-pro test + run: ENABLE_JEST_CONSOLE=y pnpm --filter react-on-rails-pro test From f3f3fe170d63c8a3336c77a61fdbbe76c2fdf0c1 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 11:15:48 +0200 Subject: [PATCH 20/26] bug investigation changes --- .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 8e76569d2d..1ed1999d57 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -34,7 +34,7 @@ const PromiseContainer = ({ name, onResolved, tick }: { name: string, onResolved resolve(`Value of name ${name}`); } }, 20); - }, 200); + }, 0); }); return ( @@ -140,7 +140,7 @@ test('[bug] catches logs outside the component during reading the stream', async renderingReturnsPromises: true, throwJsErrors: true, domNodeId: 'dom-id', - props: { name: 'First Unique Name', onResolved: () => { resolved = true; }, tick: () => { executedIntervals += 1 } }, + props: { name: 'Bug Investigation Name', onResolved: () => { resolved = true; }, tick: () => { executedIntervals += 1 } }, }); let content1 = ''; @@ -166,7 +166,7 @@ test('[bug] catches logs outside the component during reading the stream', async clearInterval(intervalId); expect(resolved).toBe(true); - expect(content1).toContain('First Unique Name'); + expect(content1).toContain('Bug Investigation Name'); expect(content1).not.toContain('From Interval'); // Here's the bug expect(content1).toContain('Outside The Component'); From 6275fb401762645ded45afbdbf11c1a6a2bf541b Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 11:22:20 +0200 Subject: [PATCH 21/26] trick --- .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 1ed1999d57..b8fc3d226c 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -145,13 +145,15 @@ test('[bug] catches logs outside the component during reading the stream', async let content1 = ''; let i = 0; + let j = 0; readable1.on('data', (chunk: Buffer) => { i += 1; if (i === 1) { expect(resolved).toBe(false); } // To avoid infinite loop - if (i < 5) { + if (executedIntervals > 0 && j < 5) { + j += 1; console.log('Outside The Component'); console.log(`Interval Count: ${executedIntervals}`); } From 6af7949fada22cf16746aa612747452db1383215 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 11:47:35 +0200 Subject: [PATCH 22/26] Revert "trick" This reverts commit 6275fb401762645ded45afbdbf11c1a6a2bf541b. --- .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index b8fc3d226c..1ed1999d57 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -145,15 +145,13 @@ test('[bug] catches logs outside the component during reading the stream', async let content1 = ''; let i = 0; - let j = 0; readable1.on('data', (chunk: Buffer) => { i += 1; if (i === 1) { expect(resolved).toBe(false); } // To avoid infinite loop - if (executedIntervals > 0 && j < 5) { - j += 1; + if (i < 5) { console.log('Outside The Component'); console.log(`Interval Count: ${executedIntervals}`); } From 1ae0f1dba8315b09dc7f3213d3725409e11e4dc7 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 12:06:05 +0200 Subject: [PATCH 23/26] revert all changes to reproduce the console leakage bug on CI --- .github/workflows/package-js-tests.yml | 2 +- packages/react-on-rails-pro/package.json | 1 - ...serverRenderRSCReactComponent.rsc.test.tsx | 41 +++++++------------ 3 files changed, 15 insertions(+), 29 deletions(-) diff --git a/.github/workflows/package-js-tests.yml b/.github/workflows/package-js-tests.yml index 1e4242d823..ec5406123b 100644 --- a/.github/workflows/package-js-tests.yml +++ b/.github/workflows/package-js-tests.yml @@ -129,4 +129,4 @@ jobs: - name: Run JS unit tests for react-on-rails package run: pnpm --filter react-on-rails test - name: Run JS unit tests for react-on-rails-pro package - run: ENABLE_JEST_CONSOLE=y pnpm --filter react-on-rails-pro test + run: pnpm --filter react-on-rails-pro test diff --git a/packages/react-on-rails-pro/package.json b/packages/react-on-rails-pro/package.json index c3fa6614f7..231f7052f6 100644 --- a/packages/react-on-rails-pro/package.json +++ b/packages/react-on-rails-pro/package.json @@ -8,7 +8,6 @@ "build-watch": "pnpm run clean && tsc --watch", "clean": "rm -rf ./lib", "test": "pnpm run test:non-rsc && pnpm run test:rsc", - "jest": "jest", "test:non-rsc": "jest tests --testPathIgnorePatterns=\".*(RSC|stream|registerServerComponent|serverRenderReactComponent|SuspenseHydration).*\"", "test:rsc": "node scripts/check-react-version.cjs || NODE_CONDITIONS=react-server jest tests/*.rsc.test.*", "type-check": "tsc --noEmit --noErrorTruncation", diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 1ed1999d57..4053f8280e 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -11,37 +11,31 @@ import { finished } from 'stream/promises'; import { text } from 'stream/consumers'; import ReactOnRails, { RailsContextWithServerStreamingCapabilities } from '../src/ReactOnRailsRSC.ts'; -const PromiseWrapper = async ({ promise, name, onResolved }: { promise: Promise; name: string, onResolved?: () => {} }) => { +const PromiseWrapper = async ({ promise, name }: { promise: Promise; name: string }) => { console.log(`[${name}] Before awaitng`); const value = await promise; - if (onResolved) { - onResolved(); - } console.log(`[${name}] After awaitng`); return

Value: {value}

; }; -const PromiseContainer = ({ name, onResolved, tick }: { name: string, onResolved?: () => {}, tick?: () => {} }) => { +const PromiseContainer = ({ name }: { name: string }) => { const promise = new Promise((resolve) => { let i = 0; - setTimeout(() => { - const intervalId = setInterval(() => { - console.log(`Interval ${i} at [${name}]`); - i += 1; - tick?.(); - if (i === 50) { - clearInterval(intervalId); - resolve(`Value of name ${name}`); - } - }, 20); - }, 0); + const intervalId = setInterval(() => { + console.log(`Interval ${i} at [${name}]`); + i += 1; + if (i === 50) { + clearInterval(intervalId); + resolve(`Value of name ${name}`); + } + }, 1); }); return (

Initial Header

Loading Promise

}> - +
); @@ -129,8 +123,6 @@ test('no logs lekage from outside the component', async () => { }); test('[bug] catches logs outside the component during reading the stream', async () => { - let resolved = false; - let executedIntervals = 0; const readable1 = ReactOnRails.serverRenderRSCReactComponent({ railsContext: { reactClientManifestFileName: 'react-client-manifest.json', @@ -140,20 +132,16 @@ test('[bug] catches logs outside the component during reading the stream', async renderingReturnsPromises: true, throwJsErrors: true, domNodeId: 'dom-id', - props: { name: 'Bug Investigation Name', onResolved: () => { resolved = true; }, tick: () => { executedIntervals += 1 } }, + props: { name: 'First Unique Name' }, }); let content1 = ''; let i = 0; readable1.on('data', (chunk: Buffer) => { i += 1; - if (i === 1) { - expect(resolved).toBe(false); - } // To avoid infinite loop if (i < 5) { console.log('Outside The Component'); - console.log(`Interval Count: ${executedIntervals}`); } content1 += chunk.toString(); }); @@ -164,10 +152,9 @@ test('[bug] catches logs outside the component during reading the stream', async }, 2); await finished(readable1); clearInterval(intervalId); - expect(resolved).toBe(true); - expect(content1).toContain('Bug Investigation Name'); + expect(content1).toContain('First Unique Name'); expect(content1).not.toContain('From Interval'); // Here's the bug expect(content1).toContain('Outside The Component'); -}, 10000); +}); From 617e7c883cae8df57e625246ad3bfeef82663043 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 12:07:03 +0200 Subject: [PATCH 24/26] increase number of logged messages outside the component --- .../tests/serverRenderRSCReactComponent.rsc.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx index 4053f8280e..7d61388dc9 100644 --- a/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/serverRenderRSCReactComponent.rsc.test.tsx @@ -140,7 +140,7 @@ test('[bug] catches logs outside the component during reading the stream', async readable1.on('data', (chunk: Buffer) => { i += 1; // To avoid infinite loop - if (i < 5) { + if (i < 10) { console.log('Outside The Component'); } content1 += chunk.toString(); From 2d00a85451ab2c1d3e8d0ca15fdf10b49c3c5bec Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 12:32:53 +0200 Subject: [PATCH 25/26] make async quque waits for all chunks to be received --- .../react-on-rails-pro/tests/AsyncQueue.ts | 83 +++++++++++-------- .../react-on-rails-pro/tests/StreamReader.ts | 2 +- ...oncurrentRSCPayloadGeneration.rsc.test.tsx | 24 +----- ...treamServerRenderedReactComponent.test.jsx | 2 +- .../tests/utils/removeRSCChunkStack.ts | 19 +++-- 5 files changed, 69 insertions(+), 61 deletions(-) diff --git a/packages/react-on-rails-pro/tests/AsyncQueue.ts b/packages/react-on-rails-pro/tests/AsyncQueue.ts index 0cf6c843b8..10de9cf101 100644 --- a/packages/react-on-rails-pro/tests/AsyncQueue.ts +++ b/packages/react-on-rails-pro/tests/AsyncQueue.ts @@ -1,22 +1,32 @@ import * as EventEmitter from 'node:events'; -class AsyncQueue { - private eventEmitter = new EventEmitter(); - - private buffer: T[] = []; +const debounce = ( + callback: (...args: T) => void, + delay: number, +) => { + let timeoutTimer: ReturnType; + + return (...args: T) => { + clearTimeout(timeoutTimer); + + timeoutTimer = setTimeout(() => { + callback(...args); + }, delay); + }; +}; +class AsyncQueue { + private eventEmitter = new EventEmitter<{ data: any, end: any }>(); + private buffer: string = ''; private isEnded = false; - enqueue(value: T) { + enqueue(value: string) { if (this.isEnded) { - throw new Error('Queue Ended'); + throw new Error("Queue Ended"); } - if (this.eventEmitter.listenerCount('data') > 0) { - this.eventEmitter.emit('data', value); - } else { - this.buffer.push(value); - } + this.buffer += value; + this.eventEmitter.emit('data', value); } end() { @@ -25,33 +35,38 @@ class AsyncQueue { } dequeue() { - return new Promise((resolve, reject) => { - const bufferValueIfExist = this.buffer.length > 0 ? this.buffer.join('') : undefined; - this.buffer.length = 0; - if (bufferValueIfExist) { - resolve(bufferValueIfExist as T); - } else if (this.isEnded) { - reject(new Error('Queue Ended')); - } else { - let teardown = () => {}; - const onData = (value: T) => { - resolve(value); - teardown(); - }; + return new Promise((resolve, reject) => { + if (this.isEnded) { + reject(new Error("Queue Ended")); + return; + } - const onEnd = () => { + const checkBuffer = debounce(() => { + const teardown = () => { + this.eventEmitter.off('data', checkBuffer); + this.eventEmitter.off('end', checkBuffer); + } + + if (this.buffer.length > 0) { + resolve(this.buffer); + this.buffer = ''; + teardown(); + } else if (this.isEnded) { reject(new Error('Queue Ended')); teardown(); - }; - - this.eventEmitter.on('data', onData); - this.eventEmitter.on('end', onEnd); - teardown = () => { - this.eventEmitter.off('data', onData); - this.eventEmitter.off('end', onEnd); - }; + } + }, 250); + + if (this.buffer.length > 0) { + checkBuffer(); } - }); + this.eventEmitter.on('data', checkBuffer); + this.eventEmitter.on('end', checkBuffer); + }) + } + + toString() { + return "" } } diff --git a/packages/react-on-rails-pro/tests/StreamReader.ts b/packages/react-on-rails-pro/tests/StreamReader.ts index 86ee5195b4..f42ebf867b 100644 --- a/packages/react-on-rails-pro/tests/StreamReader.ts +++ b/packages/react-on-rails-pro/tests/StreamReader.ts @@ -2,7 +2,7 @@ import { PassThrough, Readable } from 'node:stream'; import AsyncQueue from './AsyncQueue.ts'; class StreamReader { - private asyncQueue: AsyncQueue; + private asyncQueue: AsyncQueue; constructor(pipeableStream: Pick) { this.asyncQueue = new AsyncQueue(); diff --git a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx index 9f6a8b254d..d80c9c3b72 100644 --- a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx @@ -31,7 +31,7 @@ afterEach(() => mock.restore()); const AsyncQueueItem = async ({ asyncQueue, children, -}: PropsWithChildren<{ asyncQueue: AsyncQueue }>) => { +}: PropsWithChildren<{ asyncQueue: AsyncQueue }>) => { const value = await asyncQueue.dequeue(); return ( @@ -42,7 +42,7 @@ const AsyncQueueItem = async ({ ); }; -const AsyncQueueContainer = ({ asyncQueue }: { asyncQueue: AsyncQueue }) => { +const AsyncQueueContainer = ({ asyncQueue }: { asyncQueue: AsyncQueue }) => { return (

Async Queue

@@ -78,7 +78,7 @@ const renderComponent = (props: Record) => { }; const createParallelRenders = (size: number) => { - const asyncQueues = new Array(size).fill(null).map(() => new AsyncQueue()); + const asyncQueues = new Array(size).fill(null).map(() => new AsyncQueue()); const streams = asyncQueues.map((asyncQueue) => { return renderComponent({ asyncQueue }); }); @@ -100,21 +100,13 @@ const createParallelRenders = (size: number) => { return { enqueue, expectNextChunk, expectEndOfStream }; }; -const delay = (ms: number) => - new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, ms); - }); - test('Renders concurrent rsc streams as single rsc stream', async () => { // expect.assertions(258); - const asyncQueue = new AsyncQueue(); + const asyncQueue = new AsyncQueue(); const stream = renderComponent({ asyncQueue }); const reader = new StreamReader(stream); const chunks: string[] = []; - await delay(100); let chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Async Queue'); @@ -123,19 +115,16 @@ test('Renders concurrent rsc streams as single rsc stream', async () => { asyncQueue.enqueue('Random Value1'); - await delay(100); chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Random Value1'); asyncQueue.enqueue('Random Value2'); - await delay(100); chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Random Value2'); asyncQueue.enqueue('Random Value3'); - await delay(100); chunk = await reader.nextChunk(); chunks.push(chunk); expect(chunk).toContain('Random Value3'); @@ -145,17 +134,12 @@ test('Renders concurrent rsc streams as single rsc stream', async () => { const { enqueue, expectNextChunk, expectEndOfStream } = createParallelRenders(50); expect(chunks).toHaveLength(4); - await delay(100); await expectNextChunk(chunks[0]); enqueue('Random Value1'); - await delay(100); await expectNextChunk(chunks[1]); enqueue('Random Value2'); - await delay(100); await expectNextChunk(chunks[2]); enqueue('Random Value3'); - await delay(100); await expectNextChunk(chunks[3]); - await delay(100); await expectEndOfStream(); }); diff --git a/packages/react-on-rails-pro/tests/streamServerRenderedReactComponent.test.jsx b/packages/react-on-rails-pro/tests/streamServerRenderedReactComponent.test.jsx index 7fb0421869..df1e4d6379 100644 --- a/packages/react-on-rails-pro/tests/streamServerRenderedReactComponent.test.jsx +++ b/packages/react-on-rails-pro/tests/streamServerRenderedReactComponent.test.jsx @@ -193,7 +193,7 @@ describe('streamServerRenderedReactComponent', () => { // One of the chunks should have a hasErrors property of true expect(chunks[0].hasErrors || chunks[1].hasErrors).toBe(true); expect(chunks[0].hasErrors && chunks[1].hasErrors).toBe(false); - }, 100000); + }, 10000); it("doesn't emit an error if there is an error in the async content and throwJsErrors is false", async () => { const { renderResult, chunks } = setupStreamTest({ throwAsyncError: true, throwJsErrors: false }); diff --git a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts index 57795540a6..cb0f28c971 100644 --- a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts +++ b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts @@ -13,19 +13,28 @@ const removeRSCChunkStackInternal = (chunk: string) => { } const { html } = parsedJson; const santizedHtml = html.split('\n').map((chunkLine) => { - if (!chunkLine.includes('"stack":')) { + if (/^[0-9a-fA-F]+\:D/.exec(chunkLine) || chunkLine.startsWith(':N')) { + return ''; + } + if (!(chunkLine.includes('"stack":') || chunkLine.includes('"start":') || chunkLine.includes('"end":'))) { return chunkLine; } - const regexMatch = /(^\d+):\{/.exec(chunkLine); + const regexMatch = /([^\{]+)\{/.exec(chunkLine) if (!regexMatch) { return chunkLine; } const chunkJsonString = chunkLine.slice(chunkLine.indexOf('{')); - const chunkJson = JSON.parse(chunkJsonString) as { stack?: string }; - delete chunkJson.stack; - return `${regexMatch[1]}:${JSON.stringify(chunkJson)}`; + try { + const chunkJson = JSON.parse(chunkJsonString); + delete chunkJson.stack; + delete chunkJson.start; + delete chunkJson.end; + return `${regexMatch[1]}${JSON.stringify(chunkJson)}` + } catch { + return chunkLine + } }); return JSON.stringify({ From e7e1c87b08937c8ba1ea0da4ec35ab34d75dba40 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 14 Dec 2025 12:42:59 +0200 Subject: [PATCH 26/26] linting --- .../react-on-rails-pro/tests/AsyncQueue.ts | 23 ++++++++----------- ...oncurrentRSCPayloadGeneration.rsc.test.tsx | 5 +--- .../tests/utils/removeRSCChunkStack.ts | 6 ++--- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/react-on-rails-pro/tests/AsyncQueue.ts b/packages/react-on-rails-pro/tests/AsyncQueue.ts index 10de9cf101..7222560db8 100644 --- a/packages/react-on-rails-pro/tests/AsyncQueue.ts +++ b/packages/react-on-rails-pro/tests/AsyncQueue.ts @@ -1,14 +1,11 @@ import * as EventEmitter from 'node:events'; -const debounce = ( - callback: (...args: T) => void, - delay: number, -) => { +const debounce = (callback: (...args: T) => void, delay: number) => { let timeoutTimer: ReturnType; - + return (...args: T) => { clearTimeout(timeoutTimer); - + timeoutTimer = setTimeout(() => { callback(...args); }, delay); @@ -16,13 +13,13 @@ const debounce = ( }; class AsyncQueue { - private eventEmitter = new EventEmitter<{ data: any, end: any }>(); + private eventEmitter = new EventEmitter<{ data: any; end: any }>(); private buffer: string = ''; private isEnded = false; enqueue(value: string) { if (this.isEnded) { - throw new Error("Queue Ended"); + throw new Error('Queue Ended'); } this.buffer += value; @@ -37,7 +34,7 @@ class AsyncQueue { dequeue() { return new Promise((resolve, reject) => { if (this.isEnded) { - reject(new Error("Queue Ended")); + reject(new Error('Queue Ended')); return; } @@ -45,8 +42,8 @@ class AsyncQueue { const teardown = () => { this.eventEmitter.off('data', checkBuffer); this.eventEmitter.off('end', checkBuffer); - } - + }; + if (this.buffer.length > 0) { resolve(this.buffer); this.buffer = ''; @@ -62,11 +59,11 @@ class AsyncQueue { } this.eventEmitter.on('data', checkBuffer); this.eventEmitter.on('end', checkBuffer); - }) + }); } toString() { - return "" + return ''; } } diff --git a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx index d80c9c3b72..5001c4059a 100644 --- a/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx +++ b/packages/react-on-rails-pro/tests/concurrentRSCPayloadGeneration.rsc.test.tsx @@ -28,10 +28,7 @@ beforeEach(() => { afterEach(() => mock.restore()); -const AsyncQueueItem = async ({ - asyncQueue, - children, -}: PropsWithChildren<{ asyncQueue: AsyncQueue }>) => { +const AsyncQueueItem = async ({ asyncQueue, children }: PropsWithChildren<{ asyncQueue: AsyncQueue }>) => { const value = await asyncQueue.dequeue(); return ( diff --git a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts index cb0f28c971..4f5f88bb1d 100644 --- a/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts +++ b/packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts @@ -20,7 +20,7 @@ const removeRSCChunkStackInternal = (chunk: string) => { return chunkLine; } - const regexMatch = /([^\{]+)\{/.exec(chunkLine) + const regexMatch = /([^\{]+)\{/.exec(chunkLine); if (!regexMatch) { return chunkLine; } @@ -31,9 +31,9 @@ const removeRSCChunkStackInternal = (chunk: string) => { delete chunkJson.stack; delete chunkJson.start; delete chunkJson.end; - return `${regexMatch[1]}${JSON.stringify(chunkJson)}` + return `${regexMatch[1]}${JSON.stringify(chunkJson)}`; } catch { - return chunkLine + return chunkLine; } });