Skip to content

Commit

Permalink
feat: implement BodyReadable.bytes (#3391)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsctx committed Jul 6, 2024
1 parent 64cd7a9 commit db8e642
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ The `body` mixins are the most common way to format the request/response body. M

- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)

Expand Down
12 changes: 7 additions & 5 deletions docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,11 +488,13 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.

`body` contains the following additional [body mixin](https://fetch.spec.whatwg.org/#body-mixin) methods and properties:

- `text()`
- `json()`
- `arrayBuffer()`
- `body`
- `bodyUsed`
* [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
* [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
* [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
* [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
* [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
* `body`
* `bodyUsed`

`body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.

Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/Fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This API is implemented as per the standard, you can find documentation on [MDN]

- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
Expand Down
42 changes: 33 additions & 9 deletions lib/api/readable.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ class BodyReadable extends Readable {
return consume(this, 'blob')
}

// https://fetch.spec.whatwg.org/#dom-body-bytes
async bytes () {
return consume(this, 'bytes')
}

// https://fetch.spec.whatwg.org/#dom-body-arraybuffer
async arrayBuffer () {
return consume(this, 'arrayBuffer')
Expand Down Expand Up @@ -308,6 +313,31 @@ function chunksDecode (chunks, length) {
return buffer.utf8Slice(start, bufferLength)
}

/**
* @param {Buffer[]} chunks
* @param {number} length
* @returns {Uint8Array}
*/
function chunksConcat (chunks, length) {
if (chunks.length === 0 || length === 0) {
return new Uint8Array(0)
}
if (chunks.length === 1) {
// fast-path
return new Uint8Array(chunks[0])
}
const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer)

let offset = 0
for (let i = 0; i < chunks.length; ++i) {
const chunk = chunks[i]
buffer.set(chunk, offset)
offset += chunk.length
}

return buffer
}

function consumeEnd (consume) {
const { type, body, resolve, stream, length } = consume

Expand All @@ -317,17 +347,11 @@ function consumeEnd (consume) {
} else if (type === 'json') {
resolve(JSON.parse(chunksDecode(body, length)))
} else if (type === 'arrayBuffer') {
const dst = new Uint8Array(length)

let pos = 0
for (const buf of body) {
dst.set(buf, pos)
pos += buf.byteLength
}

resolve(dst.buffer)
resolve(chunksConcat(body, length).buffer)
} else if (type === 'blob') {
resolve(new Blob(body, { type: stream[kContentType] }))
} else if (type === 'bytes') {
resolve(chunksConcat(body, length))
}

consumeFinish(consume)
Expand Down
26 changes: 26 additions & 0 deletions test/client-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,32 @@ test('request arrayBuffer', async (t) => {
await t.completed
})

test('request bytes', async (t) => {
t = tspl(t, { plan: 2 })

const obj = { asd: true }
const server = createServer((req, res) => {
res.end(JSON.stringify(obj))
})
after(() => server.close())

server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())

const { body } = await client.request({
path: '/',
method: 'GET'
})
const bytes = await body.bytes()

t.deepStrictEqual(new TextEncoder().encode(JSON.stringify(obj)), bytes)
t.ok(bytes instanceof Uint8Array)
})

await t.completed
})

test('request body', async (t) => {
t = tspl(t, { plan: 1 })

Expand Down
21 changes: 21 additions & 0 deletions test/readable.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@ describe('Readable', () => {
t.deepStrictEqual(arrayBuffer, expected)
})

test('.bytes()', async function (t) {
t = tspl(t, { plan: 1 })

function resume () {
}
function abort () {
}
const r = new Readable({ resume, abort })

r.push(Buffer.from('hello'))
r.push(Buffer.from(' world'))

process.nextTick(() => {
r.push(null)
})

const bytes = await r.bytes()

t.deepStrictEqual(bytes, new TextEncoder().encode('hello world'))
})

test('.json()', async function (t) {
t = tspl(t, { plan: 1 })

Expand Down
3 changes: 3 additions & 0 deletions test/types/readable.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ expectAssignable<BodyReadable>(new BodyReadable())
// blob
expectAssignable<Promise<Blob>>(readable.blob())

// bytes
expectAssignable<Promise<Uint8Array>>(readable.bytes())

// arrayBuffer
expectAssignable<Promise<ArrayBuffer>>(readable.arrayBuffer())

Expand Down
1 change: 1 addition & 0 deletions types/dispatcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ declare namespace Dispatcher {
readonly bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
bytes(): Promise<Uint8Array>;
formData(): Promise<never>;
json(): Promise<unknown>;
text(): Promise<string>;
Expand Down
5 changes: 5 additions & 0 deletions types/readable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ declare class BodyReadable extends Readable {
*/
blob(): Promise<Blob>

/** Consumes and returns the body as an Uint8Array
* https://fetch.spec.whatwg.org/#dom-body-bytes
*/
bytes(): Promise<Uint8Array>

/** Consumes and returns the body as an ArrayBuffer
* https://fetch.spec.whatwg.org/#dom-body-arraybuffer
*/
Expand Down

0 comments on commit db8e642

Please sign in to comment.