Skip to content
Open
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
32 changes: 32 additions & 0 deletions examples/aws-svelte-kit/src/routes/streaming/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @type {import('./$types').PageServerLoad} */
export async function load() {
// This data is available immediately
const instant = { message: "This rendered instantly!", timestamp: Date.now() };

// These promises are NOT awaited — SvelteKit streams them to the client
const slow = new Promise((resolve) => {
setTimeout(() => {
resolve({
message: "This was streamed after 2 seconds!",
timestamp: Date.now(),
});
}, 2000);
});

const slower = new Promise((resolve) => {
setTimeout(() => {
resolve({
message: "This was streamed after 4 seconds!",
timestamp: Date.now(),
});
}, 4000);
});

return {
instant,
streamed: {
slow,
slower,
},
};
}
71 changes: 71 additions & 0 deletions examples/aws-svelte-kit/src/routes/streaming/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>

<style>
section {
max-width: 600px;
margin: 2rem auto;
font-family: system-ui, sans-serif;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.card h3 {
margin: 0 0 0.5rem;
}
.loading {
color: #888;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.done {
color: #16a34a;
}
code {
background: #f3f4f6;
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.85em;
}
</style>

<section>
<h1>SvelteKit Streaming Demo</h1>
<p>This page demonstrates Lambda response streaming. The shell and instant data arrive first, then streamed promises resolve progressively.</p>

<div class="card">
<h3>Instant Data</h3>
<p class="done">{data.instant.message}</p>
<p><code>timestamp: {data.instant.timestamp}</code></p>
</div>

<div class="card">
<h3>Streamed — 2s delay</h3>
{#await data.streamed.slow}
<p class="loading">Waiting for data...</p>
{:then result}
<p class="done">{result.message}</p>
<p><code>timestamp: {result.timestamp}</code></p>
{/await}
</div>

<div class="card">
<h3>Streamed — 4s delay</h3>
{#await data.streamed.slower}
<p class="loading">Waiting for data...</p>
{:then result}
<p class="done">{result.message}</p>
<p><code>timestamp: {result.timestamp}</code></p>
{/await}
</div>

<p><a href="/">← Back to home</a></p>
</section>
112 changes: 112 additions & 0 deletions platform/functions/sveltekit-server/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Streaming Lambda handler for SvelteKit.
// Copied by SST at deploy time — replaces the buffered handler from svelte-kit-sst.

import fs from "node:fs";
import path from "node:path";
import { installPolyfills } from "@sveltejs/kit/node/polyfills";
import { Server } from "./index.js";
import { manifest } from "./manifest.js";
import prerenderedFiles from "./lambda-handler/prerendered-file-list.js";

installPolyfills();

const app = new Server(manifest);
await app.init({ env: process.env });

export function convertLambdaRequestToNode(event) {
if (event.headers["x-forwarded-host"]) {
event.headers.host = event.headers["x-forwarded-host"];
}

const search = event.rawQueryString.length ? `?${event.rawQueryString}` : "";
const url = new URL(event.rawPath + search, `https://${event.headers.host}`);

const headers = new Headers();
for (let [header, value] of Object.entries(event.headers)) {
if (value) {
headers.append(header, value);
}
}

return new Request(url.href, {
method: event.requestContext.http.method,
headers,
body:
event.body && event.isBase64Encoded
? Buffer.from(event.body, "base64")
: event.body,
});
}

export function isPrerenderedFile(uri, prerenderedFiles) {
uri = uri.replace(/^\/|\/$/g, "");

if (uri === "") {
return prerenderedFiles.includes("index.html") ? "index.html" : undefined;
}

if (prerenderedFiles.includes(uri)) {
return uri;
}
if (prerenderedFiles.includes(uri + "/index.html")) {
return uri + "/index.html";
}
if (prerenderedFiles.includes(uri + ".html")) {
return uri + ".html";
}
}

export const handler = awslambda.streamifyResponse(
async (event, responseStream, context) => {
context.callbackWaitsForEmptyEventLoop = false;

// Check for prerendered files on GET requests
if (event.requestContext?.http?.method === "GET") {
const filePath = isPrerenderedFile(event.rawPath, prerenderedFiles);
if (filePath) {
const body = fs.readFileSync(
path.join("prerendered", filePath),
"utf8",
);
const writer = awslambda.HttpResponseStream.from(responseStream, {
statusCode: 200,
headers: {
"content-type": "text/html",
"cache-control":
"public, max-age=0, s-maxage=31536000, must-revalidate",
},
});
writer.write(body);
writer.end();
return;
}
}

// Handle dynamic requests through SvelteKit
const request = convertLambdaRequestToNode(event);
const response = await app.respond(request, {
getClientAddress: () => event.requestContext.http.sourceIp,
});

const writer = awslambda.HttpResponseStream.from(responseStream, {
statusCode: response.status,
headers: {
...Object.fromEntries(response.headers.entries()),
"Transfer-Encoding": "chunked",
},
cookies: response.headers.getSetCookie(),
});

if (response.body) {
const reader = response.body.getReader();
let readResult = await reader.read();
while (!readResult.done) {
writer.write(readResult.value);
readResult = await reader.read();
}
} else {
writer.write(" ");
}
writer.end();
},
);
18 changes: 13 additions & 5 deletions platform/src/components/aws/svelte-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,14 +437,22 @@ export class SvelteKit extends SsrSite {
}
} catch (e) {}

// Copy streaming handler into the server output directory
fs.copyFileSync(
path.join(
$cli.paths.platform,
"functions",
"sveltekit-server",
"server.mjs",
),
path.join(serverOutputPath, "server.mjs"),
);

return {
base: basepath,
server: {
handler: path.join(
serverOutputPath,
"lambda-handler",
"index.handler",
),
handler: path.join(serverOutputPath, "server.handler"),
streaming: true,
nodejs: {
esbuild: {
minify: process.env.SST_DEBUG ? false : true,
Expand Down
Loading