Skip to content

Commit d6e69b0

Browse files
tmp
1 parent 891e780 commit d6e69b0

File tree

4 files changed

+324
-55
lines changed

4 files changed

+324
-55
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { randomUUID } from 'crypto';
2+
import { createClient } from 'redis';
3+
4+
import buildApp from '../src/worker';
5+
import config from './testingNodeRendererConfigs';
6+
import { makeRequest } from './httpRequestUtils';
7+
import { Config } from '../src/shared/configBuilder';
8+
9+
const app = buildApp(config as Partial<Config>);
10+
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
11+
const redisClient = createClient({ url: redisUrl });
12+
13+
// const runningPromises: (string | undefined)[] = [];
14+
// let isRunning = false;
15+
// const OldPromise = globalThis.Promise;
16+
// globalThis.Promise = class Promise<T> extends OldPromise<T> {
17+
// constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
18+
// super(executor); // call native Promise constructor
19+
// if (!isRunning) {
20+
// isRunning = true;
21+
// const stack = new Error().stack;
22+
// runningPromises.push(stack);
23+
// this.then(() => {
24+
// const index = runningPromises.indexOf(stack);
25+
// runningPromises.splice(index, 1);
26+
// });
27+
// isRunning = false;
28+
// }
29+
// }
30+
// };
31+
32+
beforeAll(async () => {
33+
await redisClient.connect();
34+
await app.ready();
35+
await app.listen({ port: 0 });
36+
});
37+
38+
afterAll(async () => {
39+
console.log("Closing app");
40+
await app.close();
41+
console.log("Closed app");
42+
await redisClient.close();
43+
console.log("Closed redis");
44+
}, 20000);
45+
46+
const sendRedisValue = async (redisRequestId: string, key: string, value: string) => {
47+
await redisClient.xAdd(`stream:${redisRequestId}`, '*', { [`:${key}`]: JSON.stringify(value) });
48+
};
49+
50+
const sendRedisItemValue = async (redisRequestId: string, itemIndex: number, value: string) => {
51+
await sendRedisValue(redisRequestId, `Item${itemIndex}`, value);
52+
};
53+
54+
const extractHtmlFromChunks = (chunks: string) => {
55+
chunks.split("\n").map(chunk => chunk.trim().length > 0 ? JSON.parse(chunk).html : chunk).join("");
56+
}
57+
58+
const createParallelRenders = (size: number) => {
59+
const redisRequestIds = Array(size).fill(null).map(() => randomUUID());
60+
const renderRequests = redisRequestIds.map(redisRequestId => {
61+
return makeRequest(app, {
62+
componentName: 'RedisReceiver',
63+
props: { requestId: redisRequestId },
64+
});
65+
});
66+
67+
const expectNextChunk = async (expectedNextChunk: string) => {
68+
const nextChunks = await Promise.all(renderRequests.map(renderRequest => renderRequest.waitForNextChunk()));
69+
nextChunks.forEach((chunk, index) => {
70+
const redisRequestId = redisRequestIds[index]!;
71+
console.log("Asserting Chunk")
72+
expect(extractHtmlFromChunks(chunk.replace(new RegExp(redisRequestId, 'g'), '')))
73+
.toEqual(extractHtmlFromChunks(expectedNextChunk));
74+
});
75+
}
76+
77+
const sendRedisItemValues = async (itemIndex: number, itemValue: string) => {
78+
await Promise.all(redisRequestIds.map(redisRequestId => sendRedisItemValue(redisRequestId, itemIndex, itemValue)));
79+
}
80+
81+
const waitUntilFinished = async () => {
82+
await Promise.all(renderRequests.map(renderRequest => renderRequest.finishedPromise));
83+
renderRequests.forEach(renderRequest => expect(renderRequest.getBuffer()).toHaveLength(0));
84+
}
85+
86+
return {
87+
expectNextChunk,
88+
sendRedisItemValues,
89+
waitUntilFinished,
90+
}
91+
}
92+
93+
test('Happy Path', async () => {
94+
const parallelInstances = 20;
95+
expect.assertions(parallelInstances*7 + 7);
96+
const redisRequestId = randomUUID();
97+
const { waitForNextChunk, finishedPromise, getBuffer } = makeRequest(app, {
98+
componentName: 'RedisReceiver',
99+
props: { requestId: redisRequestId },
100+
});
101+
const chunks: string[] = [];
102+
let chunk = await waitForNextChunk();
103+
expect(chunk).not.toContain('Unique Value');
104+
chunks.push(chunk.replace(new RegExp(redisRequestId, 'g'), ''));
105+
106+
sendRedisItemValue(redisRequestId, 0, 'First Unique Value');
107+
chunk = await waitForNextChunk();
108+
expect(chunk).toContain('First Unique Value');
109+
chunks.push(chunk.replace(new RegExp(redisRequestId, 'g'), ''));
110+
111+
sendRedisItemValue(redisRequestId, 4, 'Fifth Unique Value');
112+
chunk = await waitForNextChunk();
113+
expect(chunk).toContain('Fifth Unique Value');
114+
chunks.push(chunk.replace(new RegExp(redisRequestId, 'g'), ''));
115+
116+
sendRedisItemValue(redisRequestId, 2, 'Third Unique Value');
117+
chunk = await waitForNextChunk();
118+
expect(chunk).toContain('Third Unique Value');
119+
chunks.push(chunk.replace(new RegExp(redisRequestId, 'g'), ''));
120+
121+
sendRedisItemValue(redisRequestId, 1, 'Second Unique Value');
122+
chunk = await waitForNextChunk();
123+
expect(chunk).toContain('Second Unique Value');
124+
chunks.push(chunk.replace(new RegExp(redisRequestId, 'g'), ''));
125+
126+
sendRedisItemValue(redisRequestId, 3, 'Forth Unique Value');
127+
chunk = await waitForNextChunk();
128+
expect(chunk).toContain('Forth Unique Value');
129+
chunks.push(chunk.replace(new RegExp(redisRequestId, 'g'), ''));
130+
131+
await finishedPromise;
132+
expect(getBuffer).toHaveLength(0);
133+
134+
const { expectNextChunk, sendRedisItemValues, waitUntilFinished } = createParallelRenders(parallelInstances);
135+
await expectNextChunk(chunks[0]!);
136+
sendRedisItemValues(0, 'First Unique Value');
137+
await expectNextChunk(chunks[1]!);
138+
sendRedisItemValues(4, 'Fifth Unique Value');
139+
await expectNextChunk(chunks[2]!);
140+
sendRedisItemValues(2, 'Third Unique Value');
141+
await expectNextChunk(chunks[3]!);
142+
sendRedisItemValues(1, 'Second Unique Value');
143+
await expectNextChunk(chunks[4]!);
144+
sendRedisItemValues(3, 'Forth Unique Value');
145+
await expectNextChunk(chunks[5]!);
146+
await waitUntilFinished();
147+
}, 20000);

