Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle ResolveSuspended from middleware #42

Merged
merged 5 commits into from
Jan 23, 2024
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
23 changes: 23 additions & 0 deletions .changeset/nasty-geese-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"simple-stack-stream": minor
---

Fix rendering issues when using Layouts with simple stream. This change replaces the `ResolveSuspended` component with automatic rendering via middleware. Once updated, you should now remove `ResolveSuspended` from your project code:

```diff
---
import {
Suspense,
- ResolveSuspended
} from 'simple-stack-stream/components';
---

<Suspense>
<VideoPlayer />
<LoadingSkeleton slot="fallback" />
</Suspense>

<Footer />
- <ResolveSuspended />
```

15 changes: 15 additions & 0 deletions examples/playground/src/components/Layout.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
---


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Suspense</title>
</head>
<body>
<slot />
</body>
</html>
19 changes: 4 additions & 15 deletions examples/playground/src/pages/stream.astro
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
---
import Layout from "../components/Layout.astro";
import Wait from "../components/Wait.astro";
import { Suspense, ResolveSuspended } from 'simple-stack-stream/components'
import { Suspense, } from 'simple-stack-stream/components'
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Suspense</title>
</head>
<body>
<Layout>
<h1>Out of order streaming</h1>
<!-- out-of-order streaming: fallback,
JS to swap content -->
Expand All @@ -28,8 +21,4 @@ import { Suspense, ResolveSuspended } from 'simple-stack-stream/components'
<p>Join the newsletter</p>
</footer>
</Wait>

<!-- render all suspended content -->
<ResolveSuspended />
</body>
</html>
</Layout>
4 changes: 1 addition & 3 deletions packages/stream/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ https://github.com/bholmesdev/simple-stack/assets/51384119/99ed15a4-5a70-4f19-bc

```astro
---
import { Suspense, ResolveSuspended } from 'simple-stack-stream/components';
import { Suspense } from 'simple-stack-stream/components';
---

<h1>Simple stream</h1>
Expand All @@ -20,8 +20,6 @@ import { Suspense, ResolveSuspended } from 'simple-stack-stream/components';
</Suspense>

<Footer />
<!--Render suspended content-->
<ResolveSuspended />
```


Expand Down
10 changes: 4 additions & 6 deletions packages/stream/components/Suspense.astro
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
---
import { customAlphabet, urlAlphabet } from "nanoid";
import type { LocalsWithStreamInternals } from "./types";

const safeId = customAlphabet(urlAlphabet, 10);

const slotPromise = Astro.slots.render('default');
const id = safeId();

const { stream } = Astro.locals as LocalsWithStreamInternals;
stream._internal.components.set(id, slotPromise);

const idx = stream.components.length;
stream.components.push(slotPromise);
---

<simple-suspense id={id}>
<simple-suspense data-suspense-id={idx}>
<slot name="fallback" />
</simple-suspense>
1 change: 0 additions & 1 deletion packages/stream/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default as ResolveSuspended } from "./ResolveSuspended.astro";
export { default as Suspense } from "./Suspense.astro";
4 changes: 1 addition & 3 deletions packages/stream/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export type LocalsWithStreamInternals = {
stream: {
_internal: {
components: Map<string, Promise<string>>;
};
components: Array<Promise<string>>;
};
};
30 changes: 25 additions & 5 deletions packages/stream/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import { defineMiddleware } from "astro/middleware";

export const onRequest = defineMiddleware(({ request, locals }, next) => {
export const onRequest = defineMiddleware(async ({ locals }, next) => {
locals.stream = {
_internal: {
components: new Map(),
},
components: [],
};
let response = await next();
if (!response.headers.get("content-type")?.startsWith("text/html")) {
return response;
}

return next();
async function* render() {
// @ts-expect-error ReadableStream does not have asyncIterator
for await (let chunk of response.body) {
yield chunk;
}
for (let [idx, component] of locals.stream.components.entries()) {
yield `<template data-suspense-id=${JSON.stringify(
idx,
)}>${await component}</template>
<script>
const template = document.querySelector(\`template[data-suspense-id="${idx}"]\`).content;
const dest = document.querySelector(\`simple-suspense[data-suspense-id="${idx}"]\`);
dest.replaceWith(template);
</script>`;
}
}

// @ts-expect-error generator not assignable to ReadableStream
return new Response(render(), response.headers);
});
Binary file modified www/public/assets/simple-stream-intro.mov
Binary file not shown.
26 changes: 1 addition & 25 deletions www/src/content/docs/stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Suspend Astro components with fallback content. Like React Server Components, bu

```astro
---
import { Suspense, ResolveSuspended } from 'simple-stack-stream/components';
import { Suspense } from 'simple-stack-stream/components';
---

<h1>Simple stream</h1>
Expand All @@ -25,8 +25,6 @@ import { Suspense, ResolveSuspended } from 'simple-stack-stream/components';
</Suspense>

<Footer />
<!--Render suspended content-->
<ResolveSuspended />
```

## Installation
Expand Down Expand Up @@ -71,25 +69,3 @@ import { Suspense } from 'simple-stack-stream/components';
</div>
</Suspense>
```

### `ResolveSuspended`

The `<ResolveSuspended />` component renders all suspended content. This component should be placed at the _end_ of your HTML document, ideally before the closing `</body>` tag. This prevents `ResolveSuspended` from blocking components below it when [using Astro SSR](https://docs.astro.build/en/guides/server-side-rendering/#html-streaming).

We recommend [a reusable Layout](https://docs.astro.build/en/core-concepts/layouts/) to ensure this component is present wherever `<Suspense>` is used:

```astro
---
// src/layouts/Layout.astro
import { ResolveSuspended } from 'simple-stack-stream/components';
---

<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<slot />
<ResolveSuspended />
</body>
</html>
```
Loading