Skip to content

Commit 016d92b

Browse files
jesseskinnerkjmph
authored andcommitted
Add support for streaming async iterator responses to adapter-static
1 parent 8417562 commit 016d92b

File tree

8 files changed

+69
-9
lines changed

8 files changed

+69
-9
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
<h1>This page was prerendered</h1>
1+
<h1>This page was prerendered</h1>
2+
3+
<a href="/test.csv">csv</a>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/** @type {import('@sveltejs/kit').RequestHandler} */
2+
export function get() {
3+
return {
4+
headers: { 'Content-Type': 'text/csv' },
5+
body: generateCSV()
6+
};
7+
}
8+
9+
async function* generateCSV() {
10+
yield '1,one\n';
11+
yield '2,two\n';
12+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<nav>
22
<a href="/">home</a>
33
<a href="/about">about</a>
4+
<a href="/test.txt">test.txt</a>
45
</nav>
56

6-
<slot></slot>
7+
<slot></slot>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<h1>This page was not prerendered</h1>
1+
<h1>This page was not prerendered</h1>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @type {import('@sveltejs/kit').RequestHandler} */
2+
export function get() {
3+
return {
4+
body: stream()
5+
};
6+
}
7+
8+
async function* stream() {
9+
yield 'foo';
10+
yield 'bar';
11+
}

packages/adapter-static/test/test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ run('prerendered', (test) => {
1111
await page.goto(base);
1212
assert.equal(await page.textContent('h1'), 'This page was prerendered');
1313
});
14+
15+
test('prerenders a streaming CSV file', async ({ cwd }) => {
16+
assert.equal(String(fs.readFileSync(`${cwd}/build/test.csv`)), '1,one\n2,two\n');
17+
});
1418
});
1519

1620
run('spa', (test) => {
@@ -35,4 +39,13 @@ run('spa', (test) => {
3539
await page.goto(`${base}/nosuchpage`);
3640
assert.equal(await page.textContent('h1'), '404');
3741
});
42+
43+
test('prerenders a streaming text file', async ({ cwd }) => {
44+
assert.equal(String(fs.readFileSync(`${cwd}/build/test.txt`)), 'foobar');
45+
});
46+
47+
test('renders a streaming text file', async ({ base, page }) => {
48+
const response = await page.goto(`${base}/test.txt`);
49+
assert.equal(String(await response.body()), 'foobar');
50+
});
3851
});

packages/kit/src/core/adapt/prerender.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,23 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
206206
}
207207

208208
if (rendered.status === 200) {
209-
log.info(`${rendered.status} ${decoded_path}`);
210-
writeFileSync(file, rendered.body || '');
209+
log.info(`${rendered.status} ${path}`);
210+
211+
if (
212+
rendered.body &&
213+
typeof rendered.body === 'object' &&
214+
typeof rendered.body[Symbol.asyncIterator] === 'function'
215+
) {
216+
let body = '';
217+
218+
for await (const chunk of rendered.body) {
219+
body += chunk;
220+
}
221+
222+
writeFileSync(file, body);
223+
} else {
224+
writeFileSync(file, rendered.body || '');
225+
}
211226
written_files.push(file);
212227
} else if (response_type !== OK) {
213228
error({ status: rendered.status, path, referrer, referenceType: 'linked' });

packages/kit/src/runtime/server/endpoint.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ function is_string(s) {
1515
return typeof s === 'string' || s instanceof String;
1616
}
1717

18+
/** @param {unknown} s */
19+
function is_async_iterator(s) {
20+
return s && typeof s === 'object' && typeof s[Symbol.asyncIterator] === 'function';
21+
}
22+
1823
/**
1924
* Decides how the body should be parsed based on its mime type. Should match what's in parse_body
2025
*
@@ -28,7 +33,8 @@ function is_content_type_textual(content_type) {
2833
type === 'text/plain' ||
2934
type === 'application/json' ||
3035
type === 'application/x-www-form-urlencoded' ||
31-
type === 'multipart/form-data'
36+
type === 'multipart/form-data' ||
37+
type === 'text/event-stream'
3238
);
3339
}
3440

@@ -67,17 +73,17 @@ export async function render_endpoint(request, route, match) {
6773

6874
const is_type_textual = is_content_type_textual(type);
6975

70-
if (!is_type_textual && !(body instanceof Uint8Array || is_string(body))) {
76+
if (!is_type_textual && !(body instanceof Uint8Array || is_string(body) || is_async_iterator(body))) {
7177
return error(
72-
`${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type`
78+
`${preface}: body must be an instance of string or Uint8Array or an async iterator if content-type is not a supported textual content-type`
7379
);
7480
}
7581

7682
/** @type {import('types/hooks').StrictBody} */
7783
let normalized_body;
7884

7985
// ensure the body is an object
80-
if (body && typeof body === 'object' && typeof body[Symbol.asyncIterator] === 'function') {
86+
if (is_async_iterator(body)) {
8187
normalized_body = /** @type {object} */ (body);
8288
} else if (
8389
(typeof body === 'object' || typeof body === 'undefined') &&

0 commit comments

Comments
 (0)