Skip to content

Commit 0385d57

Browse files
phryneasCopilot
andauthored
fix race condition in createInjectionTransformStream (#533)
* fix race condition in `createInjectionTransformStream` * specify test title * Update packages/client-react-streaming/src/stream-utils/createInjectionTransformStream.test.tsx Co-authored-by: Copilot <[email protected]> * increase timing for flaky tests --------- Co-authored-by: Copilot <[email protected]>
1 parent 00e61e5 commit 0385d57

File tree

3 files changed

+47
-3
lines changed

3 files changed

+47
-3
lines changed

.changeset/quick-numbers-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client-react-streaming": patch
3+
---
4+
5+
fix race condition in `createInjectionTransformStream`

packages/client-react-streaming/src/stream-utils/createInjectionTransformStream.test.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ test("if the head has been flushed, just insert wherever", async () => {
8787
const { resultPromise, controller } = setupStream(transformStream);
8888

8989
controller.enqueue("<html><head><title>Test</title></head><body>");
90-
await scheduler.wait(1);
90+
await scheduler.wait(10);
9191
injectIntoStream(() => <TestComponent />);
9292
controller.enqueue("</body></html>");
9393
controller.close();
@@ -107,6 +107,36 @@ test("if the head has been flushed, just insert wherever", async () => {
107107
);
108108
});
109109

110+
test("race condition: injection queued while currentlyStreaming is true, no async follow-up chunks", async () => {
111+
const { transformStream, injectIntoStream } =
112+
createInjectionTransformStream();
113+
const { resultPromise, controller } = setupStream(transformStream);
114+
115+
controller.enqueue("<html><head><title>Test</title></head><body>");
116+
await scheduler.wait(10);
117+
controller.enqueue("<di");
118+
injectIntoStream(() => <TestComponent />);
119+
controller.enqueue("v>unrelated content</div>");
120+
controller.enqueue("</body></html>");
121+
// TODO: it would be better if the script chunk ended up inside the `html` tag, but right now it's better here than nowhere. Browsers will be okay with it.
122+
controller.close();
123+
124+
assert.equal(
125+
await resultPromise,
126+
html`<html>
127+
<head>
128+
<title>Test</title>
129+
</head>
130+
<body>
131+
<div>unrelated content</div>
132+
</body>
133+
</html>
134+
<script>
135+
console.log("test");
136+
</script>`
137+
);
138+
});
139+
110140
test("if the head is chopped into pieces, take the last chunk into account when looking for the end of the head", async () => {
111141
const { transformStream, injectIntoStream } =
112142
createInjectionTransformStream();

packages/client-react-streaming/src/stream-utils/createInjectionTransformStream.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ export function createInjectionTransformStream(): {
8383

8484
const transformStream = new TransformStream({
8585
async transform(chunk, controller) {
86-
// While react is flushing chunks, we don't apply insertions
86+
// While react is flushing chunks, we don't apply insertions.
87+
// Injecting during streaming could break HTML by inserting content
88+
// at arbitrary chunk boundaries (e.g., inside partial tags like <di${injected}v>).
8789
if (currentlyStreaming) {
8890
controller.enqueue(chunk);
8991
return;
@@ -109,14 +111,21 @@ export function createInjectionTransformStream(): {
109111
controller.enqueue(textEncoder.encode(content.slice(0, -KEEP_BYTES)));
110112
}
111113
} else {
112-
controller.enqueue(textEncoder.encode(await renderInjectedHtml()));
114+
if (queuedInjections.length > 0) {
115+
controller.enqueue(textEncoder.encode(await renderInjectedHtml()));
116+
}
113117
controller.enqueue(chunk);
114118
currentlyStreaming = true;
115119
setImmediate(() => {
116120
currentlyStreaming = false;
117121
});
118122
}
119123
},
124+
async flush(controller) {
125+
if (queuedInjections.length > 0) {
126+
controller.enqueue(textEncoder.encode(await renderInjectedHtml()));
127+
}
128+
},
120129
});
121130

122131
return {

0 commit comments

Comments
 (0)