'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client } = require('..')
const { createServer } = require('node:http')
const { finished, Readable } = require('node:stream')
const { kConnect } = require('../lib/core/symbols')
const EE = require('node:events')
const { kBusy, kRunning, kSize } = require('../lib/core/symbols')
const { maybeWrapStream, consts } = require('./utils/async-iterators')
test('20 times GET with pipelining 10', async (t) => {
const num = 20
t = tspl(t, { plan: 3 * num + 1 })
let count = 0
let countGreaterThanOne = false
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
count++
setTimeout(function () {
countGreaterThanOne = countGreaterThanOne || count > 1
res.end(req.url)
}, 10)
})
after(() => server.close())
// needed to check for a warning on the maxListeners on the socket
function onWarning (warning) {
if (warning.name === 'MaxListenersExceededWarning') {
t.fail()
}
}
process.on('warning', onWarning)
after(() => {
process.removeListener('warning', onWarning)
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
makeRequestAndExpectUrl(client, i, t, () => {
count--
if (i === num - 1) {
t.ok(countGreaterThanOne, 'seen more than one parallel request')
}
})
}
})
await t.completed
})
function makeRequestAndExpectUrl (client, i, t, cb) {
return client.request({ path: '/' + i, method: 'GET', blocking: false }, (err, { statusCode, headers, body }) => {
cb()
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8'))
})
})
}
test('A client should enqueue as much as twice its pipelining factor', async (t) => {
const num = 10
let sent = 0
// x * 6 + 1 t.ok + 5 drain
t = tspl(t, { plan: num * 6 + 1 + 5 + 2 })
let count = 0
let countGreaterThanOne = false
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
count++
t.ok(count <= 5)
setTimeout(function () {
countGreaterThanOne = countGreaterThanOne || count > 1
res.end(req.url)
}, 10)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
for (; sent < 2;) {
t.ok(client[kSize] <= client.pipelining, 'client is not full')
makeRequest()
t.ok(client[kSize] <= client.pipelining, 'we can send more requests')
}
t.ok(client[kBusy], 'client is busy')
t.ok(client[kSize] <= client.pipelining, 'client is full')
makeRequest()
t.ok(client[kBusy], 'we must stop now')
t.ok(client[kBusy], 'client is busy')
t.ok(client[kSize] > client.pipelining, 'client is full')
function makeRequest () {
makeRequestAndExpectUrl(client, sent++, t, () => {
count--
setImmediate(() => {
if (client[kSize] === 0) {
t.ok(countGreaterThanOne, 'seen more than one parallel request')
const start = sent
for (; sent < start + 2 && sent < num;) {
t.ok(client[kSize] <= client.pipelining, 'client is not full')
t.ok(makeRequest())
}
}
})
})
return client[kSize] <= client.pipelining
}
})
await t.completed
})
test('pipeline 1 is 1 active request', async (t) => {
t = tspl(t, { plan: 9 })
let res2
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
res2 = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
after(() => client.destroy())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.strictEqual(client[kSize], 1)
t.ifError(err)
t.strictEqual(client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
finished(data.body, (err) => {
t.ok(err)
client.close((err) => {
t.ifError(err)
})
})
data.body.destroy()
res2.end()
}), undefined)
data.body.resume()
res2.end()
})
t.ok(client[kSize] <= client.pipelining)
t.ok(client[kBusy])
t.strictEqual(client[kSize], 1)
})
await t.completed
})
test('pipelined chunked POST stream', async (t) => {
t = tspl(t, { plan: 4 + 8 + 8 })
let a = 0
let b = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', chunk => {
// Make sure a and b don't interleave.
t.ok(a === 9 || b === 0)
res.write(chunk)
}).on('end', () => {
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: new Readable({
read () {
this.push(++a > 8 ? null : 'a')
}
})
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: new Readable({
read () {
this.push(++b > 8 ? null : 'b')
}
})
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
})
await t.completed
})
test('pipelined chunked POST iterator', async (t) => {
t = tspl(t, { plan: 4 + 8 + 8 })
let a = 0
let b = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', chunk => {
// Make sure a and b don't interleave.
t.ok(a === 9 || b === 0)
res.write(chunk)
}).on('end', () => {
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: (async function * () {
while (++a <= 8) {
yield 'a'
}
})()
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'GET'
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
client.request({
path: '/',
method: 'POST',
body: (async function * () {
while (++b <= 8) {
yield 'b'
}
})()
}, (err, { body }) => {
body.resume()
t.ifError(err)
})
})
await t.completed
})
function erroredInflightPost (bodyType) {
test(`errored POST body lets inflight complete ${bodyType}`, async (t) => {
t = tspl(t, { plan: 6 })
let serverRes
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
serverRes = res
res.write('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.once('data', () => {
client.request({
path: '/',
method: 'POST',
opaque: 'asd',
body: maybeWrapStream(new Readable({
read () {
this.destroy(new Error('kaboom'))
}
}).once('error', (err) => {
t.ok(err)
}).on('error', () => {
// Readable emits error twice...
}), bodyType)
}, (err, data) => {
t.ok(err)
t.strictEqual(data.opaque, 'asd')
})
client.close((err) => {
t.ifError(err)
})
serverRes.end()
})
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
}
erroredInflightPost(consts.STREAM)
erroredInflightPost(consts.ASYNC_ITERATOR)
test('pipelining non-idempotent', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
setTimeout(() => {
res.end('asd')
}, 10)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
let ended = false
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
ended = true
})
})
client.request({
path: '/',
method: 'GET',
idempotent: false
}, (err, data) => {
t.ifError(err)
t.strictEqual(ended, true)
data.body.resume()
})
})
await t.completed
})
function pipeliningNonIdempotentWithBody (bodyType) {
test(`pipelining non-idempotent w body ${bodyType}`, async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
setImmediate(() => {
res.end('asd')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 2
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
let ended = false
let reading = false
client.request({
path: '/',
method: 'POST',
body: maybeWrapStream(new Readable({
read () {
if (reading) {
return
}
reading = true
this.push('asd')
setImmediate(() => {
this.push(null)
ended = true
})
}
}), bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({
path: '/',
method: 'GET',
idempotent: false
}, (err, data) => {
t.ifError(err)
t.strictEqual(ended, true)
data.body.resume()
})
})
await t.completed
})
}
pipeliningNonIdempotentWithBody(consts.STREAM)
pipeliningNonIdempotentWithBody(consts.ASYNC_ITERATOR)
function pipeliningHeadBusy (bodyType) {
test(`pipelining HEAD busy ${bodyType}`, async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client[kConnect](() => {
let ended = false
client.once('disconnect', () => {
t.strictEqual(ended, true)
})
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'HEAD',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
ended = true
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
})
})
await t.completed
})
}
pipeliningHeadBusy(consts.STREAM)
pipeliningHeadBusy(consts.ASYNC_ITERATOR)
test('pipelining empty pipeline before reset', async (t) => {
t = tspl(t, { plan: 8 })
let c = 0
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
if (c++ === 0) {
res.end('asd')
} else {
setTimeout(() => {
res.end('asd')
}, 100)
}
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client[kConnect](() => {
let ended = false
client.once('disconnect', () => {
t.strictEqual(ended, true)
})
client.request({
path: '/',
method: 'GET',
blocking: false
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], false)
client.request({
path: '/',
method: 'HEAD',
body: 'asd'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
ended = true
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], true)
t.strictEqual(client[kRunning], 2)
})
})
await t.completed
})
function pipeliningIdempotentBusy (bodyType) {
test(`pipelining idempotent busy ${bodyType}`, async (t) => {
t = tspl(t, { plan: 12 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
client[kConnect](() => {
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
{
const signal = new EE()
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
body: maybeWrapStream(body, bodyType),
signal
}, (err, data) => {
t.ok(err)
})
t.strictEqual(client[kBusy], true)
signal.emit('abort')
t.strictEqual(client[kBusy], true)
}
{
const body = new Readable({
read () { }
})
client.request({
path: '/',
method: 'GET',
idempotent: false,
body: maybeWrapStream(body, bodyType)
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
body.push(null)
t.strictEqual(client[kBusy], true)
}
})
})
await t.completed
})
}
pipeliningIdempotentBusy(consts.STREAM)
pipeliningIdempotentBusy(consts.ASYNC_ITERATOR)
test('pipelining blocked', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true })
let blocking = true
let count = 0
server.on('request', (req, res) => {
t.ok(!count || !blocking)
count++
setImmediate(() => {
res.end('asd')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 10
})
after(() => client.close())
client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})
client.request({
path: '/',
method: 'GET',
blocking: true
}, (err, data) => {
t.ifError(err)
blocking = false
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})