Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/react-on-rails-pro-node-renderer/src/worker/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,13 @@ 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,
URLSearchParams,
ReadableStream,
performance,
process,
setTimeout,
setInterval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const makeRequest = async (options = {}) => {
const jsonChunks = [];
let firstByteTime;
let status;
let buffer = '';
const decoder = new TextDecoder();

request.on('response', (headers) => {
Expand All @@ -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) => {
Expand Down Expand Up @@ -197,6 +205,7 @@ describe('html streaming', () => {
expect(fullBody).toContain('branch2 (level 1)');
expect(fullBody).toContain('branch2 (level 0)');

// Fail to findout the chunks content on CI
expect(jsonChunks[0].isShellReady).toBeTruthy();
expect(jsonChunks[0].hasErrors).toBeTruthy();
expect(jsonChunks[0].renderingError).toMatchObject({
Expand Down
6 changes: 3 additions & 3 deletions packages/react-on-rails-pro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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-on-rails-rsc": "^19.0.3"
"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"
}
}
5 changes: 3 additions & 2 deletions packages/react-on-rails-pro/tests/AsyncQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ class AsyncQueue<T> {

dequeue() {
return new Promise<T>((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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,30 +100,42 @@ const createParallelRenders = (size: number) => {
return { enqueue, expectNextChunk, expectEndOfStream };
};

const delay = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});

test('Renders concurrent rsc streams as single rsc stream', async () => {
expect.assertions(258);
// expect.assertions(258);
const asyncQueue = new AsyncQueue<string>();
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');
expect(chunk).toContain('Loading Item2');
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');
Expand All @@ -133,12 +145,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();
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,36 @@ import { finished } from 'stream/promises';
import { text } from 'stream/consumers';
import ReactOnRails, { RailsContextWithServerStreamingCapabilities } from '../src/ReactOnRailsRSC.ts';

const PromiseWrapper = async ({ promise, name }: { promise: Promise<string>; name: string }) => {
const PromiseWrapper = async ({ promise, name, onResolved }: { promise: Promise<string>; name: string, onResolved?: () => {} }) => {
console.log(`[${name}] Before awaitng`);
const value = await promise;
if (onResolved) {
onResolved();
}
console.log(`[${name}] After awaitng`);
return <p>Value: {value}</p>;
};

const PromiseContainer = ({ name }: { name: string }) => {
const PromiseContainer = ({ name, onResolved }: { name: string, onResolved?: () => {} }) => {
const promise = new Promise<string>((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}`);
}
}, 1);
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 (
<div>
<h1>Initial Header</h1>
<Suspense fallback={<p>Loading Promise</p>}>
<PromiseWrapper name={name} promise={promise} />
<PromiseWrapper name={name} promise={promise} onResolved={onResolved} />
</Suspense>
</div>
);
Expand Down Expand Up @@ -123,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',
Expand All @@ -132,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');
Expand All @@ -152,9 +161,10 @@ 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');
// Here's the bug
expect(content1).toContain('Outside The Component');
});
}, 10000);
17 changes: 15 additions & 2 deletions packages/react-on-rails-pro/tests/utils/removeRSCChunkStack.ts
Original file line number Diff line number Diff line change
@@ -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":')) {
Expand All @@ -25,4 +34,8 @@ const removeRSCChunkStack = (chunk: string) => {
});
};

const removeRSCChunkStack = (chunk: string) => {
chunk.split('\n').map(removeRSCChunkStackInternal).join('\n');
};

export default removeRSCChunkStack;
Loading
Loading