Skip to content

Commit d589727

Browse files
committed
init
0 parents  commit d589727

File tree

7 files changed

+4826
-0
lines changed

7 files changed

+4826
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.DS_Store
2+
node_modules
3+
coverage

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Jonathan Ong me@jongleberry.com
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# @koa/json-async-iterator
2+
3+
Koa JSON response body streaming for async iterators
4+
5+
## Usage
6+
7+
```js
8+
const jsonAsyncIterator = require('@koa/json-async-iterator')
9+
const app = new Koa()
10+
11+
app.use(jsonAsyncIterator())
12+
13+
app.use(async ctx => {
14+
const iterator = async function* () {
15+
yield Promise.resolve({ foo: 'bar' })
16+
yield Promise.resolve({ baz: 'qux' })
17+
}
18+
ctx.body = iterator()
19+
})
20+
```
21+
22+
Output:
23+
24+
```json
25+
[
26+
{"foo":"bar"}
27+
,
28+
{"baz":"qux"}
29+
]
30+
```
31+
32+
Example with PostgreSQL:
33+
34+
```js
35+
import QueryStream from 'pg-query-stream'
36+
import pg from 'pg'
37+
38+
const client = new pg.Client()
39+
await client.connect()
40+
41+
// later in your middleware
42+
app.use(async (ctx) => {
43+
// stream all the rows from the database to the client
44+
ctx.body = client.query(new QueryStream(`
45+
SELECT *
46+
FROM posts
47+
ORDER BY id ASC
48+
`))
49+
})
50+
```
51+
52+
## Options
53+
54+
### `before`
55+
56+
The string to write before the first value in the iterator.
57+
58+
Default: `'[\n'`
59+
60+
### `separator`
61+
62+
The string to write between each value in the iterator.
63+
64+
Default: `'\n,\n'`
65+
66+
### `after`
67+
68+
The string to write after the last value in the iterator.
69+
70+
Default: `'\n]'`
71+
72+
### `formatError`
73+
74+
A function that takes an error and returns a value to be stringified.
75+
76+
Default: `(err) => ({ error: { message: err.message } })`
77+
78+
### `pretty`
79+
80+
Whether to pretty-print the JSON.
81+
82+
Default: `false`
83+
84+
### `spaces`
85+
86+
The number of spaces to use for pretty-printing.
87+
88+
Default: `2`
89+
90+
## License
91+
92+
MIT

__tests__/index.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const supertest = require('supertest')
2+
const Koa = require('koa')
3+
const jsonAsyncIterator = require('..')
4+
5+
describe('koa-json-async-iterator', () => {
6+
it('should stream an async iterator', async () => {
7+
const app = new Koa()
8+
app.use(jsonAsyncIterator())
9+
app.use(async ctx => {
10+
const iterator = async function* () {
11+
yield new Promise((resolve) => setTimeout(resolve, 100))
12+
yield Promise.resolve({ foo: 'bar' })
13+
yield new Promise((resolve) => setTimeout(resolve, 100))
14+
yield Promise.resolve({ baz: 'qux' })
15+
}
16+
ctx.body = iterator()
17+
})
18+
19+
const response = await supertest(app.callback()).get('/')
20+
expect(response.status).toBe(200)
21+
expect(response.headers['content-type']).toBe('application/json; charset=utf-8')
22+
expect(response.body).toEqual([
23+
{ foo: 'bar' },
24+
{ baz: 'qux' }
25+
])
26+
})
27+
28+
it('should thrown errors', async () => {
29+
const app = new Koa()
30+
app.use(jsonAsyncIterator())
31+
app.use(async ctx => {
32+
const iterator = async function* () {
33+
yield new Promise((resolve) => setTimeout(resolve, 100))
34+
yield Promise.resolve({ foo: 'bar' })
35+
yield new Promise((resolve) => setTimeout(resolve, 100))
36+
yield Promise.resolve({ baz: 'qux' })
37+
yield new Promise((resolve) => setTimeout(resolve, 100))
38+
throw new Error('test')
39+
}
40+
ctx.body = iterator()
41+
})
42+
43+
const response = await supertest(app.callback()).get('/')
44+
expect(response.status).toBe(200)
45+
expect(response.body).toEqual([
46+
{ foo: 'bar' },
47+
{ baz: 'qux' },
48+
{ error: {
49+
message: 'test',
50+
} }
51+
])
52+
})
53+
})

index.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const { PassThrough } = require('stream')
2+
3+
module.exports = (options = {}) => {
4+
const before = options.before || '[\n'
5+
const after = options.after || '\n]'
6+
const separator = options.separator || '\n,\n'
7+
const pretty = options.pretty || false
8+
const spaces = pretty ? options.spaces || 2 : 0
9+
const formatError = options.formatError || ((err) => ({
10+
error: {
11+
message: err.message,
12+
}
13+
}))
14+
return async (ctx, next) => {
15+
await next()
16+
17+
if (ctx.body !== ctx.body?.[Symbol.asyncIterator]?.()) return
18+
19+
const iterator = ctx.body
20+
21+
ctx.response.type = 'json'
22+
const stream =
23+
ctx.response.body = new PassThrough()
24+
25+
stream.write(before)
26+
27+
let ended = false
28+
;(async () => {
29+
let first = true
30+
try {
31+
for await (const value of iterator) {
32+
if (!value) continue
33+
if (first) {
34+
first = false
35+
} else {
36+
stream.write(separator)
37+
}
38+
stream.write(JSON.stringify(value, null, spaces))
39+
}
40+
ended = true
41+
} catch (err) {
42+
if (!first) {
43+
stream.write(separator)
44+
}
45+
stream.write(JSON.stringify(formatError(err), null, spaces))
46+
ctx.app.emit('error', err, ctx)
47+
} finally {
48+
stream.write(after)
49+
stream.end()
50+
}
51+
})()
52+
}
53+
}

0 commit comments

Comments
 (0)