react_on_rails_pro/packages/node-renderer/tests/htmlStreaming.test.js

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import fs from 'fs';
21
import http2 from 'http2';
3-
import path from 'path';
4-
import FormData from 'form-data';
52
import buildApp from '../src/worker';
63
import config from './testingNodeRendererConfigs';
7-
import { readRenderingRequest } from './helper';
84
import * as errorReporter from '../src/shared/errorReporter';
9-
import packageJson from '../src/shared/packageJson';
5+
import { createForm } from './httpRequestUtils';
106

117
const app = buildApp(config);
128

@@ -21,54 +17,6 @@ afterAll(async () => {
2117

2218
jest.spyOn(errorReporter, 'message').mockImplementation(jest.fn());
2319

24-
const SERVER_BUNDLE_TIMESTAMP = '77777-test';
25-
// Ensure to match the rscBundleHash at `asyncComponentsTreeForTestingRenderingRequest.js` fixture
26-
const RSC_BUNDLE_TIMESTAMP = '88888-test';
27-
28-
const createForm = ({ project = 'spec-dummy', commit = '', props = {}, throwJsErrors = false } = {}) => {
29-
const form = new FormData();
30-
form.append('gemVersion', packageJson.version);
31-
form.append('protocolVersion', packageJson.protocolVersion);
32-
form.append('password', 'myPassword1');
33-
form.append('dependencyBundleTimestamps[]', RSC_BUNDLE_TIMESTAMP);
34-
35-
let renderingRequestCode = readRenderingRequest(
36-
project,
37-
commit,
38-
'asyncComponentsTreeForTestingRenderingRequest.js',
39-
);
40-
renderingRequestCode = renderingRequestCode.replace(/\(\s*\)\s*$/, `(undefined, ${JSON.stringify(props)})`);
41-
if (throwJsErrors) {
42-
renderingRequestCode = renderingRequestCode.replace('throwJsErrors: false', 'throwJsErrors: true');
43-
}
44-
form.append('renderingRequest', renderingRequestCode);
45-
46-
const testBundlesDirectory = path.join(__dirname, '../../../spec/dummy/ssr-generated');
47-
const testClientBundlesDirectory = path.join(__dirname, '../../../spec/dummy/public/webpack/test');
48-
const bundlePath = path.join(testBundlesDirectory, 'server-bundle.js');
49-
form.append(`bundle_${SERVER_BUNDLE_TIMESTAMP}`, fs.createReadStream(bundlePath), {
50-
contentType: 'text/javascript',
51-
filename: 'server-bundle.js',
52-
});
53-
const rscBundlePath = path.join(testBundlesDirectory, 'rsc-bundle.js');
54-
form.append(`bundle_${RSC_BUNDLE_TIMESTAMP}`, fs.createReadStream(rscBundlePath), {
55-
contentType: 'text/javascript',
56-
filename: 'rsc-bundle.js',
57-
});
58-
const clientManifestPath = path.join(testClientBundlesDirectory, 'react-client-manifest.json');
59-
form.append('asset1', fs.createReadStream(clientManifestPath), {
60-
contentType: 'application/json',
61-
filename: 'react-client-manifest.json',
62-
});
63-
const reactServerClientManifestPath = path.join(testBundlesDirectory, 'react-server-client-manifest.json');
64-
form.append('asset2', fs.createReadStream(reactServerClientManifestPath), {
65-
contentType: 'application/json',
66-
filename: 'react-server-client-manifest.json',
67-
});
68-
69-
return form;
70-
};
71-
7220
const makeRequest = async (options = {}) => {
7321
const startTime = Date.now();
7422
const form = createForm(options);
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import http2 from 'http2';
4+
import FormData from 'form-data';
5+
import buildApp from '../src/worker';
6+
import { readRenderingRequest } from './helper';
7+
import packageJson from '../src/shared/packageJson';
8+
9+
export const SERVER_BUNDLE_TIMESTAMP = '77777-test';
10+
// Ensure to match the rscBundleHash at `asyncComponentsTreeForTestingRenderingRequest.js` fixture
11+
export const RSC_BUNDLE_TIMESTAMP = '88888-test';
12+
13+
export const createForm = ({
14+
project = 'spec-dummy',
15+
commit = '',
16+
props = {},
17+
throwJsErrors = false,
18+
componentName = undefined,
19+
} = {}) => {
20+
const form = new FormData();
21+
form.append('gemVersion', packageJson.version);
22+
form.append('protocolVersion', packageJson.protocolVersion);
23+
form.append('password', 'myPassword1');
24+
form.append('dependencyBundleTimestamps[]', RSC_BUNDLE_TIMESTAMP);
25+
26+
let renderingRequestCode = readRenderingRequest(
27+
project,
28+
commit,
29+
'asyncComponentsTreeForTestingRenderingRequest.js',
30+
);
31+
const componentNameString = componentName ? `'${componentName}'` : String(undefined);
32+
renderingRequestCode = renderingRequestCode.replace(
33+
/\(\s*\)\s*$/,
34+
`(${componentNameString}, ${JSON.stringify(props)})`,
35+
);
36+
if (throwJsErrors) {
37+
renderingRequestCode = renderingRequestCode.replace('throwJsErrors: false', 'throwJsErrors: true');
38+
}
39+
form.append('renderingRequest', renderingRequestCode);
40+
41+
const testBundlesDirectory = path.join(__dirname, '../../../spec/dummy/ssr-generated');
42+
const testClientBundlesDirectory = path.join(__dirname, '../../../spec/dummy/public/webpack/test');
43+
const bundlePath = path.join(testBundlesDirectory, 'server-bundle.js');
44+
form.append(`bundle_${SERVER_BUNDLE_TIMESTAMP}`, fs.createReadStream(bundlePath), {
45+
contentType: 'text/javascript',
46+
filename: 'server-bundle.js',
47+
});
48+
const rscBundlePath = path.join(testBundlesDirectory, 'rsc-bundle.js');
49+
form.append(`bundle_${RSC_BUNDLE_TIMESTAMP}`, fs.createReadStream(rscBundlePath), {
50+
contentType: 'text/javascript',
51+
filename: 'rsc-bundle.js',
52+
});
53+
const clientManifestPath = path.join(testClientBundlesDirectory, 'react-client-manifest.json');
54+
form.append('asset1', fs.createReadStream(clientManifestPath), {
55+
contentType: 'application/json',
56+
filename: 'react-client-manifest.json',
57+
});
58+
const reactServerClientManifestPath = path.join(testBundlesDirectory, 'react-server-client-manifest.json');
59+
form.append('asset2', fs.createReadStream(reactServerClientManifestPath), {
60+
contentType: 'application/json',
61+
filename: 'react-server-client-manifest.json',
62+
});
63+
64+
return form;
65+
};
66+
67+
const getAppUrl = (app: ReturnType<typeof buildApp>) => {
68+
const addresssInfo = app.server.address();
69+
if (!addresssInfo) {
70+
throw new Error('The app has no address, ensure to run the app before running tests');
71+
}
72+
73+
if (typeof addresssInfo === 'string') {
74+
return addresssInfo;
75+
}
76+
77+
return `http://localhost:${addresssInfo.port}`;
78+
};
79+
80+
export const makeRequest = (app: ReturnType<typeof buildApp>, options = {}) => {
81+
const form = createForm(options);
82+
const client = http2.connect(getAppUrl(app));
83+
const request = client.request({
84+
':method': 'POST',
85+
':path': `/bundles/${SERVER_BUNDLE_TIMESTAMP}/render/454a82526211afdb215352755d36032c`,
86+
'content-type': `multipart/form-data; boundary=${form.getBoundary()}`,
87+
});
88+
request.setEncoding('utf8');
89+
90+
const buffer: string[] = [];
91+
92+
const statusPromise = new Promise<number | undefined>((resolve) => {
93+
request.on('response', (headers) => {
94+
resolve(headers[':status']);
95+
});
96+
});
97+
98+
let resolveChunksPromise: ((chunks: string) => void) | undefined;
99+
let rejectChunksPromise: ((error: unknown) => void) | undefined;
100+
let resolveChunkPromiseTimeout: NodeJS.Timeout;
101+
102+
const scheduleResolveChunkPromise = () => {
103+
if (resolveChunkPromiseTimeout) {
104+
clearTimeout(resolveChunkPromiseTimeout);
105+
}
106+
107+
resolveChunkPromiseTimeout = setTimeout(() => {
108+
resolveChunksPromise?.(buffer.join(''));
109+
resolveChunksPromise = undefined;
110+
rejectChunksPromise = undefined;
111+
buffer.length = 0;
112+
}, 200);
113+
};
114+
115+
request.on('data', (data) => {
116+
buffer.push(data.toString());
117+
if (resolveChunksPromise) {
118+
scheduleResolveChunkPromise();
119+
}
120+
});
121+
122+
form.pipe(request);
123+
form.on('end', () => {
124+
request.end();
125+
});
126+
127+
const rejectPendingChunkPromise = () => {
128+
if (rejectChunksPromise && buffer.length === 0) {
129+
rejectChunksPromise('Request already eneded');
130+
}
131+
};
132+
133+
const finishedPromise = new Promise<void>((resolve, reject) => {
134+
request.on('end', () => {
135+
client.destroy();
136+
resolve();
137+
rejectPendingChunkPromise();
138+
});
139+
request.on('error', (err) => {
140+
client.destroy();
141+
reject(err);
142+
rejectPendingChunkPromise();
143+
});
144+
}).then(() => console.log("Finished Request"));
145+
146+
const waitForNextChunk = () =>
147+
new Promise<string>((resolve, reject) => {
148+
if (client.closed && buffer.length === 0) {
149+
reject('Request already eneded');
150+
}
151+
resolveChunksPromise = resolve;
152+
rejectChunksPromise = reject;
153+
if (buffer.length > 0) {
154+
scheduleResolveChunkPromise();
155+
}
156+
});
157+
158+
const getBuffer = () => [...buffer];
159+
160+
return {
161+
statusPromise,
162+
finishedPromise,
163+
waitForNextChunk,
164+
getBuffer,
165+
};
166+
};

0 commit comments

Comments
 (0)