Skip to content
Merged
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ pnpm add payload @payloadcms/ui @payloadcms/translations @payloadcms/graphql gra
pnpm add -D @react-router/[email protected] @vitejs/[email protected] vite sass-embedded
```

**Patch `react-router` 8.4.0.** In RSC mode, a client that aborts a document request can stop
the Node process (`TypeError: Invalid state: Unable to enqueue`,
[remix-run/react-router#15611](https://github.com/remix-run/react-router/issues/15611)). Until a
release contains [the fix](https://github.com/remix-run/react-router/pull/15612), copy
[`patches/[email protected]`](patches/[email protected]) into your app and add it to
`pnpm-workspace.yaml`:

```yaml
patchedDependencies:
[email protected]: patches/[email protected]
```

### 2. Switch the app to RSC Framework Mode

```ts
Expand Down Expand Up @@ -263,6 +275,8 @@ A plain "SSR + loaders" React Router app cannot host the admin without changing
## Known limitations

- React Router RSC mode is `unstable_`; pin versions and re-run the bench on upgrades.
- `react-router` 8.4.0 needs a patch, or an aborted request can stop the server: see
[Dependencies](#1-dependencies).
- Mounting the admin at `/` (Payload's `admin-root` setup) is untested; TanStack skips it too.
- The Payload CLI does not detect React Router: use `importMapFile` and `PAYLOAD_CONFIG_PATH`.
- Each admin page is rendered to completion on the server before streaming starts (needed
Expand Down
162 changes: 162 additions & 0 deletions patches/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
diff --git a/dist/development/lib/rsc/html-stream/server.js b/dist/development/lib/rsc/html-stream/server.js
index 326e0ea1136babf7a9ed03bb59087140069758bf..6804d2a45649ed37a2821b2aeb2efb37423e570e 100644
--- a/dist/development/lib/rsc/html-stream/server.js
+++ b/dist/development/lib/rsc/html-stream/server.js
@@ -21,11 +21,31 @@ function injectRSCPayload(rscStream, { nonce } = {}) {
let rscReader = null;
let buffered = [];
let timeout = null;
+ async function stop(reason) {
+ cancelled = true;
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ buffered.length = 0;
+ if (rscReader) await rscReader.cancel(reason).catch(() => {});
+ else await rscStream.cancel(reason).catch(() => {});
+ resolveFlightDataPromise();
+ }
+ function tryEnqueue(controller, chunk) {
+ try {
+ controller.enqueue(chunk);
+ return true;
+ } catch (error) {
+ stop(error);
+ return false;
+ }
+ }
function flushBufferedChunks(controller) {
for (let chunk of buffered) {
let buf = decoder.decode(chunk, { stream: true });
if (buf.endsWith(trailer)) buf = buf.slice(0, -14);
- controller.enqueue(encoder.encode(buf));
+ if (!tryEnqueue(controller, encoder.encode(buf))) return;
}
buffered.length = 0;
timeout = null;
@@ -37,31 +57,32 @@ function injectRSCPayload(rscStream, { nonce } = {}) {
timeout = setTimeout(async () => {
if (cancelled) return;
flushBufferedChunks(controller);
+ if (cancelled) return;
if (!startedRSC) {
startedRSC = true;
rscReader = rscStream.getReader();
- writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => controller.error(err)).then(resolveFlightDataPromise);
+ writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => {
+ if (cancelled) return;
+ try {
+ controller.error(err);
+ } catch (error) {
+ return stop(error);
+ }
+ }).then(resolveFlightDataPromise);
}
}, 0);
},
async flush(controller) {
await flightDataPromise;
+ if (cancelled) return;
if (timeout) {
clearTimeout(timeout);
flushBufferedChunks(controller);
}
- controller.enqueue(encoder.encode("</body></html>"));
+ tryEnqueue(controller, encoder.encode("</body></html>"));
},
async cancel(reason) {
- cancelled = true;
- if (timeout) {
- clearTimeout(timeout);
- timeout = null;
- }
- buffered.length = 0;
- if (rscReader) await rscReader.cancel(reason).catch(() => {});
- else await rscStream.cancel(reason).catch(() => {});
- resolveFlightDataPromise();
+ await stop(reason);
}
});
}
diff --git a/dist/production/lib/rsc/html-stream/server.js b/dist/production/lib/rsc/html-stream/server.js
index 326e0ea1136babf7a9ed03bb59087140069758bf..6804d2a45649ed37a2821b2aeb2efb37423e570e 100644
--- a/dist/production/lib/rsc/html-stream/server.js
+++ b/dist/production/lib/rsc/html-stream/server.js
@@ -21,11 +21,31 @@ function injectRSCPayload(rscStream, { nonce } = {}) {
let rscReader = null;
let buffered = [];
let timeout = null;
+ async function stop(reason) {
+ cancelled = true;
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ buffered.length = 0;
+ if (rscReader) await rscReader.cancel(reason).catch(() => {});
+ else await rscStream.cancel(reason).catch(() => {});
+ resolveFlightDataPromise();
+ }
+ function tryEnqueue(controller, chunk) {
+ try {
+ controller.enqueue(chunk);
+ return true;
+ } catch (error) {
+ stop(error);
+ return false;
+ }
+ }
function flushBufferedChunks(controller) {
for (let chunk of buffered) {
let buf = decoder.decode(chunk, { stream: true });
if (buf.endsWith(trailer)) buf = buf.slice(0, -14);
- controller.enqueue(encoder.encode(buf));
+ if (!tryEnqueue(controller, encoder.encode(buf))) return;
}
buffered.length = 0;
timeout = null;
@@ -37,31 +57,32 @@ function injectRSCPayload(rscStream, { nonce } = {}) {
timeout = setTimeout(async () => {
if (cancelled) return;
flushBufferedChunks(controller);
+ if (cancelled) return;
if (!startedRSC) {
startedRSC = true;
rscReader = rscStream.getReader();
- writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => controller.error(err)).then(resolveFlightDataPromise);
+ writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => {
+ if (cancelled) return;
+ try {
+ controller.error(err);
+ } catch (error) {
+ return stop(error);
+ }
+ }).then(resolveFlightDataPromise);
}
}, 0);
},
async flush(controller) {
await flightDataPromise;
+ if (cancelled) return;
if (timeout) {
clearTimeout(timeout);
flushBufferedChunks(controller);
}
- controller.enqueue(encoder.encode("</body></html>"));
+ tryEnqueue(controller, encoder.encode("</body></html>"));
},
async cancel(reason) {
- cancelled = true;
- if (timeout) {
- clearTimeout(timeout);
- timeout = null;
- }
- buffered.length = 0;
- if (rscReader) await rscReader.cancel(reason).catch(() => {});
- else await rscStream.cancel(reason).catch(() => {});
- resolveFlightDataPromise();
+ await stop(reason);
}
});
}
41 changes: 23 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ onlyBuiltDependencies:
- better-sqlite3
- esbuild
- sharp

patchedDependencies:
[email protected]: patches/[email protected]
Loading