Skip to content

Commit 544ad6b

Browse files
committed
feat: add option to use streaming responses for server
Pass streaming: True to buildServer function to enable.
1 parent ee533ad commit 544ad6b

File tree

4 files changed

+183
-10
lines changed

4 files changed

+183
-10
lines changed

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Ignore artifacts:
2+
coverage
3+
dist
4+
CHANGELOG.md

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,12 @@ handler and lambda@edge router. They are defined as follows:
6868

6969
**Kind**: global function
7070

71-
| Param | Type | Default | Description |
72-
| -------------- | ------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
73-
| builder | <code>any</code> | | <p>The SvelteKit provided [Builder](https://kit.svelte.dev/docs/types#public-types-builder) object</p> |
74-
| artifactPath | <code>string</code> | <code>&quot;build&quot;</code> | <p>The path where to place to SvelteKit files</p> |
75-
| esbuildOptions | <code>any</code> | | <p>Options to pass to esbuild</p> |
71+
| Param | Type | Default | Description |
72+
| -------------- | -------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
73+
| builder | <code>any</code> | | <p>The SvelteKit provided [Builder](https://kit.svelte.dev/docs/types#public-types-builder) object</p> |
74+
| artifactPath | <code>string</code> | <code>&quot;build&quot;</code> | <p>The path where to place to SvelteKit files</p> |
75+
| esbuildOptions | <code>any</code> | | <p>Options to pass to esbuild</p> |
76+
| streaming | <code>boolean</code> | <code>false</code> | <p>Use Lambda response streaming</p> |
7677

7778
<a name="buildOptions"></a>
7879

index.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ type SiteProps = {
3737
* @param {any} builder The SvelteKit provided [Builder]{@link https://kit.svelte.dev/docs/types#public-types-builder} object
3838
* @param {string} artifactPath The path where to place to SvelteKit files
3939
* @param {any} esbuildOptions Options to pass to esbuild
40+
* @param {boolean} streaming Use Lambda response streaming
4041
* @returns {Promise<SiteProps>}
4142
*/
4243
export async function buildServer(
4344
builder: any,
4445
artifactPath: string = 'build',
45-
esbuildOptions: any = {}
46+
esbuildOptions: any = {},
47+
streaming: boolean = false
4648
): Promise<SiteProps> {
4749
emptyDirSync(artifactPath)
4850

@@ -66,10 +68,13 @@ export async function buildServer(
6668

6769
builder.log.minor('Copying server files.')
6870
await builder.writeServer(artifactPath)
69-
copyFileSync(
70-
`${__dirname}/lambda/serverless.js`,
71-
`${server_directory}/_index.js`
72-
)
71+
72+
let serverlessPath = `${__dirname}/lambda/serverless.js`
73+
if (streaming) {
74+
serverlessPath = `${__dirname}/lambda/serverless_streaming.js`
75+
}
76+
77+
copyFileSync(serverlessPath, `${server_directory}/_index.js`)
7378
copyFileSync(`${__dirname}/lambda/shims.js`, `${server_directory}/shims.js`)
7479

7580
builder.log.minor('Building AWS Lambda server function.')

lambda/serverless_streaming.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { Server } from '../index.js'
2+
import { manifest } from '../manifest.js'
3+
import { splitCookiesString } from 'set-cookie-parser'
4+
5+
export const handler = awslambda.streamifyResponse(
6+
async (event, responseStream, context) => {
7+
const app = new Server(manifest)
8+
const {
9+
rawPath,
10+
headers,
11+
rawQueryString,
12+
body,
13+
requestContext,
14+
isBase64Encoded,
15+
cookies,
16+
} = event
17+
18+
const encoding = isBase64Encoded
19+
? 'base64'
20+
: headers['content-encoding'] || 'utf-8'
21+
const rawBody =
22+
typeof body === 'string' ? Buffer.from(body, encoding) : body
23+
24+
if (cookies) {
25+
headers['cookie'] = cookies.join('; ')
26+
}
27+
28+
const domainName =
29+
'x-forwarded-host' in headers
30+
? headers['x-forwarded-host']
31+
: requestContext.domainName
32+
33+
const origin =
34+
'ORIGIN' in process.env ? process.env['ORIGIN'] : `https://${domainName}`
35+
36+
let rawURL = `${origin}${rawPath}${
37+
rawQueryString ? `?${rawQueryString}` : ''
38+
}`
39+
40+
await app.init({
41+
env: process.env,
42+
})
43+
44+
// Render the app
45+
const request = new Request(rawURL, {
46+
method: requestContext.http.method,
47+
headers: new Headers(headers),
48+
body: rawBody,
49+
})
50+
console.log(request)
51+
52+
const rendered = await app.respond(request, {
53+
platform: { context },
54+
})
55+
56+
let metadata
57+
58+
if (rendered) {
59+
metadata = {
60+
...split_headers(rendered.headers),
61+
statusCode: rendered.status,
62+
}
63+
metadata.headers['cache-control'] = 'no-cache'
64+
} else {
65+
metadata = {
66+
statusCode: 404,
67+
}
68+
}
69+
70+
responseStream = awslambda.HttpResponseStream.from(responseStream, metadata)
71+
72+
if (rendered) {
73+
setResponse(responseStream, rendered)
74+
} else {
75+
responseStream.end()
76+
}
77+
}
78+
)
79+
80+
// Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors) (MIT)
81+
// From: kit/packages/adapter-netlify/src/headers.js
82+
/**
83+
* Splits headers into two categories: single value and multi value
84+
* @param {Headers} headers
85+
* @returns {{
86+
* headers: Record<string, string>,
87+
* cookies: string[]
88+
* }}
89+
*/
90+
export function split_headers(headers) {
91+
/** @type {Record<string, string>} */
92+
const h = {}
93+
94+
/** @type {string[]} */
95+
let c = []
96+
97+
headers.forEach((value, key) => {
98+
if (key === 'set-cookie') {
99+
c = c.concat(splitCookiesString(value))
100+
} else {
101+
h[key] = value
102+
}
103+
})
104+
return {
105+
headers: h,
106+
cookies: c,
107+
}
108+
}
109+
110+
export async function setResponse(res, response) {
111+
if (!response.body) {
112+
res.end()
113+
return
114+
}
115+
116+
if (response.body.locked) {
117+
res.end(
118+
'Fatal error: Response body is locked. ' +
119+
`This can happen when the response was already read (for example through 'response.json()' or 'response.text()').`
120+
)
121+
return
122+
}
123+
124+
const reader = response.body.getReader()
125+
126+
if (res.destroyed) {
127+
reader.cancel()
128+
return
129+
}
130+
131+
const cancel = (/** @type {Error|undefined} */ error) => {
132+
res.off('close', cancel)
133+
res.off('error', cancel)
134+
135+
// If the reader has already been interrupted with an error earlier,
136+
// then it will appear here, it is useless, but it needs to be catch.
137+
reader.cancel(error).catch(() => {})
138+
if (error) res.destroy(error)
139+
}
140+
141+
res.on('close', cancel)
142+
res.on('error', cancel)
143+
144+
next()
145+
146+
async function next() {
147+
try {
148+
for (;;) {
149+
const { done, value } = await reader.read()
150+
151+
if (done) break
152+
153+
if (!res.write(value)) {
154+
res.once('drain', next)
155+
return
156+
}
157+
}
158+
res.end()
159+
} catch (error) {
160+
cancel(error instanceof Error ? error : new Error(String(error)))
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)