diff --git a/.changeset/large-cobras-return.md b/.changeset/large-cobras-return.md
new file mode 100644
index 0000000..57fd5bb
--- /dev/null
+++ b/.changeset/large-cobras-return.md
@@ -0,0 +1,5 @@
+---
+"simple-stack-stream": minor
+---
+
+Update simple:stream internals to support parallel Suspense boundaries and fix nested Suspense edge cases.
diff --git a/packages/stream/components/ResolveSuspended.astro b/packages/stream/components/ResolveSuspended.astro
deleted file mode 100644
index 8f2d5f1..0000000
--- a/packages/stream/components/ResolveSuspended.astro
+++ /dev/null
@@ -1,26 +0,0 @@
----
-import type { LocalsWithStreamInternals } from "./types";
-
-const { stream } = Astro.locals as LocalsWithStreamInternals;
-
-const entries = [...stream._internal.components.entries()];
-
-const resolvedEntries = await Promise.all(
- entries.map(
- async ([id, slotPromise]) => [id, await slotPromise],
- )
-);
----
-
-{
- resolvedEntries.map( ([id, html]) => (
- <>
-
-
- >
- ))
-}
\ No newline at end of file
diff --git a/packages/stream/components/Suspense.astro b/packages/stream/components/Suspense.astro
index defcddd..364bc1e 100644
--- a/packages/stream/components/Suspense.astro
+++ b/packages/stream/components/Suspense.astro
@@ -1,14 +1,8 @@
---
-import type { LocalsWithStreamInternals } from "./types";
-
-const slotPromise = Astro.slots.render('default');
-
-const { stream } = Astro.locals as LocalsWithStreamInternals;
-
-const idx = stream.components.length;
-stream.components.push(slotPromise);
+const idx = Astro.locals.suspend(Astro.slots.render("default"));
---
-
+
-
+
+
diff --git a/packages/stream/components/env.d.ts b/packages/stream/components/env.d.ts
index f964fe0..da816de 100644
--- a/packages/stream/components/env.d.ts
+++ b/packages/stream/components/env.d.ts
@@ -1 +1,7 @@
///
+
+declare namespace App {
+ interface Locals {
+ suspend(promise: Promise): number;
+ }
+}
diff --git a/packages/stream/src/env.d.ts b/packages/stream/src/env.d.ts
new file mode 100644
index 0000000..da816de
--- /dev/null
+++ b/packages/stream/src/env.d.ts
@@ -0,0 +1,7 @@
+///
+
+declare namespace App {
+ interface Locals {
+ suspend(promise: Promise): number;
+ }
+}
diff --git a/packages/stream/src/middleware.ts b/packages/stream/src/middleware.ts
index a8e711a..469d8ec 100644
--- a/packages/stream/src/middleware.ts
+++ b/packages/stream/src/middleware.ts
@@ -1,28 +1,63 @@
-import { defineMiddleware } from "astro/middleware";
+import { defineMiddleware } from "astro:middleware";
-export const onRequest = defineMiddleware(async ({ locals }, next) => {
- locals.stream = {
- components: [],
- };
- let response = await next();
+type SuspendedChunk = {
+ chunk: string;
+ idx: number;
+};
+
+export const onRequest = defineMiddleware(async (ctx, next) => {
+ const response = await next();
+ // ignore non-HTML responses
if (!response.headers.get("content-type")?.startsWith("text/html")) {
return response;
}
+ let streamController: ReadableStreamDefaultController;
+
async function* render() {
+ // Thank you owoce!
+ // https://gist.github.com/lubieowoce/05a4cb2e8cd252787b54b7c8a41f09fc
+ const stream = new ReadableStream({
+ start(controller) {
+ streamController = controller;
+ },
+ });
+
+ let curId = 0;
+ const pending = new Set>();
+
+ ctx.locals.suspend = (promise) => {
+ const idx = curId++;
+ pending.add(promise);
+ promise
+ .then((chunk) => {
+ streamController.enqueue({ chunk, idx });
+ pending.delete(promise);
+ })
+ .catch((e) => {
+ streamController.error(e);
+ });
+ return idx;
+ };
+
// @ts-expect-error ReadableStream does not have asyncIterator
- for await (let chunk of response.body) {
+ for await (const chunk of response.body) {
yield chunk;
}
- for (let [idx, component] of locals.stream.components.entries()) {
- yield `${await component}
+
+ if (!pending.size) return streamController.close();
+
+ // @ts-expect-error ReadableStream does not have asyncIterator
+ for await (const { chunk, idx } of stream) {
+ yield `${chunk}
`;
+ if (!pending.size) return streamController.close();
}
}