squeaky
Version:
a minimal nsq tcp client
610 lines (490 loc) • 17.6 kB
JavaScript
'use strict'
const { test } = require('tap')
const { getTopic, getPubDebugger, getSubDebugger } = require('./utils')
const Squeaky = require('../')
test('can publish', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ lookup: undefined, topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
subscriber.on('message', async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
await msg.finish()
resolver()
})
const res = await publisher.publish(topic, { some: 'object' })
assert.equals(res, 'OK')
await received
await Promise.all([
publisher.close(),
subscriber.close()
])
})
test('can publish when using a uri', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(`nsq://127.0.0.1:4150/`)
const subscriber = new Squeaky.Subscriber(`nsq://127.0.0.1:4150/${topic.slice(0, topic.indexOf('#'))}?channel=test&ephemeral`)
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
subscriber.on('message', async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
await msg.finish()
resolver()
})
const res = await publisher.publish(topic, { some: 'object' })
assert.equals(res, 'OK')
await received
await Promise.all([
publisher.close(),
subscriber.close()
])
})
test('can publish when using a uri and setting a default topic and options', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(`nsq://127.0.0.1:4150/${topic.slice(0, topic.indexOf('#'))}?ephemeral&timeout=60000`)
const subscriber = new Squeaky.Subscriber(`nsq://127.0.0.1:4150/${topic.slice(0, topic.indexOf('#'))}?channel=test&ephemeral&timeout=60000`)
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
subscriber.on('message', async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
await msg.finish()
resolver()
})
const res = await publisher.publish({ some: 'object' })
assert.equals(res, 'OK')
await received
await Promise.all([
publisher.close(),
subscriber.close()
])
})
test('can requeue a message', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
let count = 0
subscriber.on('message', async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
if (++count === 1) {
await msg.requeue()
} else {
await msg.finish()
resolver()
}
})
await publisher.publish(topic, { some: 'object' })
await received
await Promise.all([
publisher.close(),
subscriber.close()
])
})
test('can touch a message', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
subscriber.on('message', (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
setTimeout(async () => {
const oldExpiration = msg.expiresIn
await msg.touch()
assert.ok(msg.expiresIn > oldExpiration, 'expiresIn should be larger')
await msg.finish()
resolver()
}, 10)
})
await publisher.publish(topic, { some: 'object' })
await received
await Promise.all([
publisher.close(),
subscriber.close()
])
})
test('can keep a message alive', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, timeout: 1000, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
subscriber.on('message', async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
assert.ok(msg.expiresIn - msg.connection.features.msg_timeout < 25, 'expiresIn is based on msg_timeout')
await msg.keepalive()
assert.ok(msg.expiresIn - msg.connection.features.max_msg_timeout < 25, 'expiresIn is based on max_msg_timeout')
setTimeout(async () => {
const now = Date.now()
assert.equal(msg.expired, false, 'message has not expired')
assert.ok(
msg.expiresIn - (msg.published.getTime() + msg.connection.features.max_msg_timeout - now) > 2000,
'expiresIn should depend on message timestamp, not publication time'
)
await msg.finish()
resolver()
}, 1500)
})
await publisher.publish(topic, { some: 'object' }, 2000)
await received
await Promise.all([
publisher.close(),
subscriber.close()
])
})
test('subscriber can pause handling of messages', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const receivedTwo = new Promise((resolve) => {
resolver = resolve
})
let msgCount = 0
subscriber.on('message', async (msg) => {
msgCount += 1
if (msgCount === 1) {
assert.same(msg.body, { count: 0 }, 'subscriber received the first message')
await msg.finish()
await subscriber.pause()
await publisher.publish(topic, { count: 1 })
setTimeout(async () => {
await subscriber.resume()
}, 1500)
} else {
assert.same(msg.body, { count: 1 }, 'subscriber received the second message')
await msg.finish()
resolver()
}
})
await publisher.publish(topic, { count: 0 })
await receivedTwo
await Promise.all([
publisher.close(),
subscriber.close()
])
})
test('subscriber errors when pausing if subscriber state isn\'t ready', async (assert) => {
const topic = getTopic()
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
await new Promise((resolve) => subscriber.on('ready', resolve))
await subscriber.pause()
try {
await subscriber.pause()
} catch (err) {
assert.match(err, {
message: 'Must be ready in order to pause'
}, 'should throw')
}
return subscriber.close()
})
test('subscriber errors when unpausing if subscriber state isn\'t paused', async (assert) => {
const topic = getTopic()
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
try {
await subscriber.resume()
} catch (err) {
assert.match(err, {
message: 'Must be paused in order to resume'
}, 'should throw')
}
return subscriber.close()
})
test('can publish non-objects', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
const payloads = [
'strings',
5,
Buffer.from('a buffer')
]
let current = 0
const handler = async (msg) => {
const payload = payloads[current++]
assert.same(msg.body, typeof payload === 'string' ? Buffer.from(payload) : payload, 'subscriber received the right message')
await msg.finish()
if (current >= payloads.length) {
resolver()
}
}
subscriber.on('message', handler)
for (const payload of payloads) {
const res = await publisher.publish(topic, payload)
assert.equals(res, 'OK')
}
await received
return Promise.all([
publisher.close(),
subscriber.close()
])
})
test('calling publish twice synchronously works correctly', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
let counts = 0
const handler = async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
await msg.finish()
if (++counts === 2) {
resolver()
}
}
subscriber.on('message', handler)
await Promise.all([
publisher.publish(topic, { some: 'object' }),
publisher.publish(topic, { some: 'object' })
])
await received
return Promise.all([
publisher.close(),
subscriber.close()
])
})
test('can mpublish', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
let counts = 0
const handler = async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
await msg.finish()
if (++counts === 2) {
resolver()
}
}
subscriber.on('message', handler)
await publisher.publish(topic, [{ some: 'object' }, { some: 'object' }])
await received
return Promise.all([
publisher.close(),
subscriber.close()
])
})
test('can dpublish', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const received = new Promise((resolve) => {
resolver = resolve
})
const handler = async (msg) => {
assert.same(msg.body, { some: 'object' }, 'subscriber received the right message')
await msg.finish()
if (Date.now() - 50 > sent) {
resolver()
}
}
subscriber.on('message', handler)
const sent = Date.now()
await publisher.publish(topic, { some: 'object' }, 1)
await received
return Promise.all([
publisher.close(),
subscriber.close()
])
})
test('dpublish returns an error when passed an invalid timeout', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
try {
await publisher.publish(topic, { some: 'object' }, 'notatimeout')
} catch (err) {
assert.match(err, {
message: 'Received error response: E_INVALID DPUB could not parse timeout notatimeout'
}, 'should throw')
}
await publisher.close()
})
test('errors when trying to delay an mpublish', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher(getPubDebugger())
try {
await publisher.publish(topic, [{ some: 'object' }, { another: 'object' }], 500)
} catch (err) {
assert.match(err, {
message: 'Cannot delay a multi publish'
}, 'should throw')
}
return publisher.close()
})
test('reconnects when disconnected', async (assert) => {
const topic = 'squeaky_test'
const publisher = new Squeaky.Publisher(getPubDebugger())
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
await new Promise((resolve) => subscriber.on('ready', resolve))
await publisher.publish(topic, { some: 'object' })
subscriber.connections.get('127.0.0.1:4150').socket.destroy()
await new Promise((resolve) => subscriber.on('disconnect', ({ host, port }) => {
assert.equals(host, '127.0.0.1')
assert.equals(port, 4150)
resolve()
}))
await new Promise((resolve) => subscriber.on('ready', resolve))
publisher.connection.socket.destroy()
await new Promise((resolve) => publisher.on('disconnect', resolve))
await new Promise((resolve) => publisher.on('ready', resolve))
await Promise.all([
subscriber.close(),
publisher.close()
])
})
test('emits an error and stops when reconnectAttempts is exceeded', async (assert) => {
const topic = 'squeaky_test'
const publisher = new Squeaky.Publisher({ maxConnectAttempts: 1, ...getPubDebugger() })
const subscriber = new Squeaky.Subscriber({ maxConnectAttempts: 1, topic, channel: 'test#ephemeral', ...getSubDebugger() })
const subscriberErrored = new Promise((resolve) => subscriber.on('error', (err) => {
assert.equals(err.message, 'Maximum reconnect attempts exceeded')
resolve()
}))
await new Promise((resolve) => subscriber.on('ready', resolve))
await publisher.publish(topic, { some: 'object' })
subscriber.connections.get('127.0.0.1:4150').socket.destroy()
await Promise.all([
subscriberErrored,
new Promise((resolve) => subscriber.on('close', resolve))
])
const publisherErrored = new Promise((resolve) => publisher.on('error', (err) => {
assert.equals(err.message, 'Maximum reconnect attempts exceeded')
resolve()
}))
publisher.connection.socket.destroy()
await Promise.all([
publisherErrored,
new Promise((resolve) => publisher.on('close', resolve))
])
await Promise.all([
subscriber.close(),
publisher.close()
])
})
test('rejects promises when a connection is in a finished state', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher({ maxConnectAttempts: 1, ...getPubDebugger() })
await publisher.publish(topic, { some: 'object' })
const publisherErrored = new Promise((resolve) => publisher.on('error', (err) => {
assert.equals(err.message, 'Maximum reconnect attempts exceeded')
resolve()
}))
publisher.connection.socket.destroy()
await Promise.all([
publisherErrored,
new Promise((resolve) => publisher.on('close', resolve))
])
try {
await publisher.publish(topic, { some: 'object' })
} catch (err) {
assert.equals(err.message, 'The connection has been terminated')
}
await publisher.close()
})
test('errors that occur during a waited operation reject the promise', async (assert) => {
const topic = getTopic()
const publisher = new Squeaky.Publisher({ maxConnectAttempts: 1, ...getPubDebugger() })
// once to establish the connection
await publisher.publish(topic, { some: 'data' })
// don't await here, we want to disconnect before it finishes
const pubResult = publisher.publish(topic, { some: 'data' })
publisher.connection.socket.destroy()
try {
await pubResult
} catch (err) {
assert.equals(err.message, 'Maximum reconnect attempts exceeded')
}
})
test('errors for non-waited operations emit an event', async (assert) => {
const topic = getTopic()
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', ...getSubDebugger() })
let resolver
const errored = new Promise((resolve) => {
resolver = resolve
})
subscriber.once('error', (err) => {
assert.equals(err.host, '127.0.0.1')
assert.equals(err.port, 4150)
assert.equals(err.code, 'E_INVALID')
resolver()
})
subscriber.once('ready', () => {
subscriber.connections.get('127.0.0.1:4150').finish('notamessageid')
})
await errored
return subscriber.close()
})
test('publisher can delay connections', async (assert) => {
const publisher = new Squeaky.Publisher({ autoConnect: false, ...getPubDebugger() })
assert.equals(publisher.connection, undefined)
await publisher.connect()
assert.notEquals(publisher.connection, undefined)
return publisher.close()
})
test('publisher errors when trying to connect twice', async (assert) => {
const publisher = new Squeaky.Publisher({ autoConnect: false, ...getPubDebugger() })
assert.equals(publisher.connection, undefined)
await publisher.connect()
assert.notEquals(publisher.connection, undefined)
try {
await publisher.connect()
} catch (err) {
assert.equals(err.message, 'A connection has already been established')
}
return publisher.close()
})
test('subscriber can delay connections', async (assert) => {
const topic = getTopic()
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', autoConnect: false, ...getSubDebugger() })
assert.equals(subscriber.connections.size, 0)
await subscriber.connect()
assert.equals(subscriber.connections.size, 1)
return subscriber.close()
})
test('subscriber errors when trying to connect twice', async (assert) => {
const topic = getTopic()
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', autoConnect: false, ...getSubDebugger() })
assert.equals(subscriber.connections.size, 0)
await subscriber.connect()
assert.equals(subscriber.connections.size, 1)
try {
await subscriber.connect()
} catch (err) {
assert.equals(err.message, 'A connection has already been established')
}
return subscriber.close()
})
test('subscriber can add a listener before connecting', async (assert) => {
const topic = getTopic()
const subscriber = new Squeaky.Subscriber({ topic, channel: 'test#ephemeral', autoConnect: false, ...getSubDebugger() })
subscriber.on('message', (msg) => msg.finish())
assert.equals(subscriber.connections.size, 0)
await subscriber.connect()
assert.equals(subscriber.connections.size, 1)
return subscriber.close()
})