diff --git a/.changeset/neat-panthers-notice.md b/.changeset/neat-panthers-notice.md new file mode 100644 index 00000000..548c986c --- /dev/null +++ b/.changeset/neat-panthers-notice.md @@ -0,0 +1,5 @@ +--- +'@inox-tools/request-state': minor +--- + +Fixes flash of page with unsynchronized state due to response streaming. diff --git a/docs/src/content/docs/request-nanostores.mdx b/docs/src/content/docs/request-nanostores.mdx index f45405c1..ddd7731b 100644 --- a/docs/src/content/docs/request-nanostores.mdx +++ b/docs/src/content/docs/request-nanostores.mdx @@ -117,6 +117,10 @@ $comments.set(await loadCommentsForArticle()); ``` +## Caveats + +Enabling Request-Scoped Nanostores disables [response streaming](https://docs.astro.build/en/recipes/streaming-improve-page-performance/). This behavior caused by [request-state](/request-state) package that prevents race condition where nanostores not initialized before client components starts hydrating. + ## License Request Nanostores is available under the MIT license. diff --git a/docs/src/content/docs/request-state.mdx b/docs/src/content/docs/request-state.mdx index a2a7546e..1e23a43b 100644 --- a/docs/src/content/docs/request-state.mdx +++ b/docs/src/content/docs/request-state.mdx @@ -130,6 +130,10 @@ document.addEventListener('@it-astro:server-state-loaded', (event) => { }); ``` +## Caveats + +Enabling Request State disables [response streaming](https://docs.astro.build/en/recipes/streaming-improve-page-performance/). The entire application state must be generated before shipping closing head tag to the client. This behavior prevents data race when client components start hydrating before entire document is loaded. + ## License Astro Request State is available under the MIT license. diff --git a/packages/request-state/src/runtime/middleware.ts b/packages/request-state/src/runtime/middleware.ts index 1856eb2b..cdaf3265 100644 --- a/packages/request-state/src/runtime/middleware.ts +++ b/packages/request-state/src/runtime/middleware.ts @@ -13,31 +13,20 @@ export const onRequest = defineMiddleware(async (_, next) => { if (mediaType !== 'text/html' && !mediaType.startsWith('text/html+')) return result; - const newBody = result.body - ?.pipeThrough(new TextDecoderStream()) - .pipeThrough(injectState(getState)) - .pipeThrough(new TextEncoderStream()); - - return new Response(newBody, result); + const originalBody = await result.text(); + + const headCloseIndex = originalBody.indexOf(''); + if (headCloseIndex > -1) { + const state = getState(); + if (state) { + const stateScript = ``; + + return new Response( + originalBody.slice(0, headCloseIndex) + stateScript + originalBody.slice(headCloseIndex), + result + ); + } + } + + return new Response(originalBody, result); }); - -function injectState(getState: () => string | false) { - let injected = false; - return new TransformStream({ - transform(chunk, controller) { - if (!injected) { - const bodyCloseIndex = chunk.indexOf(''); - if (bodyCloseIndex > -1) { - const state = getState(); - if (state) { - const stateScript = ``; - - chunk = chunk.slice(0, bodyCloseIndex) + stateScript + chunk.slice(bodyCloseIndex); - } - injected = true; - } - } - controller.enqueue(chunk); - }, - }); -}