From affa429840d55903320b00475c2273d98a7c480d Mon Sep 17 00:00:00 2001 From: Hans Klunder Date: Fri, 7 Mar 2025 12:38:36 +0000 Subject: [PATCH 1/5] Pending changes exported from your codespace --- abstract.js | 1368 ++++++++++++++++++++-------------------- abstract.js.orig | 1562 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2225 insertions(+), 705 deletions(-) create mode 100644 abstract.js.orig diff --git a/abstract.js b/abstract.js index 4fe72a4..c6937e7 100644 --- a/abstract.js +++ b/abstract.js @@ -1,8 +1,22 @@ -const assert = require('node:assert/strict') -const looseAssert = require('node:assert') const { Readable } = require('node:stream') +const { promisify } = require('node:util') const Packet = require('aedes-packet') +function waitForEvent (obj, resolveEvt) { + return new Promise((resolve, reject) => { + obj.once(resolveEvt, () => { + resolve() + }) + obj.once('error', reject) + }) +} + +function doCleanup (t, instance) { + instance.destroy(() => { + t.diagnostic('instance destroyed') + }) +} + function abstractPersistence (opts) { const test = opts.test let _persistence = opts.persistence @@ -18,7 +32,8 @@ function abstractPersistence (opts) { } } - function persistence (cb) { + const _asyncPersistence = promisify(_persistence) + async function persistence (t) { const mq = buildEmitter() const broker = { id: 'broker-42', @@ -29,28 +44,19 @@ function abstractPersistence (opts) { counter: 0 } - _persistence((err, instance) => { - if (instance) { - // Wait for ready event, if applicable, to ensure the persistence isn't - // destroyed while it's still being set up. - // https://github.com/mcollina/aedes-persistence-redis/issues/41 - if (waitForReady) { - // We have to listen to 'ready' before setting broker because that - // can result in 'ready' being emitted. - instance.on('ready', () => { - instance.removeListener('error', cb) - cb(null, instance) - }) - instance.on('error', cb) - } - instance.broker = broker - if (waitForReady) { - // 'ready' event will call back. - return - } + const instance = await _asyncPersistence() + if (instance) { + // Wait for ready event, if applicable, to ensure the persistence isn't + // destroyed while it's still being set up. + // https://github.com/mcollina/aedes-persistence-redis/issues/41 + if (waitForReady) { + await waitForEvent(instance, 'ready') } - cb(err, instance) - }) + instance.broker = broker + t.diagnostic('instance created') + return instance + } + throw new Error('no instance') } // legacy third party streams are typically not iterable @@ -76,9 +82,19 @@ function abstractPersistence (opts) { } } - function storeRetained (instance, opts, cb) { - opts = opts || {} + function asyncStoreRetained (instance, packet) { + return new Promise((resolve, reject) => { + instance.storeRetained(packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + async function storeRetained (instance, opts = {}) { const packet = { cmd: 'publish', id: instance.broker.id, @@ -87,76 +103,67 @@ function abstractPersistence (opts) { qos: 0, retain: true } - - instance.storeRetained(packet, err => { - cb(err, packet) - }) + await asyncStoreRetained(instance, packet) + return packet } - function matchRetainedWithPattern (t, pattern, opts) { - persistence((err, instance) => { - if (err) { throw err } - - storeRetained(instance, opts, (err, packet) => { - assert.ok(!err, 'no error') - let stream - if (Array.isArray(pattern)) { - stream = instance.createRetainedStreamCombi(pattern) - } else { - stream = instance.createRetainedStream(pattern) - } - - getArrayFromStream(stream).then(list => { - assert.deepEqual(list, [packet], 'must return the packet') - instance.destroy() - }) - }) - }) - } - - function testInstance (title, cb) { - test(title, t => { - persistence((err, instance) => { - if (err) { throw err } - cb(t, instance) - }) - }) + async function matchRetainedWithPattern (t, pattern, opts) { + const instance = await persistence(t) + const packet = await storeRetained(instance, opts) + let stream + if (Array.isArray(pattern)) { + stream = instance.createRetainedStreamCombi(pattern) + } else { + stream = instance.createRetainedStream(pattern) + } + t.diagnostic('created stream') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [packet], 'must return the packet') + t.diagnostic('stream was ok') + doCleanup(t, instance) } function testPacket (t, packet, expected) { if (packet.messageId === null) packet.messageId = undefined - assert.equal(packet.messageId, undefined, 'should have an unassigned messageId in queue') + t.assert.equal(packet.messageId, undefined, 'should have an unassigned messageId in queue') // deepLooseEqual? - looseAssert.deepEqual(structuredClone(packet), expected, 'must return the packet') + t.assert.deepEqual(structuredClone(packet), expected, 'must return the packet') } function deClassed (obj) { return Object.assign({}, obj) } - test('store and look up retained messages', t => { - matchRetainedWithPattern(t, 'hello/world') + // testing starts here + test('store and look up retained messages', async t => { + t.plan(1) + await matchRetainedWithPattern(t, 'hello/world') }) - test('look up retained messages with a # pattern', t => { - matchRetainedWithPattern(t, '#') + test('look up retained messages with a # pattern', async t => { + t.plan(1) + await matchRetainedWithPattern(t, '#') }) - test('look up retained messages with a hello/world/# pattern', t => { - matchRetainedWithPattern(t, 'hello/world/#') + test('look up retained messages with a hello/world/# pattern', async t => { + t.plan(1) + await matchRetainedWithPattern(t, 'hello/world/#') }) - test('look up retained messages with a + pattern', t => { - matchRetainedWithPattern(t, 'hello/+') + test('look up retained messages with a + pattern', async t => { + t.plan(1) + await matchRetainedWithPattern(t, 'hello/+') }) - test('look up retained messages with multiple patterns', t => { - matchRetainedWithPattern(t, ['hello/+', 'other/hello']) + test('look up retained messages with multiple patterns', async t => { + t.plan(1) + await matchRetainedWithPattern(t, ['hello/+', 'other/hello']) }) - testInstance('store multiple retained messages in order', (t, instance) => { + test('store multiple retained messages in order', async (t) => { + t.plan(1000) + const instance = await persistence(t) const totalMessages = 1000 - let done = 0 const retained = { cmd: 'publish', @@ -166,59 +173,40 @@ function abstractPersistence (opts) { retain: true } - function checkIndex (index) { - const packet = new Packet(retained, instance.broker) - - instance.storeRetained(packet, err => { - assert.ok(!err, 'no error') - assert.equal(packet.brokerCounter, index + 1, 'packet stored in order') - if (++done === totalMessages) { - instance.destroy() - } - }) - } - for (let i = 0; i < totalMessages; i++) { - checkIndex(i) + const packet = new Packet(retained, instance.broker) + await storeRetained(instance, packet) + t.assert.equal(packet.brokerCounter, i + 1, 'packet stored in order') } + doCleanup(t, instance) }) - testInstance('remove retained message', (t, instance) => { - storeRetained(instance, {}, (err, packet) => { - assert.ok(!err, 'no error') - storeRetained(instance, { - payload: Buffer.alloc(0) - }, err => { - assert.ok(!err, 'no error') - - const stream = instance.createRetainedStream('#') - getArrayFromStream(stream).then(list => { - assert.deepEqual(list, [], 'must return an empty list') - instance.destroy() - }) - }) + test('remove retained message', async (t) => { + const instance = await persistence(t) + await storeRetained(instance, {}) + await storeRetained(instance, { + payload: Buffer.alloc(0) }) + const stream = instance.createRetainedStream('#') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [], 'must return an empty list') + doCleanup(t, instance) }) - testInstance('storing twice a retained message should keep only the last', (t, instance) => { - storeRetained(instance, {}, (err, packet) => { - assert.ok(!err, 'no error') - storeRetained(instance, { - payload: Buffer.from('ahah') - }, (err, packet) => { - assert.ok(!err, 'no error') - - const stream = instance.createRetainedStream('#') - - getArrayFromStream(stream).then(list => { - assert.deepEqual(list, [packet], 'must return the last packet') - instance.destroy() - }) - }) + test('storing twice a retained message should keep only the last', async (t) => { + const instance = await persistence(t) + await storeRetained(instance, {}) + const packet = await storeRetained(instance, { + payload: Buffer.from('ahah') }) + const stream = instance.createRetainedStream('#') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [packet], 'must return the last packet') + doCleanup(t, instance) }) - testInstance('Create a new packet while storing a retained message', (t, instance) => { + test('Create a new packet while storing a retained message', async (t) => { + const instance = await persistence(t) const packet = { cmd: 'publish', id: instance.broker.id, @@ -229,20 +217,17 @@ function abstractPersistence (opts) { } const newPacket = Object.assign({}, packet) - instance.storeRetained(packet, err => { - assert.ok(!err, 'no error') - // packet reference change to check if a new packet is stored always - packet.retain = false - const stream = instance.createRetainedStream('#') - - getArrayFromStream(stream).then(list => { - assert.deepEqual(list, [newPacket], 'must return the last packet') - instance.destroy() - }) - }) + await asyncStoreRetained(instance, packet) + // packet reference change to check if a new packet is stored always + packet.retain = false + const stream = instance.createRetainedStream('#') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [newPacket], 'must return the last packet') + doCleanup(t, instance) }) - testInstance('store and look up subscriptions by client', (t, instance) => { + test('store and look up subscriptions by client', async (t) => { + const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ topic: 'hello', @@ -265,18 +250,116 @@ function abstractPersistence (opts) { }] instance.addSubscriptions(client, subs, (err, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ok(!err, 'no error') + t.assert.equal(reClient, client, 'client must be the same') + t.assert.ok(!err, 'no error') instance.subscriptionsByClient(client, (err, resubs, reReClient) => { - assert.equal(reReClient, client, 'client must be the same') - assert.ok(!err, 'no error') - assert.deepEqual(resubs, subs) - instance.destroy() + t.assert.equal(reReClient, client, 'client must be the same') + t.assert.ok(!err, 'no error') + t.assert.deepEqual(resubs, subs) + doCleanup(t, instance) }) }) }) - testInstance('remove subscriptions by client', (t, instance) => { + async function addSubscriptions (instance, client, subs) { + return new Promise((resolve, reject) => { + instance.addSubscriptions(client, subs, (err, reClient) => { + if (err) { + reject(err) + } else { + resolve(reClient) + } + }) + }) + } + + async function removeSubscriptions (instance, client, subs) { + return new Promise((resolve, reject) => { + instance.removeSubscriptions(client, subs, (err, reClient) => { + if (err) { + reject(err) + } else { + resolve(reClient) + } + }) + }) + } + + async function subscriptionsByClient (instance, client) { + return new Promise((resolve, reject) => { + instance.subscriptionsByClient(client, (err, resubs, reClient) => { + if (err) { + reject(err) + } else { + resolve({ resubs, reClient }) + } + }) + }) + } + + async function subscriptionsByTopic (instance, topic) { + return new Promise((resolve, reject) => { + instance.subscriptionsByTopic(topic, (err, resubs) => { + if (err) { + reject(err) + } else { + resolve(resubs) + } + }) + }) + } + + async function cleanSubscriptions (instance, client) { + return new Promise((resolve, reject) => { + instance.cleanSubscriptions(client, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + + async function countOffline (instance) { + return new Promise((resolve, reject) => { + instance.countOffline((err, subsCount, clientsCount) => { + if (err) { + reject(err) + } else { + resolve({ subsCount, clientsCount }) + } + }) + }) + } + + async function outgoingEnqueue (instance, sub, packet) { + return new Promise((resolve, reject) => { + instance.outgoingEnqueue(sub, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + + async function outgoingEnqueueCombi (instance, subs, packet) { + return new Promise((resolve, reject) => { + instance.outgoingEnqueueCombi(subs, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + + test('remove subscriptions by client', async (t) => { + t.plan(4) + const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ topic: 'hello', @@ -292,28 +375,25 @@ function abstractPersistence (opts) { nl: false }] - instance.addSubscriptions(client, subs, (err, reClient) => { - assert.ok(!err, 'no error') - instance.removeSubscriptions(client, ['hello'], (err, reClient) => { - assert.ok(!err, 'no error') - assert.equal(reClient, client, 'client must be the same') - instance.subscriptionsByClient(client, (err, resubs, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ok(!err, 'no error') - assert.deepEqual(resubs, [{ - topic: 'matteo', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - instance.destroy() - }) - }) - }) + const reclient1 = await addSubscriptions(instance, client, subs) + t.assert.equal(reclient1, client, 'client must be the same') + const reClient2 = await removeSubscriptions(instance, client, ['hello']) + t.assert.equal(reClient2, client, 'client must be the same') + const { resubs, reClient } = await subscriptionsByClient(instance, client) + t.assert.equal(reClient, client, 'client must be the same') + t.assert.deepEqual(resubs, [{ + topic: 'matteo', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + doCleanup(t, instance) }) - testInstance('store and look up subscriptions by topic', (t, instance) => { + test('store and look up subscriptions by topic', async (t) => { + t.plan(2) + const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ topic: 'hello', @@ -335,31 +415,30 @@ function abstractPersistence (opts) { nl: false }] - instance.addSubscriptions(client, subs, err => { - assert.ok(!err, 'no error') - instance.subscriptionsByTopic('hello', (err, resubs) => { - assert.ok(!err, 'no error') - assert.deepEqual(resubs, [{ - clientId: client.id, - topic: 'hello/#', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - clientId: client.id, - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - instance.destroy() - }) - }) + const reclient = await addSubscriptions(instance, client, subs) + t.assert.equal(reclient, client, 'client must be the same') + const resubs = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs, [{ + clientId: client.id, + topic: 'hello/#', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + clientId: client.id, + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + doCleanup(t, instance) }) - testInstance('get client list after subscriptions', (t, instance) => { + test('get client list after subscriptions', async (t) => { + t.plan(1) + const instance = await persistence(t) const client1 = { id: 'abcde' } const client2 = { id: 'efghi' } const subs = [{ @@ -367,68 +446,52 @@ function abstractPersistence (opts) { qos: 1 }] - instance.addSubscriptions(client1, subs, err => { - assert.ok(!err, 'no error for client 1') - instance.addSubscriptions(client2, subs, err => { - assert.ok(!err, 'no error for client 2') - const stream = instance.getClientList(subs[0].topic) - getArrayFromStream(stream).then(out => { - assert.deepEqual(out, [client1.id, client2.id]) - instance.destroy() - }) - }) - }) + await addSubscriptions(instance, client1, subs) + await addSubscriptions(instance, client2, subs) + const stream = instance.getClientList(subs[0].topic) + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [client1.id, client2.id]) + doCleanup(t, instance) }) - testInstance('get client list after an unsubscribe', (t, instance) => { + test('get client list after an unsubscribe', async (t) => { + t.plan(1) + const instance = await persistence(t) const client1 = { id: 'abcde' } const client2 = { id: 'efghi' } const subs = [{ topic: 'helloagain', qos: 1 }] - - instance.addSubscriptions(client1, subs, err => { - assert.ok(!err, 'no error for client 1') - instance.addSubscriptions(client2, subs, err => { - assert.ok(!err, 'no error for client 2') - instance.removeSubscriptions(client2, [subs[0].topic], (err, reClient) => { - assert.ok(!err, 'no error for removeSubscriptions') - const stream = instance.getClientList(subs[0].topic) - getArrayFromStream(stream).then(out => { - assert.deepEqual(out, [client1.id]) - instance.destroy() - }) - }) - }) - }) + await addSubscriptions(instance, client1, subs) + await addSubscriptions(instance, client2, subs) + await removeSubscriptions(instance, client2, [subs[0].topic]) + const stream = instance.getClientList(subs[0].topic) + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [client1.id]) + doCleanup(t, instance) }) - testInstance('get subscriptions list after an unsubscribe', (t, instance) => { + test('get subscriptions list after an unsubscribe', async (t) => { + t.plan(1) + const instance = await persistence(t) const client1 = { id: 'abcde' } const client2 = { id: 'efghi' } const subs = [{ topic: 'helloagain', qos: 1 }] - - instance.addSubscriptions(client1, subs, err => { - assert.ok(!err, 'no error for client 1') - instance.addSubscriptions(client2, subs, err => { - assert.ok(!err, 'no error for client 2') - instance.removeSubscriptions(client2, [subs[0].topic], (err, reClient) => { - assert.ok(!err, 'no error for removeSubscriptions') - instance.subscriptionsByTopic(subs[0].topic, (err, clients) => { - assert.ok(!err, 'no error getting subscriptions by topic') - assert.deepEqual(clients[0].clientId, client1.id) - instance.destroy() - }) - }) - }) - }) + await addSubscriptions(instance, client1, subs) + await addSubscriptions(instance, client2, subs) + await removeSubscriptions(instance, client2, [subs[0].topic]) + const clients = await subscriptionsByTopic(instance, subs[0].topic) + t.assert.deepEqual(clients[0].clientId, client1.id) + doCleanup(t, instance) }) - testInstance('QoS 0 subscriptions, restored but not matched', (t, instance) => { + test('QoS 0 subscriptions, restored but not matched', async (t) => { + t.plan(2) + const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ topic: 'hello', @@ -450,28 +513,24 @@ function abstractPersistence (opts) { nl: false }] - instance.addSubscriptions(client, subs, err => { - assert.ok(!err, 'no error') - instance.subscriptionsByClient(client, (err, resubs) => { - assert.ok(!err, 'no error') - assert.deepEqual(resubs, subs) - instance.subscriptionsByTopic('hello', (err, resubs2) => { - assert.ok(!err, 'no error') - assert.deepEqual(resubs2, [{ - clientId: client.id, - topic: 'hello/#', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - instance.destroy() - }) - }) - }) + await addSubscriptions(instance, client, subs) + const { resubs } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs, subs) + const resubs2 = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs2, [{ + clientId: client.id, + topic: 'hello/#', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + doCleanup(t, instance) }) - testInstance('clean subscriptions', (t, instance) => { + test('clean subscriptions', async (t) => { + t.plan(4) + const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ topic: 'hello', @@ -481,57 +540,37 @@ function abstractPersistence (opts) { qos: 1 }] - instance.addSubscriptions(client, subs, err => { - assert.ok(!err, 'no error') - instance.cleanSubscriptions(client, err => { - assert.ok(!err, 'no error') - instance.subscriptionsByTopic('hello', (err, resubs) => { - assert.ok(!err, 'no error') - assert.deepEqual(resubs, [], 'no subscriptions') - - instance.subscriptionsByClient(client, (err, resubs) => { - assert.ifError(err) - assert.deepEqual(resubs, null, 'no subscriptions') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 0, 'no subscriptions added') - assert.equal(clientsCount, 0, 'no clients added') - - instance.destroy() - }) - }) - }) - }) - }) + await addSubscriptions(instance, client, subs) + await cleanSubscriptions(instance, client) + const resubs = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs, [], 'no subscriptions') + const { resubs: resubs2 } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs2, null, 'no subscriptions') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 0, 'no subscriptions added') + t.assert.equal(clientsCount, 0, 'no clients added') + doCleanup(t, instance) }) - testInstance('clean subscriptions with no active subscriptions', (t, instance) => { + test('clean subscriptions with no active subscriptions', async (t) => { + t.plan(4) + const instance = await persistence(t) const client = { id: 'abcde' } - instance.cleanSubscriptions(client, err => { - assert.ok(!err, 'no error') - instance.subscriptionsByTopic('hello', (err, resubs) => { - assert.ok(!err, 'no error') - assert.deepEqual(resubs, [], 'no subscriptions') - - instance.subscriptionsByClient(client, (err, resubs) => { - assert.ifError(err) - assert.deepEqual(resubs, null, 'no subscriptions') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 0, 'no subscriptions added') - assert.equal(clientsCount, 0, 'no clients added') - - instance.destroy() - }) - }) - }) - }) + await cleanSubscriptions(instance, client) + const resubs = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs, [], 'no subscriptions') + const { resubs: resubs2 } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs2, null, 'no subscriptions') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 0, 'no subscriptions added') + t.assert.equal(clientsCount, 0, 'no clients added') + doCleanup(t, instance) }) - testInstance('same topic, different QoS', (t, instance) => { + test('same topic, different QoS', async (t) => { + t.plan(5) + const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ topic: 'hello', @@ -547,89 +586,69 @@ function abstractPersistence (opts) { nl: false }] - instance.addSubscriptions(client, subs, (err, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ifError(err, 'no error') - - instance.subscriptionsByClient(client, (err, subsForClient, client) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForClient, [{ - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - - instance.subscriptionsByTopic('hello', (err, subsForTopic) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForTopic, [{ - clientId: 'abcde', - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 1, 'one subscription added') - assert.equal(clientsCount, 1, 'one client added') - - instance.destroy() - }) - }) - }) - }) + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const { resubs } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs, [{ + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + + const resubs2 = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs2, [{ + clientId: 'abcde', + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 1, 'one subscription added') + t.assert.equal(clientsCount, 1, 'one client added') + doCleanup(t, instance) }) - testInstance('replace subscriptions', (t, instance) => { + test('replace subscriptions', async (t) => { + t.plan(25) + const instance = await persistence(t) const client = { id: 'abcde' } const topic = 'hello' const sub = { topic, rh: 0, rap: true, nl: false } const subByTopic = { clientId: client.id, topic, rh: 0, rap: true, nl: false } - function check (qos, cb) { + async function check (qos) { sub.qos = subByTopic.qos = qos - instance.addSubscriptions(client, [sub], (err, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ifError(err, 'no error') - instance.subscriptionsByClient(client, (err, subsForClient, client) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForClient, [sub]) - instance.subscriptionsByTopic(topic, (err, subsForTopic) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForTopic, qos === 0 ? [] : [subByTopic]) - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - if (qos === 0) { - assert.equal(subsCount, 0, 'no subscriptions added') - } else { - assert.equal(subsCount, 1, 'one subscription added') - } - assert.equal(clientsCount, 1, 'one client added') - cb() - }) - }) - }) - }) + const reClient = await addSubscriptions(instance, client, [sub]) + t.assert.equal(reClient, client, 'client must be the same') + const { resubs } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs, [sub]) + const subsForTopic = await subscriptionsByTopic(instance, topic) + t.assert.deepEqual(subsForTopic, qos === 0 ? [] : [subByTopic]) + const { subsCount, clientsCount } = await countOffline(instance) + if (qos === 0) { + t.assert.equal(subsCount, 0, 'no subscriptions added') + } else { + t.assert.equal(subsCount, 1, 'one subscription added') + } + t.assert.equal(clientsCount, 1, 'one client added') } - check(0, () => { - check(1, () => { - check(2, () => { - check(1, () => { - check(0, () => { - instance.destroy() - }) - }) - }) - }) - }) + await check(0) + await check(1) + await check(2) + await check(1) + await check(0) + doCleanup(t, instance) }) - testInstance('replace subscriptions in same call', (t, instance) => { + test('replace subscriptions in same call', async (t) => { + t.plan(5) + const instance = await persistence(t) const client = { id: 'abcde' } const topic = 'hello' const subs = [ @@ -639,27 +658,21 @@ function abstractPersistence (opts) { { topic, qos: 1, rh: 0, rap: true, nl: false }, { topic, qos: 0, rh: 0, rap: true, nl: false } ] - instance.addSubscriptions(client, subs, (err, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ifError(err, 'no error') - instance.subscriptionsByClient(client, (err, subsForClient, client) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForClient, [{ topic, qos: 0, rh: 0, rap: true, nl: false }]) - instance.subscriptionsByTopic(topic, (err, subsForTopic) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForTopic, []) - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 0, 'no subscriptions added') - assert.equal(clientsCount, 1, 'one client added') - instance.destroy() - }) - }) - }) - }) + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const { resubs: subsForClient } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(subsForClient, [{ topic, qos: 0, rh: 0, rap: true, nl: false }]) + const subsForTopic = await subscriptionsByTopic(instance, topic) + t.assert.deepEqual(subsForTopic, []) + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 0, 'no subscriptions added') + t.assert.equal(clientsCount, 1, 'one client added') + doCleanup(t, instance) }) - testInstance('store and count subscriptions', (t, instance) => { + test('store and count subscriptions', async (t) => { + t.plan(11) + const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ topic: 'hello', @@ -672,61 +685,33 @@ function abstractPersistence (opts) { qos: 0 }] - instance.addSubscriptions(client, subs, (err, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ifError(err, 'no error') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 2, 'two subscriptions added') - assert.equal(clientsCount, 1, 'one client added') - - instance.removeSubscriptions(client, ['hello'], (err, reClient) => { - assert.ifError(err, 'no error') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 1, 'one subscription added') - assert.equal(clientsCount, 1, 'one client added') - - instance.removeSubscriptions(client, ['matteo'], (err, reClient) => { - assert.ifError(err, 'no error') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 0, 'zero subscriptions added') - assert.equal(clientsCount, 1, 'one client added') - - instance.removeSubscriptions(client, ['noqos'], (err, reClient) => { - assert.ifError(err, 'no error') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 0, 'zero subscriptions added') - assert.equal(clientsCount, 0, 'zero clients added') - - instance.removeSubscriptions(client, ['noqos'], (err, reClient) => { - assert.ifError(err, 'no error') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, 0, 'zero subscriptions added') - assert.equal(clientsCount, 0, 'zero clients added') - - instance.destroy() - }) - }) - }) - }) - }) - }) - }) - }) - }) - }) + const reclient = await addSubscriptions(instance, client, subs) + t.assert.equal(reclient, client, 'client must be the same') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 2, 'two subscriptions added') + t.assert.equal(clientsCount, 1, 'one client added') + await removeSubscriptions(instance, client, ['hello']) + const { subsCount: subsCount2, clientsCount: clientsCount2 } = await countOffline(instance) + t.assert.equal(subsCount2, 1, 'one subscription added') + t.assert.equal(clientsCount2, 1, 'one client added') + await removeSubscriptions(instance, client, ['matteo']) + const { subsCount: subsCount3, clientsCount: clientsCount3 } = await countOffline(instance) + t.assert.equal(subsCount3, 0, 'zero subscriptions added') + t.assert.equal(clientsCount3, 1, 'one client added') + await removeSubscriptions(instance, client, ['noqos']) + const { subsCount: subsCount4, clientsCount: clientsCount4 } = await countOffline(instance) + t.assert.equal(subsCount4, 0, 'zero subscriptions added') + t.assert.equal(clientsCount4, 0, 'zero clients added') + await removeSubscriptions(instance, client, ['noqos']) + const { subsCount: subsCount5, clientsCount: clientsCount5 } = await countOffline(instance) + t.assert.equal(subsCount5, 0, 'zero subscriptions added') + t.assert.equal(clientsCount5, 0, 'zero clients added') + doCleanup(t, instance) }) - testInstance('count subscriptions with two clients', (t, instance) => { + test('count subscriptions with two clients', async (t) => { + t.plan(26) + const instance = await persistence(t) const client1 = { id: 'abcde' } const client2 = { id: 'fghij' } const subs = [{ @@ -740,51 +725,32 @@ function abstractPersistence (opts) { qos: 0 }] - function remove (client, subs, expectedSubs, expectedClients, cb) { - instance.removeSubscriptions(client, subs, (err, reClient) => { - assert.ifError(err, 'no error') - assert.equal(reClient, client, 'client must be the same') - - instance.countOffline((err, subsCount, clientsCount) => { - assert.ifError(err, 'no error') - assert.equal(subsCount, expectedSubs, 'subscriptions added') - assert.equal(clientsCount, expectedClients, 'clients added') - - cb() - }) - }) - } - - instance.addSubscriptions(client1, subs, (err, reClient) => { - assert.equal(reClient, client1, 'client must be the same') - assert.ifError(err, 'no error') - - instance.addSubscriptions(client2, subs, (err, reClient) => { - assert.equal(reClient, client2, 'client must be the same') - assert.ifError(err, 'no error') - - remove(client1, ['foobar'], 4, 2, () => { - remove(client1, ['hello'], 3, 2, () => { - remove(client1, ['hello'], 3, 2, () => { - remove(client1, ['matteo'], 2, 2, () => { - remove(client1, ['noqos'], 2, 1, () => { - remove(client2, ['hello'], 1, 1, () => { - remove(client2, ['matteo'], 0, 1, () => { - remove(client2, ['noqos'], 0, 0, () => { - instance.destroy() - }) - }) - }) - }) - }) - }) - }) - }) - }) - }) + async function remove (client, subs, expectedSubs, expectedClients) { + const reClient = await removeSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, expectedSubs, 'subscriptions added') + t.assert.equal(clientsCount, expectedClients, 'clients added') + } + + const reClient1 = await addSubscriptions(instance, client1, subs) + t.assert.equal(reClient1, client1, 'client must be the same') + const reClient2 = await addSubscriptions(instance, client2, subs) + t.assert.equal(reClient2, client2, 'client must be the same') + await remove(client1, ['foobar'], 4, 2) + await remove(client1, ['hello'], 3, 2) + await remove(client1, ['hello'], 3, 2) + await remove(client1, ['matteo'], 2, 2) + await remove(client1, ['noqos'], 2, 1) + await remove(client2, ['hello'], 1, 1) + await remove(client2, ['matteo'], 0, 1) + await remove(client2, ['noqos'], 0, 0) + doCleanup(t, instance) }) - testInstance('add duplicate subs to persistence for qos > 0', (t, instance) => { + test('add duplicate subs to persistence for qos > 0', async (t) => { + t.plan(3) + const instance = await persistence(t) const client = { id: 'abcde' } const topic = 'hello' const subs = [{ @@ -795,24 +761,19 @@ function abstractPersistence (opts) { nl: false }] - instance.addSubscriptions(client, subs, (err, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ifError(err, 'no error') - - instance.addSubscriptions(client, subs, (err, resCLient) => { - assert.equal(resCLient, client, 'client must be the same') - assert.ifError(err, 'no error') - subs[0].clientId = client.id - instance.subscriptionsByTopic(topic, (err, subsForTopic) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForTopic, subs) - instance.destroy() - }) - }) - }) + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const reClient2 = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient2, client, 'client must be the same') + subs[0].clientId = client.id + const subsForTopic = await subscriptionsByTopic(instance, topic) + t.assert.deepEqual(subsForTopic, subs) + doCleanup(t, instance) }) - testInstance('add duplicate subs to persistence for qos 0', (t, instance) => { + test('add duplicate subs to persistence for qos 0', async (t) => { + t.plan(3) + const instance = await persistence(t) const client = { id: 'abcde' } const topic = 'hello' const subs = [{ @@ -823,23 +784,18 @@ function abstractPersistence (opts) { nl: false }] - instance.addSubscriptions(client, subs, (err, reClient) => { - assert.equal(reClient, client, 'client must be the same') - assert.ifError(err, 'no error') - - instance.addSubscriptions(client, subs, (err, resCLient) => { - assert.equal(resCLient, client, 'client must be the same') - assert.ifError(err, 'no error') - instance.subscriptionsByClient(client, (err, subsForClient, client) => { - assert.ifError(err, 'no error') - assert.deepEqual(subsForClient, subs) - instance.destroy() - }) - }) - }) + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const reClient2 = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient2, client, 'client must be the same') + const { resubs: subsForClient } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(subsForClient, subs) + doCleanup(t, instance) }) - testInstance('get topic list after concurrent subscriptions of a client', (t, instance) => { + test('get topic list after concurrent subscriptions of a client', async (t) => { + t.plan(3) + const instance = await persistence(t) const client = { id: 'abcde' } const subs1 = [{ topic: 'hello1', @@ -857,28 +813,31 @@ function abstractPersistence (opts) { }] let calls = 2 - function done () { - if (!--calls) { - instance.subscriptionsByClient(client, (err, resubs) => { - assert.ok(!err, 'no error') - resubs.sort((a, b) => b.topic.localeCompare(b.topic, 'en')) - assert.deepEqual(resubs, [subs1[0], subs2[0]]) - instance.destroy() - }) + await new Promise((resolve, reject) => { + async function done () { + if (!--calls) { + const { resubs } = await subscriptionsByClient(instance, client) + resubs.sort((a, b) => a.topic.localeCompare(b.topic, 'en')) + t.assert.deepEqual(resubs, [subs1[0], subs2[0]]) + doCleanup(t, instance) + resolve() + } } - } - instance.addSubscriptions(client, subs1, err => { - assert.ok(!err, 'no error for hello1') - done() - }) - instance.addSubscriptions(client, subs2, err => { - assert.ok(!err, 'no error for hello2') - done() + instance.addSubscriptions(client, subs1, err => { + t.assert.ok(!err, 'no error for hello1') + done() + }) + instance.addSubscriptions(client, subs2, err => { + t.assert.ok(!err, 'no error for hello2') + done() + }) }) }) - testInstance('add outgoing packet and stream it', (t, instance) => { + test('add outgoing packet and stream it', async (t) => { + t.plan(2) + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', @@ -910,19 +869,16 @@ function abstractPersistence (opts) { messageId: undefined } - instance.outgoingEnqueue(sub, packet, err => { - assert.ifError(err) - const stream = instance.outgoingStream(client) - - getArrayFromStream(stream).then(list => { - const packet = list[0] - testPacket(t, packet, expected) - instance.destroy() - }) - }) + await outgoingEnqueue(instance, sub, packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + doCleanup(t, instance) }) - testInstance('add outgoing packet for multiple subs and stream to all', (t, instance) => { + test('add outgoing packet for multiple subs and stream to all', async (t) => { + t.plan(4) + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', @@ -963,24 +919,20 @@ function abstractPersistence (opts) { messageId: undefined } - instance.outgoingEnqueueCombi(subs, packet, err => { - assert.ifError(err) - const stream = instance.outgoingStream(client) - getArrayFromStream(stream).then(list => { - const packet = list[0] - testPacket(t, packet, expected) - - const stream2 = instance.outgoingStream(client2) - getArrayFromStream(stream2).then(list2 => { - const packet = list2[0] - testPacket(t, packet, expected) - instance.destroy() - }) - }) - }) + await outgoingEnqueueCombi(instance, subs, packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + + const stream2 = instance.outgoingStream(client2) + const list2 = await getArrayFromStream(stream2) + testPacket(t, list2[0], expected) + doCleanup(t, instance) }) - testInstance('add outgoing packet as a string and pump', (t, instance) => { + test('add outgoing packet as a string and pump', async (t) => { + t.plan(7) + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', @@ -1008,33 +960,27 @@ function abstractPersistence (opts) { brokerCounter: 50 } const queue = [] - enqueueAndUpdate(t, instance, client, sub, packet1, 42, updated1 => { - enqueueAndUpdate(t, instance, client, sub, packet2, 43, updated2 => { - const stream = instance.outgoingStream(client) - - async function clearQueue (data) { - return new Promise((resolve, reject) => { - instance.outgoingUpdate(client, data, - (err, client, packet) => { - assert.ok(!err, 'no error') - queue.push(packet) - resolve() - }) - }) - } - streamForEach(stream, clearQueue).then(function done () { - assert.equal(queue.length, 2) - if (queue.length === 2) { - assert.deepEqual(deClassed(queue[0]), deClassed(updated1)) - assert.deepEqual(deClassed(queue[1]), deClassed(updated2)) - } - instance.destroy() - }) - }) - }) + + const updated1 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet1, 42) + const updated2 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet2, 43) + const stream = instance.outgoingStream(client) + + async function clearQueue (data) { + const { repacket } = await outgoingUpdate(instance, client, data) + t.diagnostic('packet received') + queue.push(repacket) + } + + await streamForEach(stream, clearQueue) + t.assert.equal(queue.length, 2) + t.assert.deepEqual(deClassed(queue[0]), deClassed(updated1)) + t.assert.deepEqual(deClassed(queue[1]), deClassed(updated2)) + doCleanup(t, instance) }) - testInstance('add outgoing packet as a string and stream', (t, instance) => { + test('add outgoing packet as a string and stream', async (t) => { + t.plan(2) + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', @@ -1066,19 +1012,15 @@ function abstractPersistence (opts) { messageId: undefined } - instance.outgoingEnqueueCombi([sub], packet, err => { - assert.ifError(err) - const stream = instance.outgoingStream(client) - - getArrayFromStream(stream).then(list => { - const packet = list[0] - testPacket(t, packet, expected) - instance.destroy() - }) - }) + await outgoingEnqueueCombi(instance, [sub], packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + doCleanup(t, instance) }) - testInstance('add outgoing packet and stream it twice', (t, instance) => { + test('add outgoing packet and stream it twice', async (t) => { + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', @@ -1111,42 +1053,53 @@ function abstractPersistence (opts) { messageId: undefined } - instance.outgoingEnqueueCombi([sub], packet, err => { - assert.ifError(err) - const stream = instance.outgoingStream(client) + await outgoingEnqueueCombi(instance, [sub], packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + const stream2 = instance.outgoingStream(client) + const list2 = await getArrayFromStream(stream2) + testPacket(t, list2[0], expected) + t.assert.notEqual(packet, expected, 'packet must be a different object') + doCleanup(t, instance) + }) - getArrayFromStream(stream).then(list => { - const packet = list[0] - testPacket(t, packet, expected) + async function enqueueAndUpdate (t, instance, client, sub, packet, messageId) { + await outgoingEnqueueCombi(instance, [sub], packet) + const updated = new Packet(packet) + updated.messageId = messageId - const stream2 = instance.outgoingStream(client) + const { reclient, repacket } = await outgoingUpdate(instance, client, updated) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') + return repacket + } - getArrayFromStream(stream2).then(list2 => { - const packet = list2[0] - testPacket(t, packet, expected) - assert.notEqual(packet, expected, 'packet must be a different object') - instance.destroy() - }) + async function outgoingUpdate (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.outgoingUpdate(client, packet, (err, reclient, repacket) => { + if (err) { + reject(err) + } else { + resolve({ reclient, repacket }) + } }) }) - }) - - function enqueueAndUpdate (t, instance, client, sub, packet, messageId, callback) { - instance.outgoingEnqueueCombi([sub], packet, err => { - assert.ifError(err) - const updated = new Packet(packet) - updated.messageId = messageId + } - instance.outgoingUpdate(client, updated, (err, reclient, repacket) => { - assert.ifError(err) - assert.equal(reclient, client, 'client matches') - assert.equal(repacket, updated, 'packet matches') - callback(updated) - }) - }) + async function asyncEnqueueAndUpdate (t, instance, client, sub, packet, messageId) { + await outgoingEnqueueCombi(instance, [sub], packet) + const updated = new Packet(packet) + updated.messageId = messageId + const { reclient, repacket } = await outgoingUpdate(instance, client, updated) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') + return updated } - testInstance('add outgoing packet and update messageId', (t, instance) => { + test('add outgoing packet and update messageId', async (t) => { + t.plan(5) + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 } @@ -1165,20 +1118,19 @@ function abstractPersistence (opts) { brokerCounter: 42 } - enqueueAndUpdate(t, instance, client, sub, packet, 42, updated => { - const stream = instance.outgoingStream(client) - delete updated.messageId - getArrayFromStream(stream).then(list => { - delete list[0].messageId - assert.notEqual(list[0], updated, 'must not be the same object') - assert.deepEqual(deClassed(list[0]), deClassed(updated), 'must return the packet') - assert.equal(list.length, 1, 'must return only one packet') - instance.destroy() - }) - }) + const updated = await enqueueAndUpdate(t, instance, client, sub, packet, 42) + delete updated.messageId + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + delete list[0].messageId + t.assert.notEqual(list[0], updated, 'must not be the same object') + t.assert.deepEqual(deClassed(list[0]), deClassed(updated), 'must return the packet') + t.assert.equal(list.length, 1, 'must return only one packet') + doCleanup(t, instance) }) - testInstance('add 2 outgoing packet and clear messageId', (t, instance) => { + test('add 2 outgoing packet and clear messageId', async (t) => { + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 } @@ -1208,28 +1160,26 @@ function abstractPersistence (opts) { brokerCounter: 43 } - enqueueAndUpdate(t, instance, client, sub, packet1, 42, updated1 => { - enqueueAndUpdate(t, instance, client, sub, packet2, 43, updated2 => { - instance.outgoingClearMessageId(client, updated1, (err, packet) => { - assert.ifError(err) - assert.deepEqual(packet.messageId, 42, 'must have the same messageId') - assert.deepEqual(packet.payload.toString(), packet1.payload.toString(), 'must have original payload') - assert.deepEqual(packet.topic, packet1.topic, 'must have original topic') - const stream = instance.outgoingStream(client) - delete updated2.messageId - getArrayFromStream(stream).then(list => { - delete list[0].messageId - assert.notEqual(list[0], updated2, 'must not be the same object') - assert.deepEqual(deClassed(list[0]), deClassed(updated2), 'must return the packet') - assert.equal(list.length, 1, 'must return only one packet') - instance.destroy() - }) - }) - }) + const updated1 = await enqueueAndUpdate(t, instance, client, sub, packet1, 42) + const updated2 = await enqueueAndUpdate(t, instance, client, sub, packet2, 43) + instance.outgoingClearMessageId(client, updated1, async (err, packet) => { + t.assert.ifError(err) + t.assert.deepEqual(packet.messageId, 42, 'must have the same messageId') + t.assert.deepEqual(packet.payload.toString(), packet1.payload.toString(), 'must have original payload') + t.assert.deepEqual(packet.topic, packet1.topic, 'must have original topic') + const stream = instance.outgoingStream(client) + delete updated2.messageId + const list = await getArrayFromStream(stream) + delete list[0].messageId + t.assert.notEqual(list[0], updated2, 'must not be the same object') + t.assert.deepEqual(deClassed(list[0]), deClassed(updated2), 'must return the packet') + t.assert.equal(list.length, 1, 'must return only one packet') + doCleanup(t, instance) }) }) - testInstance('add many outgoing packets and clear messageIds', async (t, instance) => { + test('add many outgoing packets and clear messageIds', async (t) => { + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 } @@ -1270,8 +1220,8 @@ function abstractPersistence (opts) { function clearMessage (p) { return new Promise((resolve, reject) => { instance.outgoingClearMessageId(client, p, (err, received) => { - assert.ifError(err) - assert.deepEqual(received, p, 'must return the packet') + t.assert.ifError(err) + t.assert.deepEqual(received, p, 'must return the packet') resolve() }) }) @@ -1287,7 +1237,7 @@ function abstractPersistence (opts) { queued++ } } - assert.equal(queued, total, `outgoing queue must hold ${total} items`) + t.assert.equal(queued, total, `outgoing queue must hold ${total} items`) for await (const p of outStream(instance, client)) { await clearMessage(p) @@ -1299,11 +1249,12 @@ function abstractPersistence (opts) { queued2++ } } - assert.equal(queued2, 0, 'outgoing queue is empty') - instance.destroy() + t.assert.equal(queued2, 0, 'outgoing queue is empty') + doCleanup(t, instance) }) - testInstance('update to publish w/ same messageId', (t, instance) => { + test('update to publish w/ same messageId', async (t) => { + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 } @@ -1341,12 +1292,12 @@ function abstractPersistence (opts) { instance.outgoingUpdate(client, packet2, () => { const stream = instance.outgoingStream(client) getArrayFromStream(stream).then(list => { - assert.equal(list.length, 2, 'must have two items in queue') - assert.equal(list[0].brokerCounter, packet1.brokerCounter, 'brokerCounter must match') - assert.equal(list[0].messageId, packet1.messageId, 'messageId must match') - assert.equal(list[1].brokerCounter, packet2.brokerCounter, 'brokerCounter must match') - assert.equal(list[1].messageId, packet2.messageId, 'messageId must match') - instance.destroy() + t.assert.equal(list.length, 2, 'must have two items in queue') + t.assert.equal(list[0].brokerCounter, packet1.brokerCounter, 'brokerCounter must match') + t.assert.equal(list[0].messageId, packet1.messageId, 'messageId must match') + t.assert.equal(list[1].brokerCounter, packet2.brokerCounter, 'brokerCounter must match') + t.assert.equal(list[1].messageId, packet2.messageId, 'messageId must match') + doCleanup(t, instance) }) }) }) @@ -1354,7 +1305,8 @@ function abstractPersistence (opts) { }) }) - testInstance('update to pubrel', (t, instance) => { + test('update to pubrel', async (t) => { + const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 } @@ -1374,14 +1326,14 @@ function abstractPersistence (opts) { } instance.outgoingEnqueueCombi([sub], packet, err => { - assert.ifError(err) + t.assert.ifError(err) const updated = new Packet(packet) updated.messageId = 42 instance.outgoingUpdate(client, updated, (err, reclient, repacket) => { - assert.ifError(err) - assert.equal(reclient, client, 'client matches') - assert.equal(repacket, updated, 'packet matches') + t.assert.ifError(err) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') const pubrel = { cmd: 'pubrel', @@ -1389,20 +1341,21 @@ function abstractPersistence (opts) { } instance.outgoingUpdate(client, pubrel, err => { - assert.ifError(err) + t.assert.ifError(err) const stream = instance.outgoingStream(client) getArrayFromStream(stream).then(list => { - assert.deepEqual(list, [pubrel], 'must return the packet') - instance.destroy() + t.assert.deepEqual(list, [pubrel], 'must return the packet') + doCleanup(t, instance) }) }) }) }) }) - testInstance('add incoming packet, get it, and clear with messageId', (t, instance) => { + test('add incoming packet, get it, and clear with messageId', async (t) => { + const instance = await persistence(t) const client = { id: 'abcde' } @@ -1418,12 +1371,12 @@ function abstractPersistence (opts) { } instance.incomingStorePacket(client, packet, err => { - assert.ifError(err) + t.assert.ifError(err) instance.incomingGetPacket(client, { messageId: packet.messageId }, (err, retrieved) => { - assert.ifError(err) + t.assert.ifError(err) // adjusting the objects so they match delete retrieved.brokerCounter @@ -1433,23 +1386,24 @@ function abstractPersistence (opts) { const result = structuredClone(retrieved) // Convert Uint8 to Buffer for comparison result.payload = Buffer.from(result.payload) - assert.deepEqual(result, packet, 'retrieved packet must be deeply equal') - assert.notEqual(retrieved, packet, 'retrieved packet must not be the same object') + t.assert.deepEqual(result, packet, 'retrieved packet must be deeply equal') + t.assert.notEqual(retrieved, packet, 'retrieved packet must not be the same object') instance.incomingDelPacket(client, retrieved, err => { - assert.ifError(err) + t.assert.ifError(err) instance.incomingGetPacket(client, { messageId: packet.messageId }, (err, retrieved) => { - assert.ok(err, 'must error') - instance.destroy() + t.assert.ok(err, 'must error') + doCleanup(t, instance) }) }) }) }) }) - testInstance('store, fetch and delete will message', (t, instance) => { + test('store, fetch and delete will message', async (t) => { + const instance = await persistence(t) const client = { id: '12345' } @@ -1461,29 +1415,30 @@ function abstractPersistence (opts) { } instance.putWill(client, expected, (err, c) => { - assert.ifError(err, 'no error') - assert.equal(c, client, 'client matches') + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') instance.getWill(client, (err, packet, c) => { - assert.ifError(err, 'no error') - assert.deepEqual(packet, expected, 'will matches') - assert.equal(c, client, 'client matches') + t.assert.ifError(err, 'no error') + t.assert.deepEqual(packet, expected, 'will matches') + t.assert.equal(c, client, 'client matches') client.brokerId = packet.brokerId instance.delWill(client, (err, packet, c) => { - assert.ifError(err, 'no error') - assert.deepEqual(packet, expected, 'will matches') - assert.equal(c, client, 'client matches') + t.assert.ifError(err, 'no error') + t.assert.deepEqual(packet, expected, 'will matches') + t.assert.equal(c, client, 'client matches') instance.getWill(client, (err, packet, c) => { - assert.ifError(err, 'no error') - assert.ok(!packet, 'no will after del') - assert.equal(c, client, 'client matches') - instance.destroy() + t.assert.ifError(err, 'no error') + t.assert.ok(!packet, 'no will after del') + t.assert.equal(c, client, 'client matches') + doCleanup(t, instance) }) }) }) }) }) - testInstance('stream all will messages', (t, instance) => { + test('stream all will messages', async (t) => { + const instance = await persistence(t) const client = { id: '12345', brokerId: instance.broker.id @@ -1496,10 +1451,10 @@ function abstractPersistence (opts) { } instance.putWill(client, toWrite, (err, c) => { - assert.ifError(err, 'no error') - assert.equal(c, client, 'client matches') + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') streamForEach(instance.streamWill(), (chunk) => { - assert.deepEqual(chunk, { + t.assert.deepEqual(chunk, { clientId: client.id, brokerId: instance.broker.id, topic: 'hello/died', @@ -1508,14 +1463,15 @@ function abstractPersistence (opts) { retain: true }, 'packet matches') instance.delWill(client, (err, result, client) => { - assert.ifError(err, 'no error') - instance.destroy() + t.assert.ifError(err, 'no error') + doCleanup(t, instance) }) }) }) }) - testInstance('stream all will message for unknown brokers', (t, instance) => { + test('stream all will message for unknown brokers', async (t) => { + const instance = await persistence(t) const originalId = instance.broker.id const client = { id: '42', @@ -1539,16 +1495,16 @@ function abstractPersistence (opts) { } instance.putWill(client, toWrite1, (err, c) => { - assert.ifError(err, 'no error') - assert.equal(c, client, 'client matches') + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') instance.broker.id = 'anotherBroker' instance.putWill(anotherClient, toWrite2, (err, c) => { - assert.ifError(err, 'no error') - assert.equal(c, anotherClient, 'client matches') + t.assert.ifError(err, 'no error') + t.assert.equal(c, anotherClient, 'client matches') streamForEach(instance.streamWill({ anotherBroker: Date.now() }), (chunk) => { - assert.deepEqual(chunk, { + t.assert.deepEqual(chunk, { clientId: client.id, brokerId: originalId, topic: 'hello/died42', @@ -1557,15 +1513,16 @@ function abstractPersistence (opts) { retain: true }, 'packet matches') instance.delWill(client, (err, result, client) => { - assert.ifError(err, 'no error') - instance.destroy() + t.assert.ifError(err, 'no error') + doCleanup(t, instance) }) }) }) }) }) - testInstance('delete wills from dead brokers', (t, instance) => { + test('delete wills from dead brokers', async (t) => { + const instance = await persistence(t) const client = { id: '42' } @@ -1578,25 +1535,26 @@ function abstractPersistence (opts) { } instance.putWill(client, toWrite1, (err, c) => { - assert.ifError(err, 'no error') - assert.equal(c, client, 'client matches') + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') instance.broker.id = 'anotherBroker' client.brokerId = instance.broker.id instance.delWill(client, (err, result, client) => { - assert.ifError(err, 'no error') - instance.destroy() + t.assert.ifError(err, 'no error') + doCleanup(t, instance) }) }) }) - testInstance('do not error if unkown messageId in outoingClearMessageId', (t, instance) => { + test('do not error if unkown messageId in outoingClearMessageId', async (t) => { + const instance = await persistence(t) const client = { id: 'abc-123' } instance.outgoingClearMessageId(client, 42, err => { - assert.ifError(err) - instance.destroy() + t.assert.ifError(err) + doCleanup(t, instance) }) }) } diff --git a/abstract.js.orig b/abstract.js.orig new file mode 100644 index 0000000..c6937e7 --- /dev/null +++ b/abstract.js.orig @@ -0,0 +1,1562 @@ +const { Readable } = require('node:stream') +const { promisify } = require('node:util') +const Packet = require('aedes-packet') + +function waitForEvent (obj, resolveEvt) { + return new Promise((resolve, reject) => { + obj.once(resolveEvt, () => { + resolve() + }) + obj.once('error', reject) + }) +} + +function doCleanup (t, instance) { + instance.destroy(() => { + t.diagnostic('instance destroyed') + }) +} + +function abstractPersistence (opts) { + const test = opts.test + let _persistence = opts.persistence + const waitForReady = opts.waitForReady + + // requiring it here so it will not error for modules + // not using the default emitter + const buildEmitter = opts.buildEmitter || require('mqemitter') + + if (_persistence.length === 0) { + _persistence = function asyncify (cb) { + cb(null, opts.persistence()) + } + } + + const _asyncPersistence = promisify(_persistence) + async function persistence (t) { + const mq = buildEmitter() + const broker = { + id: 'broker-42', + mq, + publish: mq.emit.bind(mq), + subscribe: mq.on.bind(mq), + unsubscribe: mq.removeListener.bind(mq), + counter: 0 + } + + const instance = await _asyncPersistence() + if (instance) { + // Wait for ready event, if applicable, to ensure the persistence isn't + // destroyed while it's still being set up. + // https://github.com/mcollina/aedes-persistence-redis/issues/41 + if (waitForReady) { + await waitForEvent(instance, 'ready') + } + instance.broker = broker + t.diagnostic('instance created') + return instance + } + throw new Error('no instance') + } + + // legacy third party streams are typically not iterable + function iterableStream (stream) { + if (typeof stream[Symbol.asyncIterator] !== 'function') { + return new Readable({ objectMode: true }).wrap(stream) + } + return stream + } + // end of legacy third party streams support + + async function getArrayFromStream (stream) { + const list = [] + for await (const item of iterableStream(stream)) { + list.push(item) + } + return list + } + + async function streamForEach (stream, fn) { + for await (const item of iterableStream(stream)) { + await fn(item) + } + } + + function asyncStoreRetained (instance, packet) { + return new Promise((resolve, reject) => { + instance.storeRetained(packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + + async function storeRetained (instance, opts = {}) { + const packet = { + cmd: 'publish', + id: instance.broker.id, + topic: opts.topic || 'hello/world', + payload: opts.payload || Buffer.from('muahah'), + qos: 0, + retain: true + } + await asyncStoreRetained(instance, packet) + return packet + } + + async function matchRetainedWithPattern (t, pattern, opts) { + const instance = await persistence(t) + const packet = await storeRetained(instance, opts) + let stream + if (Array.isArray(pattern)) { + stream = instance.createRetainedStreamCombi(pattern) + } else { + stream = instance.createRetainedStream(pattern) + } + t.diagnostic('created stream') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [packet], 'must return the packet') + t.diagnostic('stream was ok') + doCleanup(t, instance) + } + + function testPacket (t, packet, expected) { + if (packet.messageId === null) packet.messageId = undefined + t.assert.equal(packet.messageId, undefined, 'should have an unassigned messageId in queue') + // deepLooseEqual? + t.assert.deepEqual(structuredClone(packet), expected, 'must return the packet') + } + + function deClassed (obj) { + return Object.assign({}, obj) + } + + // testing starts here + test('store and look up retained messages', async t => { + t.plan(1) + await matchRetainedWithPattern(t, 'hello/world') + }) + + test('look up retained messages with a # pattern', async t => { + t.plan(1) + await matchRetainedWithPattern(t, '#') + }) + + test('look up retained messages with a hello/world/# pattern', async t => { + t.plan(1) + await matchRetainedWithPattern(t, 'hello/world/#') + }) + + test('look up retained messages with a + pattern', async t => { + t.plan(1) + await matchRetainedWithPattern(t, 'hello/+') + }) + + test('look up retained messages with multiple patterns', async t => { + t.plan(1) + await matchRetainedWithPattern(t, ['hello/+', 'other/hello']) + }) + + test('store multiple retained messages in order', async (t) => { + t.plan(1000) + const instance = await persistence(t) + const totalMessages = 1000 + + const retained = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + retain: true + } + + for (let i = 0; i < totalMessages; i++) { + const packet = new Packet(retained, instance.broker) + await storeRetained(instance, packet) + t.assert.equal(packet.brokerCounter, i + 1, 'packet stored in order') + } + doCleanup(t, instance) + }) + + test('remove retained message', async (t) => { + const instance = await persistence(t) + await storeRetained(instance, {}) + await storeRetained(instance, { + payload: Buffer.alloc(0) + }) + const stream = instance.createRetainedStream('#') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [], 'must return an empty list') + doCleanup(t, instance) + }) + + test('storing twice a retained message should keep only the last', async (t) => { + const instance = await persistence(t) + await storeRetained(instance, {}) + const packet = await storeRetained(instance, { + payload: Buffer.from('ahah') + }) + const stream = instance.createRetainedStream('#') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [packet], 'must return the last packet') + doCleanup(t, instance) + }) + + test('Create a new packet while storing a retained message', async (t) => { + const instance = await persistence(t) + const packet = { + cmd: 'publish', + id: instance.broker.id, + topic: opts.topic || 'hello/world', + payload: opts.payload || Buffer.from('muahah'), + qos: 0, + retain: true + } + const newPacket = Object.assign({}, packet) + + await asyncStoreRetained(instance, packet) + // packet reference change to check if a new packet is stored always + packet.retain = false + const stream = instance.createRetainedStream('#') + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [newPacket], 'must return the last packet') + doCleanup(t, instance) + }) + + test('store and look up subscriptions by client', async (t) => { + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs = [{ + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + topic: 'matteo', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + topic: 'noqos', + qos: 0, + rh: 0, + rap: true, + nl: false + }] + + instance.addSubscriptions(client, subs, (err, reClient) => { + t.assert.equal(reClient, client, 'client must be the same') + t.assert.ok(!err, 'no error') + instance.subscriptionsByClient(client, (err, resubs, reReClient) => { + t.assert.equal(reReClient, client, 'client must be the same') + t.assert.ok(!err, 'no error') + t.assert.deepEqual(resubs, subs) + doCleanup(t, instance) + }) + }) + }) + + async function addSubscriptions (instance, client, subs) { + return new Promise((resolve, reject) => { + instance.addSubscriptions(client, subs, (err, reClient) => { + if (err) { + reject(err) + } else { + resolve(reClient) + } + }) + }) + } + + async function removeSubscriptions (instance, client, subs) { + return new Promise((resolve, reject) => { + instance.removeSubscriptions(client, subs, (err, reClient) => { + if (err) { + reject(err) + } else { + resolve(reClient) + } + }) + }) + } + + async function subscriptionsByClient (instance, client) { + return new Promise((resolve, reject) => { + instance.subscriptionsByClient(client, (err, resubs, reClient) => { + if (err) { + reject(err) + } else { + resolve({ resubs, reClient }) + } + }) + }) + } + + async function subscriptionsByTopic (instance, topic) { + return new Promise((resolve, reject) => { + instance.subscriptionsByTopic(topic, (err, resubs) => { + if (err) { + reject(err) + } else { + resolve(resubs) + } + }) + }) + } + + async function cleanSubscriptions (instance, client) { + return new Promise((resolve, reject) => { + instance.cleanSubscriptions(client, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + + async function countOffline (instance) { + return new Promise((resolve, reject) => { + instance.countOffline((err, subsCount, clientsCount) => { + if (err) { + reject(err) + } else { + resolve({ subsCount, clientsCount }) + } + }) + }) + } + + async function outgoingEnqueue (instance, sub, packet) { + return new Promise((resolve, reject) => { + instance.outgoingEnqueue(sub, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + + async function outgoingEnqueueCombi (instance, subs, packet) { + return new Promise((resolve, reject) => { + instance.outgoingEnqueueCombi(subs, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + + test('remove subscriptions by client', async (t) => { + t.plan(4) + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs = [{ + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + topic: 'matteo', + qos: 1, + rh: 0, + rap: true, + nl: false + }] + + const reclient1 = await addSubscriptions(instance, client, subs) + t.assert.equal(reclient1, client, 'client must be the same') + const reClient2 = await removeSubscriptions(instance, client, ['hello']) + t.assert.equal(reClient2, client, 'client must be the same') + const { resubs, reClient } = await subscriptionsByClient(instance, client) + t.assert.equal(reClient, client, 'client must be the same') + t.assert.deepEqual(resubs, [{ + topic: 'matteo', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + doCleanup(t, instance) + }) + + test('store and look up subscriptions by topic', async (t) => { + t.plan(2) + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs = [{ + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + topic: 'hello/#', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + topic: 'matteo', + qos: 1, + rh: 0, + rap: true, + nl: false + }] + + const reclient = await addSubscriptions(instance, client, subs) + t.assert.equal(reclient, client, 'client must be the same') + const resubs = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs, [{ + clientId: client.id, + topic: 'hello/#', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + clientId: client.id, + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + doCleanup(t, instance) + }) + + test('get client list after subscriptions', async (t) => { + t.plan(1) + const instance = await persistence(t) + const client1 = { id: 'abcde' } + const client2 = { id: 'efghi' } + const subs = [{ + topic: 'helloagain', + qos: 1 + }] + + await addSubscriptions(instance, client1, subs) + await addSubscriptions(instance, client2, subs) + const stream = instance.getClientList(subs[0].topic) + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [client1.id, client2.id]) + doCleanup(t, instance) + }) + + test('get client list after an unsubscribe', async (t) => { + t.plan(1) + const instance = await persistence(t) + const client1 = { id: 'abcde' } + const client2 = { id: 'efghi' } + const subs = [{ + topic: 'helloagain', + qos: 1 + }] + await addSubscriptions(instance, client1, subs) + await addSubscriptions(instance, client2, subs) + await removeSubscriptions(instance, client2, [subs[0].topic]) + const stream = instance.getClientList(subs[0].topic) + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [client1.id]) + doCleanup(t, instance) + }) + + test('get subscriptions list after an unsubscribe', async (t) => { + t.plan(1) + const instance = await persistence(t) + const client1 = { id: 'abcde' } + const client2 = { id: 'efghi' } + const subs = [{ + topic: 'helloagain', + qos: 1 + }] + await addSubscriptions(instance, client1, subs) + await addSubscriptions(instance, client2, subs) + await removeSubscriptions(instance, client2, [subs[0].topic]) + const clients = await subscriptionsByTopic(instance, subs[0].topic) + t.assert.deepEqual(clients[0].clientId, client1.id) + doCleanup(t, instance) + }) + + test('QoS 0 subscriptions, restored but not matched', async (t) => { + t.plan(2) + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs = [{ + topic: 'hello', + qos: 0, + rh: 0, + rap: true, + nl: false + }, { + topic: 'hello/#', + qos: 1, + rh: 0, + rap: true, + nl: false + }, { + topic: 'matteo', + qos: 1, + rh: 0, + rap: true, + nl: false + }] + + await addSubscriptions(instance, client, subs) + const { resubs } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs, subs) + const resubs2 = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs2, [{ + clientId: client.id, + topic: 'hello/#', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + doCleanup(t, instance) + }) + + test('clean subscriptions', async (t) => { + t.plan(4) + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs = [{ + topic: 'hello', + qos: 1 + }, { + topic: 'matteo', + qos: 1 + }] + + await addSubscriptions(instance, client, subs) + await cleanSubscriptions(instance, client) + const resubs = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs, [], 'no subscriptions') + const { resubs: resubs2 } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs2, null, 'no subscriptions') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 0, 'no subscriptions added') + t.assert.equal(clientsCount, 0, 'no clients added') + doCleanup(t, instance) + }) + + test('clean subscriptions with no active subscriptions', async (t) => { + t.plan(4) + const instance = await persistence(t) + const client = { id: 'abcde' } + + await cleanSubscriptions(instance, client) + const resubs = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs, [], 'no subscriptions') + const { resubs: resubs2 } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs2, null, 'no subscriptions') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 0, 'no subscriptions added') + t.assert.equal(clientsCount, 0, 'no clients added') + doCleanup(t, instance) + }) + + test('same topic, different QoS', async (t) => { + t.plan(5) + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs = [{ + topic: 'hello', + qos: 0, + rh: 0, + rap: true, + nl: false + }, { + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }] + + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const { resubs } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs, [{ + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + + const resubs2 = await subscriptionsByTopic(instance, 'hello') + t.assert.deepEqual(resubs2, [{ + clientId: 'abcde', + topic: 'hello', + qos: 1, + rh: 0, + rap: true, + nl: false + }]) + + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 1, 'one subscription added') + t.assert.equal(clientsCount, 1, 'one client added') + doCleanup(t, instance) + }) + + test('replace subscriptions', async (t) => { + t.plan(25) + const instance = await persistence(t) + const client = { id: 'abcde' } + const topic = 'hello' + const sub = { topic, rh: 0, rap: true, nl: false } + const subByTopic = { clientId: client.id, topic, rh: 0, rap: true, nl: false } + + async function check (qos) { + sub.qos = subByTopic.qos = qos + const reClient = await addSubscriptions(instance, client, [sub]) + t.assert.equal(reClient, client, 'client must be the same') + const { resubs } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(resubs, [sub]) + const subsForTopic = await subscriptionsByTopic(instance, topic) + t.assert.deepEqual(subsForTopic, qos === 0 ? [] : [subByTopic]) + const { subsCount, clientsCount } = await countOffline(instance) + if (qos === 0) { + t.assert.equal(subsCount, 0, 'no subscriptions added') + } else { + t.assert.equal(subsCount, 1, 'one subscription added') + } + t.assert.equal(clientsCount, 1, 'one client added') + } + + await check(0) + await check(1) + await check(2) + await check(1) + await check(0) + doCleanup(t, instance) + }) + + test('replace subscriptions in same call', async (t) => { + t.plan(5) + const instance = await persistence(t) + const client = { id: 'abcde' } + const topic = 'hello' + const subs = [ + { topic, qos: 0, rh: 0, rap: true, nl: false }, + { topic, qos: 1, rh: 0, rap: true, nl: false }, + { topic, qos: 2, rh: 0, rap: true, nl: false }, + { topic, qos: 1, rh: 0, rap: true, nl: false }, + { topic, qos: 0, rh: 0, rap: true, nl: false } + ] + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const { resubs: subsForClient } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(subsForClient, [{ topic, qos: 0, rh: 0, rap: true, nl: false }]) + const subsForTopic = await subscriptionsByTopic(instance, topic) + t.assert.deepEqual(subsForTopic, []) + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 0, 'no subscriptions added') + t.assert.equal(clientsCount, 1, 'one client added') + doCleanup(t, instance) + }) + + test('store and count subscriptions', async (t) => { + t.plan(11) + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs = [{ + topic: 'hello', + qos: 1 + }, { + topic: 'matteo', + qos: 1 + }, { + topic: 'noqos', + qos: 0 + }] + + const reclient = await addSubscriptions(instance, client, subs) + t.assert.equal(reclient, client, 'client must be the same') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, 2, 'two subscriptions added') + t.assert.equal(clientsCount, 1, 'one client added') + await removeSubscriptions(instance, client, ['hello']) + const { subsCount: subsCount2, clientsCount: clientsCount2 } = await countOffline(instance) + t.assert.equal(subsCount2, 1, 'one subscription added') + t.assert.equal(clientsCount2, 1, 'one client added') + await removeSubscriptions(instance, client, ['matteo']) + const { subsCount: subsCount3, clientsCount: clientsCount3 } = await countOffline(instance) + t.assert.equal(subsCount3, 0, 'zero subscriptions added') + t.assert.equal(clientsCount3, 1, 'one client added') + await removeSubscriptions(instance, client, ['noqos']) + const { subsCount: subsCount4, clientsCount: clientsCount4 } = await countOffline(instance) + t.assert.equal(subsCount4, 0, 'zero subscriptions added') + t.assert.equal(clientsCount4, 0, 'zero clients added') + await removeSubscriptions(instance, client, ['noqos']) + const { subsCount: subsCount5, clientsCount: clientsCount5 } = await countOffline(instance) + t.assert.equal(subsCount5, 0, 'zero subscriptions added') + t.assert.equal(clientsCount5, 0, 'zero clients added') + doCleanup(t, instance) + }) + + test('count subscriptions with two clients', async (t) => { + t.plan(26) + const instance = await persistence(t) + const client1 = { id: 'abcde' } + const client2 = { id: 'fghij' } + const subs = [{ + topic: 'hello', + qos: 1 + }, { + topic: 'matteo', + qos: 1 + }, { + topic: 'noqos', + qos: 0 + }] + + async function remove (client, subs, expectedSubs, expectedClients) { + const reClient = await removeSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const { subsCount, clientsCount } = await countOffline(instance) + t.assert.equal(subsCount, expectedSubs, 'subscriptions added') + t.assert.equal(clientsCount, expectedClients, 'clients added') + } + + const reClient1 = await addSubscriptions(instance, client1, subs) + t.assert.equal(reClient1, client1, 'client must be the same') + const reClient2 = await addSubscriptions(instance, client2, subs) + t.assert.equal(reClient2, client2, 'client must be the same') + await remove(client1, ['foobar'], 4, 2) + await remove(client1, ['hello'], 3, 2) + await remove(client1, ['hello'], 3, 2) + await remove(client1, ['matteo'], 2, 2) + await remove(client1, ['noqos'], 2, 1) + await remove(client2, ['hello'], 1, 1) + await remove(client2, ['matteo'], 0, 1) + await remove(client2, ['noqos'], 0, 0) + doCleanup(t, instance) + }) + + test('add duplicate subs to persistence for qos > 0', async (t) => { + t.plan(3) + const instance = await persistence(t) + const client = { id: 'abcde' } + const topic = 'hello' + const subs = [{ + topic, + qos: 1, + rh: 0, + rap: true, + nl: false + }] + + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const reClient2 = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient2, client, 'client must be the same') + subs[0].clientId = client.id + const subsForTopic = await subscriptionsByTopic(instance, topic) + t.assert.deepEqual(subsForTopic, subs) + doCleanup(t, instance) + }) + + test('add duplicate subs to persistence for qos 0', async (t) => { + t.plan(3) + const instance = await persistence(t) + const client = { id: 'abcde' } + const topic = 'hello' + const subs = [{ + topic, + qos: 0, + rh: 0, + rap: true, + nl: false + }] + + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const reClient2 = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient2, client, 'client must be the same') + const { resubs: subsForClient } = await subscriptionsByClient(instance, client) + t.assert.deepEqual(subsForClient, subs) + doCleanup(t, instance) + }) + + test('get topic list after concurrent subscriptions of a client', async (t) => { + t.plan(3) + const instance = await persistence(t) + const client = { id: 'abcde' } + const subs1 = [{ + topic: 'hello1', + qos: 1, + rh: 0, + rap: true, + nl: false + }] + const subs2 = [{ + topic: 'hello2', + qos: 1, + rh: 0, + rap: true, + nl: false + }] + let calls = 2 + + await new Promise((resolve, reject) => { + async function done () { + if (!--calls) { + const { resubs } = await subscriptionsByClient(instance, client) + resubs.sort((a, b) => a.topic.localeCompare(b.topic, 'en')) + t.assert.deepEqual(resubs, [subs1[0], subs2[0]]) + doCleanup(t, instance) + resolve() + } + } + + instance.addSubscriptions(client, subs1, err => { + t.assert.ok(!err, 'no error for hello1') + done() + }) + instance.addSubscriptions(client, subs2, err => { + t.assert.ok(!err, 'no error for hello2') + done() + }) + }) + }) + + test('add outgoing packet and stream it', async (t) => { + t.plan(2) + const instance = await persistence(t) + const sub = { + clientId: 'abcde', + topic: 'hello', + qos: 1 + } + const client = { + id: sub.clientId + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42 + } + const expected = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + retain: false, + dup: false, + brokerId: instance.broker.id, + brokerCounter: 42, + messageId: undefined + } + + await outgoingEnqueue(instance, sub, packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + doCleanup(t, instance) + }) + + test('add outgoing packet for multiple subs and stream to all', async (t) => { + t.plan(4) + const instance = await persistence(t) + const sub = { + clientId: 'abcde', + topic: 'hello', + qos: 1 + } + const sub2 = { + clientId: 'fghih', + topic: 'hello', + qos: 1 + } + const subs = [sub, sub2] + const client = { + id: sub.clientId + } + const client2 = { + id: sub2.clientId + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42 + } + const expected = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + retain: false, + dup: false, + brokerId: instance.broker.id, + brokerCounter: 42, + messageId: undefined + } + + await outgoingEnqueueCombi(instance, subs, packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + + const stream2 = instance.outgoingStream(client2) + const list2 = await getArrayFromStream(stream2) + testPacket(t, list2[0], expected) + doCleanup(t, instance) + }) + + test('add outgoing packet as a string and pump', async (t) => { + t.plan(7) + const instance = await persistence(t) + const sub = { + clientId: 'abcde', + topic: 'hello', + qos: 1 + } + const client = { + id: sub.clientId + } + const packet1 = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 10 + } + const packet2 = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('matteo'), + qos: 1, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 50 + } + const queue = [] + + const updated1 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet1, 42) + const updated2 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet2, 43) + const stream = instance.outgoingStream(client) + + async function clearQueue (data) { + const { repacket } = await outgoingUpdate(instance, client, data) + t.diagnostic('packet received') + queue.push(repacket) + } + + await streamForEach(stream, clearQueue) + t.assert.equal(queue.length, 2) + t.assert.deepEqual(deClassed(queue[0]), deClassed(updated1)) + t.assert.deepEqual(deClassed(queue[1]), deClassed(updated2)) + doCleanup(t, instance) + }) + + test('add outgoing packet as a string and stream', async (t) => { + t.plan(2) + const instance = await persistence(t) + const sub = { + clientId: 'abcde', + topic: 'hello', + qos: 1 + } + const client = { + id: sub.clientId + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: 'world', + qos: 1, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42 + } + const expected = { + cmd: 'publish', + topic: 'hello', + payload: 'world', + qos: 1, + retain: false, + dup: false, + brokerId: instance.broker.id, + brokerCounter: 42, + messageId: undefined + } + + await outgoingEnqueueCombi(instance, [sub], packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + doCleanup(t, instance) + }) + + test('add outgoing packet and stream it twice', async (t) => { + const instance = await persistence(t) + const sub = { + clientId: 'abcde', + topic: 'hello', + qos: 1 + } + const client = { + id: sub.clientId + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42, + messageId: 4242 + } + const expected = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + retain: false, + dup: false, + brokerId: instance.broker.id, + brokerCounter: 42, + messageId: undefined + } + + await outgoingEnqueueCombi(instance, [sub], packet) + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + testPacket(t, list[0], expected) + const stream2 = instance.outgoingStream(client) + const list2 = await getArrayFromStream(stream2) + testPacket(t, list2[0], expected) + t.assert.notEqual(packet, expected, 'packet must be a different object') + doCleanup(t, instance) + }) + + async function enqueueAndUpdate (t, instance, client, sub, packet, messageId) { + await outgoingEnqueueCombi(instance, [sub], packet) + const updated = new Packet(packet) + updated.messageId = messageId + + const { reclient, repacket } = await outgoingUpdate(instance, client, updated) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') + return repacket + } + + async function outgoingUpdate (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.outgoingUpdate(client, packet, (err, reclient, repacket) => { + if (err) { + reject(err) + } else { + resolve({ reclient, repacket }) + } + }) + }) + } + + async function asyncEnqueueAndUpdate (t, instance, client, sub, packet, messageId) { + await outgoingEnqueueCombi(instance, [sub], packet) + const updated = new Packet(packet) + updated.messageId = messageId + const { reclient, repacket } = await outgoingUpdate(instance, client, updated) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') + return updated + } + + test('add outgoing packet and update messageId', async (t) => { + t.plan(5) + const instance = await persistence(t) + const sub = { + clientId: 'abcde', topic: 'hello', qos: 1 + } + const client = { + id: sub.clientId + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42 + } + + const updated = await enqueueAndUpdate(t, instance, client, sub, packet, 42) + delete updated.messageId + const stream = instance.outgoingStream(client) + const list = await getArrayFromStream(stream) + delete list[0].messageId + t.assert.notEqual(list[0], updated, 'must not be the same object') + t.assert.deepEqual(deClassed(list[0]), deClassed(updated), 'must return the packet') + t.assert.equal(list.length, 1, 'must return only one packet') + doCleanup(t, instance) + }) + + test('add 2 outgoing packet and clear messageId', async (t) => { + const instance = await persistence(t) + const sub = { + clientId: 'abcde', topic: 'hello', qos: 1 + } + const client = { + id: sub.clientId + } + const packet1 = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42 + } + const packet2 = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('matteo'), + qos: 1, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 43 + } + + const updated1 = await enqueueAndUpdate(t, instance, client, sub, packet1, 42) + const updated2 = await enqueueAndUpdate(t, instance, client, sub, packet2, 43) + instance.outgoingClearMessageId(client, updated1, async (err, packet) => { + t.assert.ifError(err) + t.assert.deepEqual(packet.messageId, 42, 'must have the same messageId') + t.assert.deepEqual(packet.payload.toString(), packet1.payload.toString(), 'must have original payload') + t.assert.deepEqual(packet.topic, packet1.topic, 'must have original topic') + const stream = instance.outgoingStream(client) + delete updated2.messageId + const list = await getArrayFromStream(stream) + delete list[0].messageId + t.assert.notEqual(list[0], updated2, 'must not be the same object') + t.assert.deepEqual(deClassed(list[0]), deClassed(updated2), 'must return the packet') + t.assert.equal(list.length, 1, 'must return only one packet') + doCleanup(t, instance) + }) + }) + + test('add many outgoing packets and clear messageIds', async (t) => { + const instance = await persistence(t) + const sub = { + clientId: 'abcde', topic: 'hello', qos: 1 + } + const client = { + id: sub.clientId + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 1, + dup: false, + length: 14, + retain: false + } + + function outStream (instance, client) { + return iterableStream(instance.outgoingStream(client)) + } + + // we just need a stream to figure out the high watermark + const stream = outStream(instance, client) + const total = stream.readableHighWaterMark * 2 + + function submitMessage (id) { + return new Promise((resolve, reject) => { + const p = new Packet(packet, instance.broker) + p.messageId = id + instance.outgoingEnqueue(sub, p, (err) => { + if (err) { + return reject(err) + } + instance.outgoingUpdate(client, p, resolve) + }) + }) + } + + function clearMessage (p) { + return new Promise((resolve, reject) => { + instance.outgoingClearMessageId(client, p, (err, received) => { + t.assert.ifError(err) + t.assert.deepEqual(received, p, 'must return the packet') + resolve() + }) + }) + } + + for (let i = 0; i < total; i++) { + await submitMessage(i) + } + + let queued = 0 + for await (const p of outStream(instance, client)) { + if (p) { + queued++ + } + } + t.assert.equal(queued, total, `outgoing queue must hold ${total} items`) + + for await (const p of outStream(instance, client)) { + await clearMessage(p) + } + + let queued2 = 0 + for await (const p of outStream(instance, client)) { + if (p) { + queued2++ + } + } + t.assert.equal(queued2, 0, 'outgoing queue is empty') + doCleanup(t, instance) + }) + + test('update to publish w/ same messageId', async (t) => { + const instance = await persistence(t) + const sub = { + clientId: 'abcde', topic: 'hello', qos: 1 + } + const client = { + id: sub.clientId + } + const packet1 = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 2, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42, + messageId: 42 + } + const packet2 = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 2, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 50, + messageId: 42 + } + + instance.outgoingEnqueue(sub, packet1, () => { + instance.outgoingEnqueue(sub, packet2, () => { + instance.outgoingUpdate(client, packet1, () => { + instance.outgoingUpdate(client, packet2, () => { + const stream = instance.outgoingStream(client) + getArrayFromStream(stream).then(list => { + t.assert.equal(list.length, 2, 'must have two items in queue') + t.assert.equal(list[0].brokerCounter, packet1.brokerCounter, 'brokerCounter must match') + t.assert.equal(list[0].messageId, packet1.messageId, 'messageId must match') + t.assert.equal(list[1].brokerCounter, packet2.brokerCounter, 'brokerCounter must match') + t.assert.equal(list[1].messageId, packet2.messageId, 'messageId must match') + doCleanup(t, instance) + }) + }) + }) + }) + }) + }) + + test('update to pubrel', async (t) => { + const instance = await persistence(t) + const sub = { + clientId: 'abcde', topic: 'hello', qos: 1 + } + const client = { + id: sub.clientId + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 2, + dup: false, + length: 14, + retain: false, + brokerId: instance.broker.id, + brokerCounter: 42 + } + + instance.outgoingEnqueueCombi([sub], packet, err => { + t.assert.ifError(err) + const updated = new Packet(packet) + updated.messageId = 42 + + instance.outgoingUpdate(client, updated, (err, reclient, repacket) => { + t.assert.ifError(err) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') + + const pubrel = { + cmd: 'pubrel', + messageId: updated.messageId + } + + instance.outgoingUpdate(client, pubrel, err => { + t.assert.ifError(err) + + const stream = instance.outgoingStream(client) + + getArrayFromStream(stream).then(list => { + t.assert.deepEqual(list, [pubrel], 'must return the packet') + doCleanup(t, instance) + }) + }) + }) + }) + }) + + test('add incoming packet, get it, and clear with messageId', async (t) => { + const instance = await persistence(t) + const client = { + id: 'abcde' + } + const packet = { + cmd: 'publish', + topic: 'hello', + payload: Buffer.from('world'), + qos: 2, + dup: false, + length: 14, + retain: false, + messageId: 42 + } + + instance.incomingStorePacket(client, packet, err => { + t.assert.ifError(err) + + instance.incomingGetPacket(client, { + messageId: packet.messageId + }, (err, retrieved) => { + t.assert.ifError(err) + + // adjusting the objects so they match + delete retrieved.brokerCounter + delete retrieved.brokerId + delete packet.length + // strip the class identifier from the packet + const result = structuredClone(retrieved) + // Convert Uint8 to Buffer for comparison + result.payload = Buffer.from(result.payload) + t.assert.deepEqual(result, packet, 'retrieved packet must be deeply equal') + t.assert.notEqual(retrieved, packet, 'retrieved packet must not be the same object') + + instance.incomingDelPacket(client, retrieved, err => { + t.assert.ifError(err) + instance.incomingGetPacket(client, { + messageId: packet.messageId + }, (err, retrieved) => { + t.assert.ok(err, 'must error') + doCleanup(t, instance) + }) + }) + }) + }) + }) + + test('store, fetch and delete will message', async (t) => { + const instance = await persistence(t) + const client = { + id: '12345' + } + const expected = { + topic: 'hello/died', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + } + + instance.putWill(client, expected, (err, c) => { + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') + instance.getWill(client, (err, packet, c) => { + t.assert.ifError(err, 'no error') + t.assert.deepEqual(packet, expected, 'will matches') + t.assert.equal(c, client, 'client matches') + client.brokerId = packet.brokerId + instance.delWill(client, (err, packet, c) => { + t.assert.ifError(err, 'no error') + t.assert.deepEqual(packet, expected, 'will matches') + t.assert.equal(c, client, 'client matches') + instance.getWill(client, (err, packet, c) => { + t.assert.ifError(err, 'no error') + t.assert.ok(!packet, 'no will after del') + t.assert.equal(c, client, 'client matches') + doCleanup(t, instance) + }) + }) + }) + }) + }) + + test('stream all will messages', async (t) => { + const instance = await persistence(t) + const client = { + id: '12345', + brokerId: instance.broker.id + } + const toWrite = { + topic: 'hello/died', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + } + + instance.putWill(client, toWrite, (err, c) => { + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') + streamForEach(instance.streamWill(), (chunk) => { + t.assert.deepEqual(chunk, { + clientId: client.id, + brokerId: instance.broker.id, + topic: 'hello/died', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + }, 'packet matches') + instance.delWill(client, (err, result, client) => { + t.assert.ifError(err, 'no error') + doCleanup(t, instance) + }) + }) + }) + }) + + test('stream all will message for unknown brokers', async (t) => { + const instance = await persistence(t) + const originalId = instance.broker.id + const client = { + id: '42', + brokerId: instance.broker.id + } + const anotherClient = { + id: '24', + brokerId: instance.broker.id + } + const toWrite1 = { + topic: 'hello/died42', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + } + const toWrite2 = { + topic: 'hello/died24', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + } + + instance.putWill(client, toWrite1, (err, c) => { + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') + instance.broker.id = 'anotherBroker' + instance.putWill(anotherClient, toWrite2, (err, c) => { + t.assert.ifError(err, 'no error') + t.assert.equal(c, anotherClient, 'client matches') + streamForEach(instance.streamWill({ + anotherBroker: Date.now() + }), (chunk) => { + t.assert.deepEqual(chunk, { + clientId: client.id, + brokerId: originalId, + topic: 'hello/died42', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + }, 'packet matches') + instance.delWill(client, (err, result, client) => { + t.assert.ifError(err, 'no error') + doCleanup(t, instance) + }) + }) + }) + }) + }) + + test('delete wills from dead brokers', async (t) => { + const instance = await persistence(t) + const client = { + id: '42' + } + + const toWrite1 = { + topic: 'hello/died42', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + } + + instance.putWill(client, toWrite1, (err, c) => { + t.assert.ifError(err, 'no error') + t.assert.equal(c, client, 'client matches') + instance.broker.id = 'anotherBroker' + client.brokerId = instance.broker.id + instance.delWill(client, (err, result, client) => { + t.assert.ifError(err, 'no error') + doCleanup(t, instance) + }) + }) + }) + + test('do not error if unkown messageId in outoingClearMessageId', async (t) => { + const instance = await persistence(t) + const client = { + id: 'abc-123' + } + + instance.outgoingClearMessageId(client, 42, err => { + t.assert.ifError(err) + doCleanup(t, instance) + }) + }) +} + +module.exports = abstractPersistence From 9882a54fa36bf25c5e354adcc14f43be416e000b Mon Sep 17 00:00:00 2001 From: Hans Klunder Date: Tue, 11 Mar 2025 07:56:38 +0100 Subject: [PATCH 2/5] rewrite of abstract.js --- abstract.js | 931 ++++++++++++++++++++++++++-------------------------- test.js | 25 +- 2 files changed, 497 insertions(+), 459 deletions(-) diff --git a/abstract.js b/abstract.js index c6937e7..bcf49b6 100644 --- a/abstract.js +++ b/abstract.js @@ -2,6 +2,213 @@ const { Readable } = require('node:stream') const { promisify } = require('node:util') const Packet = require('aedes-packet') +// promisified versions of the instance methods +// to avoid deep callbacks while testing +function storeRetained (instance, packet) { + return new Promise((resolve, reject) => { + instance.storeRetained(packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +async function addSubscriptions (instance, client, subs) { + return new Promise((resolve, reject) => { + instance.addSubscriptions(client, subs, (err, reClient) => { + if (err) { + reject(err) + } else { + resolve(reClient) + } + }) + }) +} + +async function removeSubscriptions (instance, client, subs) { + return new Promise((resolve, reject) => { + instance.removeSubscriptions(client, subs, (err, reClient) => { + if (err) { + reject(err) + } else { + resolve(reClient) + } + }) + }) +} + +async function subscriptionsByClient (instance, client) { + return new Promise((resolve, reject) => { + instance.subscriptionsByClient(client, (err, resubs, reClient) => { + if (err) { + reject(err) + } else { + resolve({ resubs, reClient }) + } + }) + }) +} + +async function subscriptionsByTopic (instance, topic) { + return new Promise((resolve, reject) => { + instance.subscriptionsByTopic(topic, (err, resubs) => { + if (err) { + reject(err) + } else { + resolve(resubs) + } + }) + }) +} + +async function cleanSubscriptions (instance, client) { + return new Promise((resolve, reject) => { + instance.cleanSubscriptions(client, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +async function countOffline (instance) { + return new Promise((resolve, reject) => { + instance.countOffline((err, subsCount, clientsCount) => { + if (err) { + reject(err) + } else { + resolve({ subsCount, clientsCount }) + } + }) + }) +} + +async function outgoingEnqueue (instance, sub, packet) { + return new Promise((resolve, reject) => { + instance.outgoingEnqueue(sub, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +async function outgoingEnqueueCombi (instance, subs, packet) { + return new Promise((resolve, reject) => { + instance.outgoingEnqueueCombi(subs, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +async function outgoingClearMessageId (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.outgoingClearMessageId(client, packet, (err, repacket) => { + if (err) { + reject(err) + } else { + resolve(repacket) + } + }) + }) +} + +async function outgoingUpdate (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.outgoingUpdate(client, packet, (err, reclient, repacket) => { + if (err) { + reject(err) + } else { + resolve({ reclient, repacket }) + } + }) + }) +} + +async function incomingStorePacket (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.incomingStorePacket(client, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} +async function incomingGetPacket (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.incomingGetPacket(client, packet, (err, retrieved) => { + if (err) { + reject(err) + } else { + resolve(retrieved) + } + }) + }) +} + +async function incomingDelPacket (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.incomingDelPacket(client, packet, err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +async function putWill (instance, client, packet) { + return new Promise((resolve, reject) => { + instance.putWill(client, packet, (err, reClient) => { + if (err) { + reject(err) + } else { + resolve(reClient) + } + }) + }) +} + +async function getWill (instance, client) { + return new Promise((resolve, reject) => { + instance.getWill(client, (err, packet, reClient) => { + if (err) { + reject(err) + } else { + resolve({ packet, reClient }) + } + }) + }) +} + +async function delWill (instance, client) { + return new Promise((resolve, reject) => { + instance.delWill(client, (err, packet, reClient) => { + if (err) { + reject(err) + } else { + resolve({ packet, reClient }) + } + }) + }) +} +// end of promisified versions of instance methods + +// helper functions function waitForEvent (obj, resolveEvt) { return new Promise((resolve, reject) => { obj.once(resolveEvt, () => { @@ -11,28 +218,78 @@ function waitForEvent (obj, resolveEvt) { }) } -function doCleanup (t, instance) { - instance.destroy(() => { - t.diagnostic('instance destroyed') - }) +async function doCleanup (t, instance) { + const instanceDestroy = promisify(instance.destroy).bind(instance) + await instanceDestroy() + t.diagnostic('instance cleaned up') +} + +// legacy third party streams are typically not iterable +function iterableStream (stream) { + if (typeof stream[Symbol.asyncIterator] !== 'function') { + return new Readable({ objectMode: true }).wrap(stream) + } + return stream +} +// end of legacy third party streams support + +function outgoingStream (instance, client) { + return iterableStream(instance.outgoingStream(client)) +} + +async function getArrayFromStream (stream) { + const list = [] + for await (const item of iterableStream(stream)) { + list.push(item) + } + return list +} + +async function storeRetainedPacket (instance, opts = {}) { + const packet = { + cmd: 'publish', + id: instance.broker.id, + topic: opts.topic || 'hello/world', + payload: opts.payload || Buffer.from('muahah'), + qos: 0, + retain: true + } + await storeRetained(instance, packet) + return packet +} + +async function enqueueAndUpdate (t, instance, client, sub, packet, messageId) { + await outgoingEnqueueCombi(instance, [sub], packet) + const updated = new Packet(packet) + updated.messageId = messageId + + const { reclient, repacket } = await outgoingUpdate(instance, client, updated) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') + return repacket } +function testPacket (t, packet, expected) { + if (packet.messageId === null) packet.messageId = undefined + t.assert.equal(packet.messageId, undefined, 'should have an unassigned messageId in queue') + // deepLooseEqual? + t.assert.deepEqual(structuredClone(packet), expected, 'must return the packet') +} + +function deClassed (obj) { + return Object.assign({}, obj) +} + +// start of abstractPersistence function abstractPersistence (opts) { const test = opts.test - let _persistence = opts.persistence + const _persistence = opts.persistence const waitForReady = opts.waitForReady // requiring it here so it will not error for modules // not using the default emitter const buildEmitter = opts.buildEmitter || require('mqemitter') - if (_persistence.length === 0) { - _persistence = function asyncify (cb) { - cb(null, opts.persistence()) - } - } - - const _asyncPersistence = promisify(_persistence) async function persistence (t) { const mq = buildEmitter() const broker = { @@ -44,7 +301,7 @@ function abstractPersistence (opts) { counter: 0 } - const instance = await _asyncPersistence() + const instance = await _persistence() if (instance) { // Wait for ready event, if applicable, to ensure the persistence isn't // destroyed while it's still being set up. @@ -59,57 +316,9 @@ function abstractPersistence (opts) { throw new Error('no instance') } - // legacy third party streams are typically not iterable - function iterableStream (stream) { - if (typeof stream[Symbol.asyncIterator] !== 'function') { - return new Readable({ objectMode: true }).wrap(stream) - } - return stream - } - // end of legacy third party streams support - - async function getArrayFromStream (stream) { - const list = [] - for await (const item of iterableStream(stream)) { - list.push(item) - } - return list - } - - async function streamForEach (stream, fn) { - for await (const item of iterableStream(stream)) { - await fn(item) - } - } - - function asyncStoreRetained (instance, packet) { - return new Promise((resolve, reject) => { - instance.storeRetained(packet, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - - async function storeRetained (instance, opts = {}) { - const packet = { - cmd: 'publish', - id: instance.broker.id, - topic: opts.topic || 'hello/world', - payload: opts.payload || Buffer.from('muahah'), - qos: 0, - retain: true - } - await asyncStoreRetained(instance, packet) - return packet - } - - async function matchRetainedWithPattern (t, pattern, opts) { + async function matchRetainedWithPattern (t, pattern) { const instance = await persistence(t) - const packet = await storeRetained(instance, opts) + const packet = await storeRetainedPacket(instance) let stream if (Array.isArray(pattern)) { stream = instance.createRetainedStreamCombi(pattern) @@ -120,18 +329,7 @@ function abstractPersistence (opts) { const list = await getArrayFromStream(stream) t.assert.deepEqual(list, [packet], 'must return the packet') t.diagnostic('stream was ok') - doCleanup(t, instance) - } - - function testPacket (t, packet, expected) { - if (packet.messageId === null) packet.messageId = undefined - t.assert.equal(packet.messageId, undefined, 'should have an unassigned messageId in queue') - // deepLooseEqual? - t.assert.deepEqual(structuredClone(packet), expected, 'must return the packet') - } - - function deClassed (obj) { - return Object.assign({}, obj) + await doCleanup(t, instance) } // testing starts here @@ -175,37 +373,40 @@ function abstractPersistence (opts) { for (let i = 0; i < totalMessages; i++) { const packet = new Packet(retained, instance.broker) - await storeRetained(instance, packet) + await storeRetainedPacket(instance, packet) t.assert.equal(packet.brokerCounter, i + 1, 'packet stored in order') } - doCleanup(t, instance) + await doCleanup(t, instance) }) test('remove retained message', async (t) => { + t.plan(1) const instance = await persistence(t) - await storeRetained(instance, {}) - await storeRetained(instance, { + await storeRetainedPacket(instance, {}) + await storeRetainedPacket(instance, { payload: Buffer.alloc(0) }) const stream = instance.createRetainedStream('#') const list = await getArrayFromStream(stream) t.assert.deepEqual(list, [], 'must return an empty list') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('storing twice a retained message should keep only the last', async (t) => { + t.plan(1) const instance = await persistence(t) - await storeRetained(instance, {}) - const packet = await storeRetained(instance, { + await storeRetainedPacket(instance, {}) + const packet = await storeRetainedPacket(instance, { payload: Buffer.from('ahah') }) const stream = instance.createRetainedStream('#') const list = await getArrayFromStream(stream) t.assert.deepEqual(list, [packet], 'must return the last packet') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('Create a new packet while storing a retained message', async (t) => { + t.plan(1) const instance = await persistence(t) const packet = { cmd: 'publish', @@ -217,16 +418,17 @@ function abstractPersistence (opts) { } const newPacket = Object.assign({}, packet) - await asyncStoreRetained(instance, packet) + await storeRetained(instance, packet) // packet reference change to check if a new packet is stored always packet.retain = false const stream = instance.createRetainedStream('#') const list = await getArrayFromStream(stream) t.assert.deepEqual(list, [newPacket], 'must return the last packet') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('store and look up subscriptions by client', async (t) => { + t.plan(3) const instance = await persistence(t) const client = { id: 'abcde' } const subs = [{ @@ -249,114 +451,14 @@ function abstractPersistence (opts) { nl: false }] - instance.addSubscriptions(client, subs, (err, reClient) => { - t.assert.equal(reClient, client, 'client must be the same') - t.assert.ok(!err, 'no error') - instance.subscriptionsByClient(client, (err, resubs, reReClient) => { - t.assert.equal(reReClient, client, 'client must be the same') - t.assert.ok(!err, 'no error') - t.assert.deepEqual(resubs, subs) - doCleanup(t, instance) - }) - }) + const reClient = await addSubscriptions(instance, client, subs) + t.assert.equal(reClient, client, 'client must be the same') + const { resubs, reClient: reClient2 } = await subscriptionsByClient(instance, client) + t.assert.equal(reClient2, client, 'client must be the same') + t.assert.deepEqual(resubs, subs) + await doCleanup(t, instance) }) - async function addSubscriptions (instance, client, subs) { - return new Promise((resolve, reject) => { - instance.addSubscriptions(client, subs, (err, reClient) => { - if (err) { - reject(err) - } else { - resolve(reClient) - } - }) - }) - } - - async function removeSubscriptions (instance, client, subs) { - return new Promise((resolve, reject) => { - instance.removeSubscriptions(client, subs, (err, reClient) => { - if (err) { - reject(err) - } else { - resolve(reClient) - } - }) - }) - } - - async function subscriptionsByClient (instance, client) { - return new Promise((resolve, reject) => { - instance.subscriptionsByClient(client, (err, resubs, reClient) => { - if (err) { - reject(err) - } else { - resolve({ resubs, reClient }) - } - }) - }) - } - - async function subscriptionsByTopic (instance, topic) { - return new Promise((resolve, reject) => { - instance.subscriptionsByTopic(topic, (err, resubs) => { - if (err) { - reject(err) - } else { - resolve(resubs) - } - }) - }) - } - - async function cleanSubscriptions (instance, client) { - return new Promise((resolve, reject) => { - instance.cleanSubscriptions(client, (err) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - - async function countOffline (instance) { - return new Promise((resolve, reject) => { - instance.countOffline((err, subsCount, clientsCount) => { - if (err) { - reject(err) - } else { - resolve({ subsCount, clientsCount }) - } - }) - }) - } - - async function outgoingEnqueue (instance, sub, packet) { - return new Promise((resolve, reject) => { - instance.outgoingEnqueue(sub, packet, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - - async function outgoingEnqueueCombi (instance, subs, packet) { - return new Promise((resolve, reject) => { - instance.outgoingEnqueueCombi(subs, packet, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - test('remove subscriptions by client', async (t) => { t.plan(4) const instance = await persistence(t) @@ -388,7 +490,7 @@ function abstractPersistence (opts) { rap: true, nl: false }]) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('store and look up subscriptions by topic', async (t) => { @@ -433,7 +535,7 @@ function abstractPersistence (opts) { rap: true, nl: false }]) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('get client list after subscriptions', async (t) => { @@ -451,7 +553,7 @@ function abstractPersistence (opts) { const stream = instance.getClientList(subs[0].topic) const list = await getArrayFromStream(stream) t.assert.deepEqual(list, [client1.id, client2.id]) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('get client list after an unsubscribe', async (t) => { @@ -469,7 +571,7 @@ function abstractPersistence (opts) { const stream = instance.getClientList(subs[0].topic) const list = await getArrayFromStream(stream) t.assert.deepEqual(list, [client1.id]) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('get subscriptions list after an unsubscribe', async (t) => { @@ -486,7 +588,7 @@ function abstractPersistence (opts) { await removeSubscriptions(instance, client2, [subs[0].topic]) const clients = await subscriptionsByTopic(instance, subs[0].topic) t.assert.deepEqual(clients[0].clientId, client1.id) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('QoS 0 subscriptions, restored but not matched', async (t) => { @@ -525,7 +627,7 @@ function abstractPersistence (opts) { rap: true, nl: false }]) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('clean subscriptions', async (t) => { @@ -549,7 +651,7 @@ function abstractPersistence (opts) { const { subsCount, clientsCount } = await countOffline(instance) t.assert.equal(subsCount, 0, 'no subscriptions added') t.assert.equal(clientsCount, 0, 'no clients added') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('clean subscriptions with no active subscriptions', async (t) => { @@ -565,7 +667,7 @@ function abstractPersistence (opts) { const { subsCount, clientsCount } = await countOffline(instance) t.assert.equal(subsCount, 0, 'no subscriptions added') t.assert.equal(clientsCount, 0, 'no clients added') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('same topic, different QoS', async (t) => { @@ -610,7 +712,7 @@ function abstractPersistence (opts) { const { subsCount, clientsCount } = await countOffline(instance) t.assert.equal(subsCount, 1, 'one subscription added') t.assert.equal(clientsCount, 1, 'one client added') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('replace subscriptions', async (t) => { @@ -643,7 +745,7 @@ function abstractPersistence (opts) { await check(2) await check(1) await check(0) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('replace subscriptions in same call', async (t) => { @@ -667,7 +769,7 @@ function abstractPersistence (opts) { const { subsCount, clientsCount } = await countOffline(instance) t.assert.equal(subsCount, 0, 'no subscriptions added') t.assert.equal(clientsCount, 1, 'one client added') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('store and count subscriptions', async (t) => { @@ -706,7 +808,7 @@ function abstractPersistence (opts) { const { subsCount: subsCount5, clientsCount: clientsCount5 } = await countOffline(instance) t.assert.equal(subsCount5, 0, 'zero subscriptions added') t.assert.equal(clientsCount5, 0, 'zero clients added') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('count subscriptions with two clients', async (t) => { @@ -745,7 +847,7 @@ function abstractPersistence (opts) { await remove(client2, ['hello'], 1, 1) await remove(client2, ['matteo'], 0, 1) await remove(client2, ['noqos'], 0, 0) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('add duplicate subs to persistence for qos > 0', async (t) => { @@ -768,7 +870,7 @@ function abstractPersistence (opts) { subs[0].clientId = client.id const subsForTopic = await subscriptionsByTopic(instance, topic) t.assert.deepEqual(subsForTopic, subs) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('add duplicate subs to persistence for qos 0', async (t) => { @@ -790,7 +892,7 @@ function abstractPersistence (opts) { t.assert.equal(reClient2, client, 'client must be the same') const { resubs: subsForClient } = await subscriptionsByClient(instance, client) t.assert.deepEqual(subsForClient, subs) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('get topic list after concurrent subscriptions of a client', async (t) => { @@ -819,7 +921,7 @@ function abstractPersistence (opts) { const { resubs } = await subscriptionsByClient(instance, client) resubs.sort((a, b) => a.topic.localeCompare(b.topic, 'en')) t.assert.deepEqual(resubs, [subs1[0], subs2[0]]) - doCleanup(t, instance) + await doCleanup(t, instance) resolve() } } @@ -870,10 +972,10 @@ function abstractPersistence (opts) { } await outgoingEnqueue(instance, sub, packet) - const stream = instance.outgoingStream(client) + const stream = outgoingStream(instance, client) const list = await getArrayFromStream(stream) testPacket(t, list[0], expected) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('add outgoing packet for multiple subs and stream to all', async (t) => { @@ -920,14 +1022,14 @@ function abstractPersistence (opts) { } await outgoingEnqueueCombi(instance, subs, packet) - const stream = instance.outgoingStream(client) + const stream = outgoingStream(instance, client) const list = await getArrayFromStream(stream) testPacket(t, list[0], expected) - const stream2 = instance.outgoingStream(client2) + const stream2 = outgoingStream(instance, client2) const list2 = await getArrayFromStream(stream2) testPacket(t, list2[0], expected) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('add outgoing packet as a string and pump', async (t) => { @@ -961,9 +1063,9 @@ function abstractPersistence (opts) { } const queue = [] - const updated1 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet1, 42) - const updated2 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet2, 43) - const stream = instance.outgoingStream(client) + const updated1 = await enqueueAndUpdate(t, instance, client, sub, packet1, 42) + const updated2 = await enqueueAndUpdate(t, instance, client, sub, packet2, 43) + const stream = outgoingStream(instance, client) async function clearQueue (data) { const { repacket } = await outgoingUpdate(instance, client, data) @@ -971,11 +1073,14 @@ function abstractPersistence (opts) { queue.push(repacket) } - await streamForEach(stream, clearQueue) + const list = await getArrayFromStream(stream) + for (const data of list) { + await clearQueue(data) + } t.assert.equal(queue.length, 2) t.assert.deepEqual(deClassed(queue[0]), deClassed(updated1)) t.assert.deepEqual(deClassed(queue[1]), deClassed(updated2)) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('add outgoing packet as a string and stream', async (t) => { @@ -1013,13 +1118,14 @@ function abstractPersistence (opts) { } await outgoingEnqueueCombi(instance, [sub], packet) - const stream = instance.outgoingStream(client) + const stream = outgoingStream(instance, client) const list = await getArrayFromStream(stream) testPacket(t, list[0], expected) - doCleanup(t, instance) + await doCleanup(t, instance) }) test('add outgoing packet and stream it twice', async (t) => { + t.plan(5) const instance = await persistence(t) const sub = { clientId: 'abcde', @@ -1054,49 +1160,16 @@ function abstractPersistence (opts) { } await outgoingEnqueueCombi(instance, [sub], packet) - const stream = instance.outgoingStream(client) + const stream = outgoingStream(instance, client) const list = await getArrayFromStream(stream) testPacket(t, list[0], expected) - const stream2 = instance.outgoingStream(client) + const stream2 = outgoingStream(instance, client) const list2 = await getArrayFromStream(stream2) testPacket(t, list2[0], expected) t.assert.notEqual(packet, expected, 'packet must be a different object') - doCleanup(t, instance) + await doCleanup(t, instance) }) - async function enqueueAndUpdate (t, instance, client, sub, packet, messageId) { - await outgoingEnqueueCombi(instance, [sub], packet) - const updated = new Packet(packet) - updated.messageId = messageId - - const { reclient, repacket } = await outgoingUpdate(instance, client, updated) - t.assert.equal(reclient, client, 'client matches') - t.assert.equal(repacket, updated, 'packet matches') - return repacket - } - - async function outgoingUpdate (instance, client, packet) { - return new Promise((resolve, reject) => { - instance.outgoingUpdate(client, packet, (err, reclient, repacket) => { - if (err) { - reject(err) - } else { - resolve({ reclient, repacket }) - } - }) - }) - } - - async function asyncEnqueueAndUpdate (t, instance, client, sub, packet, messageId) { - await outgoingEnqueueCombi(instance, [sub], packet) - const updated = new Packet(packet) - updated.messageId = messageId - const { reclient, repacket } = await outgoingUpdate(instance, client, updated) - t.assert.equal(reclient, client, 'client matches') - t.assert.equal(repacket, updated, 'packet matches') - return updated - } - test('add outgoing packet and update messageId', async (t) => { t.plan(5) const instance = await persistence(t) @@ -1119,17 +1192,18 @@ function abstractPersistence (opts) { } const updated = await enqueueAndUpdate(t, instance, client, sub, packet, 42) - delete updated.messageId - const stream = instance.outgoingStream(client) + updated.messageId = undefined + const stream = outgoingStream(instance, client) const list = await getArrayFromStream(stream) - delete list[0].messageId + list[0].messageId = undefined t.assert.notEqual(list[0], updated, 'must not be the same object') t.assert.deepEqual(deClassed(list[0]), deClassed(updated), 'must return the packet') t.assert.equal(list.length, 1, 'must return only one packet') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('add 2 outgoing packet and clear messageId', async (t) => { + t.plan(10) const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 @@ -1162,23 +1236,22 @@ function abstractPersistence (opts) { const updated1 = await enqueueAndUpdate(t, instance, client, sub, packet1, 42) const updated2 = await enqueueAndUpdate(t, instance, client, sub, packet2, 43) - instance.outgoingClearMessageId(client, updated1, async (err, packet) => { - t.assert.ifError(err) - t.assert.deepEqual(packet.messageId, 42, 'must have the same messageId') - t.assert.deepEqual(packet.payload.toString(), packet1.payload.toString(), 'must have original payload') - t.assert.deepEqual(packet.topic, packet1.topic, 'must have original topic') - const stream = instance.outgoingStream(client) - delete updated2.messageId - const list = await getArrayFromStream(stream) - delete list[0].messageId - t.assert.notEqual(list[0], updated2, 'must not be the same object') - t.assert.deepEqual(deClassed(list[0]), deClassed(updated2), 'must return the packet') - t.assert.equal(list.length, 1, 'must return only one packet') - doCleanup(t, instance) - }) + const pkt = await outgoingClearMessageId(instance, client, updated1) + t.assert.deepEqual(pkt.messageId, 42, 'must have the same messageId') + t.assert.deepEqual(pkt.payload.toString(), packet1.payload.toString(), 'must have original payload') + t.assert.deepEqual(pkt.topic, packet1.topic, 'must have original topic') + const stream = outgoingStream(instance, client) + updated2.messageId = undefined + const list = await getArrayFromStream(stream) + list[0].messageId = undefined + t.assert.notEqual(list[0], updated2, 'must not be the same object') + t.assert.deepEqual(deClassed(list[0]), deClassed(updated2), 'must return the packet') + t.assert.equal(list.length, 1, 'must return only one packet') + await doCleanup(t, instance) }) test('add many outgoing packets and clear messageIds', async (t) => { + // t.plan() is called below after we know the high watermark const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 @@ -1196,64 +1269,43 @@ function abstractPersistence (opts) { retain: false } - function outStream (instance, client) { - return iterableStream(instance.outgoingStream(client)) - } - // we just need a stream to figure out the high watermark - const stream = outStream(instance, client) + const stream = (outgoingStream(instance, client)) const total = stream.readableHighWaterMark * 2 - - function submitMessage (id) { - return new Promise((resolve, reject) => { - const p = new Packet(packet, instance.broker) - p.messageId = id - instance.outgoingEnqueue(sub, p, (err) => { - if (err) { - return reject(err) - } - instance.outgoingUpdate(client, p, resolve) - }) - }) - } - - function clearMessage (p) { - return new Promise((resolve, reject) => { - instance.outgoingClearMessageId(client, p, (err, received) => { - t.assert.ifError(err) - t.assert.deepEqual(received, p, 'must return the packet') - resolve() - }) - }) - } + t.plan(total * 2) for (let i = 0; i < total; i++) { - await submitMessage(i) + const p = new Packet(packet, instance.broker) + p.messageId = i + await outgoingEnqueue(instance, sub, p) + await outgoingUpdate(instance, client, p) } let queued = 0 - for await (const p of outStream(instance, client)) { + for await (const p of (outgoingStream(instance, client))) { if (p) { queued++ } } t.assert.equal(queued, total, `outgoing queue must hold ${total} items`) - for await (const p of outStream(instance, client)) { - await clearMessage(p) + for await (const p of (outgoingStream(instance, client))) { + const received = await outgoingClearMessageId(instance, client, p) + t.assert.deepEqual(received, p, 'must return the packet') } let queued2 = 0 - for await (const p of outStream(instance, client)) { + for await (const p of (outgoingStream(instance, client))) { if (p) { queued2++ } } t.assert.equal(queued2, 0, 'outgoing queue is empty') - doCleanup(t, instance) + await doCleanup(t, instance) }) test('update to publish w/ same messageId', async (t) => { + t.plan(5) const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 @@ -1286,26 +1338,22 @@ function abstractPersistence (opts) { messageId: 42 } - instance.outgoingEnqueue(sub, packet1, () => { - instance.outgoingEnqueue(sub, packet2, () => { - instance.outgoingUpdate(client, packet1, () => { - instance.outgoingUpdate(client, packet2, () => { - const stream = instance.outgoingStream(client) - getArrayFromStream(stream).then(list => { - t.assert.equal(list.length, 2, 'must have two items in queue') - t.assert.equal(list[0].brokerCounter, packet1.brokerCounter, 'brokerCounter must match') - t.assert.equal(list[0].messageId, packet1.messageId, 'messageId must match') - t.assert.equal(list[1].brokerCounter, packet2.brokerCounter, 'brokerCounter must match') - t.assert.equal(list[1].messageId, packet2.messageId, 'messageId must match') - doCleanup(t, instance) - }) - }) - }) - }) - }) + await outgoingEnqueue(instance, sub, packet1) + await outgoingEnqueue(instance, sub, packet2) + await outgoingUpdate(instance, client, packet1) + await outgoingUpdate(instance, client, packet2) + const stream = outgoingStream(instance, client) + const list = await getArrayFromStream(stream) + t.assert.equal(list.length, 2, 'must have two items in queue') + t.assert.equal(list[0].brokerCounter, packet1.brokerCounter, 'brokerCounter must match') + t.assert.equal(list[0].messageId, packet1.messageId, 'messageId must match') + t.assert.equal(list[1].brokerCounter, packet2.brokerCounter, 'brokerCounter must match') + t.assert.equal(list[1].messageId, packet2.messageId, 'messageId must match') + await doCleanup(t, instance) }) test('update to pubrel', async (t) => { + t.plan(3) const instance = await persistence(t) const sub = { clientId: 'abcde', topic: 'hello', qos: 1 @@ -1325,36 +1373,27 @@ function abstractPersistence (opts) { brokerCounter: 42 } - instance.outgoingEnqueueCombi([sub], packet, err => { - t.assert.ifError(err) - const updated = new Packet(packet) - updated.messageId = 42 - - instance.outgoingUpdate(client, updated, (err, reclient, repacket) => { - t.assert.ifError(err) - t.assert.equal(reclient, client, 'client matches') - t.assert.equal(repacket, updated, 'packet matches') - - const pubrel = { - cmd: 'pubrel', - messageId: updated.messageId - } - - instance.outgoingUpdate(client, pubrel, err => { - t.assert.ifError(err) + await outgoingEnqueueCombi(instance, [sub], packet) + const updated = new Packet(packet) + updated.messageId = 42 + const { reclient, repacket } = await outgoingUpdate(instance, client, updated) + t.assert.equal(reclient, client, 'client matches') + t.assert.equal(repacket, updated, 'packet matches') - const stream = instance.outgoingStream(client) + const pubrel = { + cmd: 'pubrel', + messageId: updated.messageId + } - getArrayFromStream(stream).then(list => { - t.assert.deepEqual(list, [pubrel], 'must return the packet') - doCleanup(t, instance) - }) - }) - }) - }) + await outgoingUpdate(instance, client, pubrel) + const stream = outgoingStream(instance, client) + const list = await getArrayFromStream(stream) + t.assert.deepEqual(list, [pubrel], 'must return the packet') + await doCleanup(t, instance) }) test('add incoming packet, get it, and clear with messageId', async (t) => { + t.plan(3) const instance = await persistence(t) const client = { id: 'abcde' @@ -1369,40 +1408,35 @@ function abstractPersistence (opts) { retain: false, messageId: 42 } - - instance.incomingStorePacket(client, packet, err => { - t.assert.ifError(err) - - instance.incomingGetPacket(client, { - messageId: packet.messageId - }, (err, retrieved) => { - t.assert.ifError(err) - - // adjusting the objects so they match - delete retrieved.brokerCounter - delete retrieved.brokerId - delete packet.length - // strip the class identifier from the packet - const result = structuredClone(retrieved) - // Convert Uint8 to Buffer for comparison - result.payload = Buffer.from(result.payload) - t.assert.deepEqual(result, packet, 'retrieved packet must be deeply equal') - t.assert.notEqual(retrieved, packet, 'retrieved packet must not be the same object') - - instance.incomingDelPacket(client, retrieved, err => { - t.assert.ifError(err) - instance.incomingGetPacket(client, { - messageId: packet.messageId - }, (err, retrieved) => { - t.assert.ok(err, 'must error') - doCleanup(t, instance) - }) - }) - }) + await incomingStorePacket(instance, client, packet) + const retrieved = await incomingGetPacket(instance, client, { + messageId: packet.messageId }) + // adjusting the objects so they match + delete retrieved.brokerCounter + delete retrieved.brokerId + delete packet.length + // strip the class identifier from the packet + const result = structuredClone(retrieved) + // Convert Uint8 to Buffer for comparison + result.payload = Buffer.from(result.payload) + t.assert.deepEqual(result, packet, 'retrieved packet must be deeply equal') + t.assert.notEqual(retrieved, packet, 'retrieved packet must not be the same object') + await incomingDelPacket(instance, client, retrieved) + incomingGetPacket(instance, client, { + messageId: packet.messageId + }) + .then(() => { + t.assert.ok(false, 'must error') + }) + .catch(async err => { + t.assert.ok(err, 'must error') + await doCleanup(t, instance) + }) }) test('store, fetch and delete will message', async (t) => { + t.plan(7) const instance = await persistence(t) const client = { id: '12345' @@ -1414,30 +1448,23 @@ function abstractPersistence (opts) { retain: true } - instance.putWill(client, expected, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - instance.getWill(client, (err, packet, c) => { - t.assert.ifError(err, 'no error') - t.assert.deepEqual(packet, expected, 'will matches') - t.assert.equal(c, client, 'client matches') - client.brokerId = packet.brokerId - instance.delWill(client, (err, packet, c) => { - t.assert.ifError(err, 'no error') - t.assert.deepEqual(packet, expected, 'will matches') - t.assert.equal(c, client, 'client matches') - instance.getWill(client, (err, packet, c) => { - t.assert.ifError(err, 'no error') - t.assert.ok(!packet, 'no will after del') - t.assert.equal(c, client, 'client matches') - doCleanup(t, instance) - }) - }) - }) - }) + const c = await putWill(instance, client, expected) + t.assert.equal(c, client, 'client matches') + const { packet: p1, reClient: c1 } = await getWill(instance, client) + t.assert.deepEqual(p1, expected, 'will matches') + t.assert.equal(c1, client, 'client matches') + client.brokerId = p1.brokerId + const { packet: p2, reClient: c2 } = await delWill(instance, client) + t.assert.deepEqual(p2, expected, 'will matches') + t.assert.equal(c2, client, 'client matches') + const { packet: p3, reClient: c3 } = await getWill(instance, client) + t.assert.ok(!p3, 'no will after del') + t.assert.equal(c3, client, 'client matches') + await doCleanup(t, instance) }) test('stream all will messages', async (t) => { + t.plan(3) const instance = await persistence(t) const client = { id: '12345', @@ -1450,27 +1477,27 @@ function abstractPersistence (opts) { retain: true } - instance.putWill(client, toWrite, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - streamForEach(instance.streamWill(), (chunk) => { - t.assert.deepEqual(chunk, { - clientId: client.id, - brokerId: instance.broker.id, - topic: 'hello/died', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - }, 'packet matches') - instance.delWill(client, (err, result, client) => { - t.assert.ifError(err, 'no error') - doCleanup(t, instance) - }) - }) - }) + const expected = { + clientId: client.id, + brokerId: instance.broker.id, + topic: 'hello/died', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + } + + const c = await putWill(instance, client, toWrite) + t.assert.equal(c, client, 'client matches') + const stream = iterableStream(instance.streamWill()) + const list = await getArrayFromStream(stream) + t.assert.equal(list.length, 1, 'must return only one packet') + t.assert.deepEqual(list[0], expected, 'packet matches') + await delWill(instance, client) + await doCleanup(t, instance) }) test('stream all will message for unknown brokers', async (t) => { + t.plan(4) const instance = await persistence(t) const originalId = instance.broker.id const client = { @@ -1493,32 +1520,28 @@ function abstractPersistence (opts) { qos: 0, retain: true } + const expected = { + clientId: client.id, + brokerId: originalId, + topic: 'hello/died42', + payload: Buffer.from('muahahha'), + qos: 0, + retain: true + } - instance.putWill(client, toWrite1, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - instance.broker.id = 'anotherBroker' - instance.putWill(anotherClient, toWrite2, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, anotherClient, 'client matches') - streamForEach(instance.streamWill({ - anotherBroker: Date.now() - }), (chunk) => { - t.assert.deepEqual(chunk, { - clientId: client.id, - brokerId: originalId, - topic: 'hello/died42', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - }, 'packet matches') - instance.delWill(client, (err, result, client) => { - t.assert.ifError(err, 'no error') - doCleanup(t, instance) - }) - }) - }) - }) + const c = await putWill(instance, client, toWrite1) + t.assert.equal(c, client, 'client matches') + instance.broker.id = 'anotherBroker' + const c2 = await putWill(instance, anotherClient, toWrite2) + t.assert.equal(c2, anotherClient, 'client matches') + const stream = iterableStream(instance.streamWill({ + anotherBroker: Date.now() + })) + const list = await getArrayFromStream(stream) + t.assert.equal(list.length, 1, 'must return only one packet') + t.assert.deepEqual(list[0], expected, 'packet matches') + await delWill(instance, client) + await doCleanup(t, instance) }) test('delete wills from dead brokers', async (t) => { @@ -1534,16 +1557,12 @@ function abstractPersistence (opts) { retain: true } - instance.putWill(client, toWrite1, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - instance.broker.id = 'anotherBroker' - client.brokerId = instance.broker.id - instance.delWill(client, (err, result, client) => { - t.assert.ifError(err, 'no error') - doCleanup(t, instance) - }) - }) + const c = await putWill(instance, client, toWrite1) + t.assert.equal(c, client, 'client matches') + instance.broker.id = 'anotherBroker' + client.brokerId = instance.broker.id + await delWill(instance, client) + await doCleanup(t, instance) }) test('do not error if unkown messageId in outoingClearMessageId', async (t) => { @@ -1552,10 +1571,8 @@ function abstractPersistence (opts) { id: 'abc-123' } - instance.outgoingClearMessageId(client, 42, err => { - t.assert.ifError(err) - doCleanup(t, instance) - }) + await outgoingClearMessageId(instance, client, 42) + await doCleanup(t, instance) }) } diff --git a/test.js b/test.js index b29cc03..106532d 100644 --- a/test.js +++ b/test.js @@ -1,8 +1,29 @@ -const test = require('node:test') -const memory = require('./') +const { test } = require('node:test') +const events = require('node:events') +const memory = require('./persistence') const abs = require('./abstract') abs({ test, persistence: memory }) + +// create a memory instance that includes an event emitter +// to test the on-ready functionality +function createAsyncMemory (opts) { + const mem = memory(opts) + mem.emitter = new events.EventEmitter() + mem.on = mem.emitter.on.bind(mem.emitter) + mem.off = mem.emitter.removeListener.bind(mem.emitter) + mem.emit = mem.emitter.emit.bind(mem.emitter) + mem.once = mem.emitter.once.bind(mem.emitter) + mem.removeAllListeners = mem.emitter.removeAllListeners.bind(mem.emitter) + // wait 100ms before emitting ready, to simulate setup activities + setTimeout(() => mem.emit('ready'), 100) + return mem +} + +abs({ + test, + persistence: createAsyncMemory +}) From 62811de8eaa7977ddfc0100150b60e832ec2bc4a Mon Sep 17 00:00:00 2001 From: Hans Klunder Date: Tue, 11 Mar 2025 13:20:07 +0100 Subject: [PATCH 3/5] cleanup --- abstract.js.orig | 1562 ---------------------------------------------- 1 file changed, 1562 deletions(-) delete mode 100644 abstract.js.orig diff --git a/abstract.js.orig b/abstract.js.orig deleted file mode 100644 index c6937e7..0000000 --- a/abstract.js.orig +++ /dev/null @@ -1,1562 +0,0 @@ -const { Readable } = require('node:stream') -const { promisify } = require('node:util') -const Packet = require('aedes-packet') - -function waitForEvent (obj, resolveEvt) { - return new Promise((resolve, reject) => { - obj.once(resolveEvt, () => { - resolve() - }) - obj.once('error', reject) - }) -} - -function doCleanup (t, instance) { - instance.destroy(() => { - t.diagnostic('instance destroyed') - }) -} - -function abstractPersistence (opts) { - const test = opts.test - let _persistence = opts.persistence - const waitForReady = opts.waitForReady - - // requiring it here so it will not error for modules - // not using the default emitter - const buildEmitter = opts.buildEmitter || require('mqemitter') - - if (_persistence.length === 0) { - _persistence = function asyncify (cb) { - cb(null, opts.persistence()) - } - } - - const _asyncPersistence = promisify(_persistence) - async function persistence (t) { - const mq = buildEmitter() - const broker = { - id: 'broker-42', - mq, - publish: mq.emit.bind(mq), - subscribe: mq.on.bind(mq), - unsubscribe: mq.removeListener.bind(mq), - counter: 0 - } - - const instance = await _asyncPersistence() - if (instance) { - // Wait for ready event, if applicable, to ensure the persistence isn't - // destroyed while it's still being set up. - // https://github.com/mcollina/aedes-persistence-redis/issues/41 - if (waitForReady) { - await waitForEvent(instance, 'ready') - } - instance.broker = broker - t.diagnostic('instance created') - return instance - } - throw new Error('no instance') - } - - // legacy third party streams are typically not iterable - function iterableStream (stream) { - if (typeof stream[Symbol.asyncIterator] !== 'function') { - return new Readable({ objectMode: true }).wrap(stream) - } - return stream - } - // end of legacy third party streams support - - async function getArrayFromStream (stream) { - const list = [] - for await (const item of iterableStream(stream)) { - list.push(item) - } - return list - } - - async function streamForEach (stream, fn) { - for await (const item of iterableStream(stream)) { - await fn(item) - } - } - - function asyncStoreRetained (instance, packet) { - return new Promise((resolve, reject) => { - instance.storeRetained(packet, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - - async function storeRetained (instance, opts = {}) { - const packet = { - cmd: 'publish', - id: instance.broker.id, - topic: opts.topic || 'hello/world', - payload: opts.payload || Buffer.from('muahah'), - qos: 0, - retain: true - } - await asyncStoreRetained(instance, packet) - return packet - } - - async function matchRetainedWithPattern (t, pattern, opts) { - const instance = await persistence(t) - const packet = await storeRetained(instance, opts) - let stream - if (Array.isArray(pattern)) { - stream = instance.createRetainedStreamCombi(pattern) - } else { - stream = instance.createRetainedStream(pattern) - } - t.diagnostic('created stream') - const list = await getArrayFromStream(stream) - t.assert.deepEqual(list, [packet], 'must return the packet') - t.diagnostic('stream was ok') - doCleanup(t, instance) - } - - function testPacket (t, packet, expected) { - if (packet.messageId === null) packet.messageId = undefined - t.assert.equal(packet.messageId, undefined, 'should have an unassigned messageId in queue') - // deepLooseEqual? - t.assert.deepEqual(structuredClone(packet), expected, 'must return the packet') - } - - function deClassed (obj) { - return Object.assign({}, obj) - } - - // testing starts here - test('store and look up retained messages', async t => { - t.plan(1) - await matchRetainedWithPattern(t, 'hello/world') - }) - - test('look up retained messages with a # pattern', async t => { - t.plan(1) - await matchRetainedWithPattern(t, '#') - }) - - test('look up retained messages with a hello/world/# pattern', async t => { - t.plan(1) - await matchRetainedWithPattern(t, 'hello/world/#') - }) - - test('look up retained messages with a + pattern', async t => { - t.plan(1) - await matchRetainedWithPattern(t, 'hello/+') - }) - - test('look up retained messages with multiple patterns', async t => { - t.plan(1) - await matchRetainedWithPattern(t, ['hello/+', 'other/hello']) - }) - - test('store multiple retained messages in order', async (t) => { - t.plan(1000) - const instance = await persistence(t) - const totalMessages = 1000 - - const retained = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - retain: true - } - - for (let i = 0; i < totalMessages; i++) { - const packet = new Packet(retained, instance.broker) - await storeRetained(instance, packet) - t.assert.equal(packet.brokerCounter, i + 1, 'packet stored in order') - } - doCleanup(t, instance) - }) - - test('remove retained message', async (t) => { - const instance = await persistence(t) - await storeRetained(instance, {}) - await storeRetained(instance, { - payload: Buffer.alloc(0) - }) - const stream = instance.createRetainedStream('#') - const list = await getArrayFromStream(stream) - t.assert.deepEqual(list, [], 'must return an empty list') - doCleanup(t, instance) - }) - - test('storing twice a retained message should keep only the last', async (t) => { - const instance = await persistence(t) - await storeRetained(instance, {}) - const packet = await storeRetained(instance, { - payload: Buffer.from('ahah') - }) - const stream = instance.createRetainedStream('#') - const list = await getArrayFromStream(stream) - t.assert.deepEqual(list, [packet], 'must return the last packet') - doCleanup(t, instance) - }) - - test('Create a new packet while storing a retained message', async (t) => { - const instance = await persistence(t) - const packet = { - cmd: 'publish', - id: instance.broker.id, - topic: opts.topic || 'hello/world', - payload: opts.payload || Buffer.from('muahah'), - qos: 0, - retain: true - } - const newPacket = Object.assign({}, packet) - - await asyncStoreRetained(instance, packet) - // packet reference change to check if a new packet is stored always - packet.retain = false - const stream = instance.createRetainedStream('#') - const list = await getArrayFromStream(stream) - t.assert.deepEqual(list, [newPacket], 'must return the last packet') - doCleanup(t, instance) - }) - - test('store and look up subscriptions by client', async (t) => { - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs = [{ - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - topic: 'matteo', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - topic: 'noqos', - qos: 0, - rh: 0, - rap: true, - nl: false - }] - - instance.addSubscriptions(client, subs, (err, reClient) => { - t.assert.equal(reClient, client, 'client must be the same') - t.assert.ok(!err, 'no error') - instance.subscriptionsByClient(client, (err, resubs, reReClient) => { - t.assert.equal(reReClient, client, 'client must be the same') - t.assert.ok(!err, 'no error') - t.assert.deepEqual(resubs, subs) - doCleanup(t, instance) - }) - }) - }) - - async function addSubscriptions (instance, client, subs) { - return new Promise((resolve, reject) => { - instance.addSubscriptions(client, subs, (err, reClient) => { - if (err) { - reject(err) - } else { - resolve(reClient) - } - }) - }) - } - - async function removeSubscriptions (instance, client, subs) { - return new Promise((resolve, reject) => { - instance.removeSubscriptions(client, subs, (err, reClient) => { - if (err) { - reject(err) - } else { - resolve(reClient) - } - }) - }) - } - - async function subscriptionsByClient (instance, client) { - return new Promise((resolve, reject) => { - instance.subscriptionsByClient(client, (err, resubs, reClient) => { - if (err) { - reject(err) - } else { - resolve({ resubs, reClient }) - } - }) - }) - } - - async function subscriptionsByTopic (instance, topic) { - return new Promise((resolve, reject) => { - instance.subscriptionsByTopic(topic, (err, resubs) => { - if (err) { - reject(err) - } else { - resolve(resubs) - } - }) - }) - } - - async function cleanSubscriptions (instance, client) { - return new Promise((resolve, reject) => { - instance.cleanSubscriptions(client, (err) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - - async function countOffline (instance) { - return new Promise((resolve, reject) => { - instance.countOffline((err, subsCount, clientsCount) => { - if (err) { - reject(err) - } else { - resolve({ subsCount, clientsCount }) - } - }) - }) - } - - async function outgoingEnqueue (instance, sub, packet) { - return new Promise((resolve, reject) => { - instance.outgoingEnqueue(sub, packet, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - - async function outgoingEnqueueCombi (instance, subs, packet) { - return new Promise((resolve, reject) => { - instance.outgoingEnqueueCombi(subs, packet, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - } - - test('remove subscriptions by client', async (t) => { - t.plan(4) - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs = [{ - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - topic: 'matteo', - qos: 1, - rh: 0, - rap: true, - nl: false - }] - - const reclient1 = await addSubscriptions(instance, client, subs) - t.assert.equal(reclient1, client, 'client must be the same') - const reClient2 = await removeSubscriptions(instance, client, ['hello']) - t.assert.equal(reClient2, client, 'client must be the same') - const { resubs, reClient } = await subscriptionsByClient(instance, client) - t.assert.equal(reClient, client, 'client must be the same') - t.assert.deepEqual(resubs, [{ - topic: 'matteo', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - doCleanup(t, instance) - }) - - test('store and look up subscriptions by topic', async (t) => { - t.plan(2) - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs = [{ - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - topic: 'hello/#', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - topic: 'matteo', - qos: 1, - rh: 0, - rap: true, - nl: false - }] - - const reclient = await addSubscriptions(instance, client, subs) - t.assert.equal(reclient, client, 'client must be the same') - const resubs = await subscriptionsByTopic(instance, 'hello') - t.assert.deepEqual(resubs, [{ - clientId: client.id, - topic: 'hello/#', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - clientId: client.id, - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - doCleanup(t, instance) - }) - - test('get client list after subscriptions', async (t) => { - t.plan(1) - const instance = await persistence(t) - const client1 = { id: 'abcde' } - const client2 = { id: 'efghi' } - const subs = [{ - topic: 'helloagain', - qos: 1 - }] - - await addSubscriptions(instance, client1, subs) - await addSubscriptions(instance, client2, subs) - const stream = instance.getClientList(subs[0].topic) - const list = await getArrayFromStream(stream) - t.assert.deepEqual(list, [client1.id, client2.id]) - doCleanup(t, instance) - }) - - test('get client list after an unsubscribe', async (t) => { - t.plan(1) - const instance = await persistence(t) - const client1 = { id: 'abcde' } - const client2 = { id: 'efghi' } - const subs = [{ - topic: 'helloagain', - qos: 1 - }] - await addSubscriptions(instance, client1, subs) - await addSubscriptions(instance, client2, subs) - await removeSubscriptions(instance, client2, [subs[0].topic]) - const stream = instance.getClientList(subs[0].topic) - const list = await getArrayFromStream(stream) - t.assert.deepEqual(list, [client1.id]) - doCleanup(t, instance) - }) - - test('get subscriptions list after an unsubscribe', async (t) => { - t.plan(1) - const instance = await persistence(t) - const client1 = { id: 'abcde' } - const client2 = { id: 'efghi' } - const subs = [{ - topic: 'helloagain', - qos: 1 - }] - await addSubscriptions(instance, client1, subs) - await addSubscriptions(instance, client2, subs) - await removeSubscriptions(instance, client2, [subs[0].topic]) - const clients = await subscriptionsByTopic(instance, subs[0].topic) - t.assert.deepEqual(clients[0].clientId, client1.id) - doCleanup(t, instance) - }) - - test('QoS 0 subscriptions, restored but not matched', async (t) => { - t.plan(2) - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs = [{ - topic: 'hello', - qos: 0, - rh: 0, - rap: true, - nl: false - }, { - topic: 'hello/#', - qos: 1, - rh: 0, - rap: true, - nl: false - }, { - topic: 'matteo', - qos: 1, - rh: 0, - rap: true, - nl: false - }] - - await addSubscriptions(instance, client, subs) - const { resubs } = await subscriptionsByClient(instance, client) - t.assert.deepEqual(resubs, subs) - const resubs2 = await subscriptionsByTopic(instance, 'hello') - t.assert.deepEqual(resubs2, [{ - clientId: client.id, - topic: 'hello/#', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - doCleanup(t, instance) - }) - - test('clean subscriptions', async (t) => { - t.plan(4) - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs = [{ - topic: 'hello', - qos: 1 - }, { - topic: 'matteo', - qos: 1 - }] - - await addSubscriptions(instance, client, subs) - await cleanSubscriptions(instance, client) - const resubs = await subscriptionsByTopic(instance, 'hello') - t.assert.deepEqual(resubs, [], 'no subscriptions') - const { resubs: resubs2 } = await subscriptionsByClient(instance, client) - t.assert.deepEqual(resubs2, null, 'no subscriptions') - const { subsCount, clientsCount } = await countOffline(instance) - t.assert.equal(subsCount, 0, 'no subscriptions added') - t.assert.equal(clientsCount, 0, 'no clients added') - doCleanup(t, instance) - }) - - test('clean subscriptions with no active subscriptions', async (t) => { - t.plan(4) - const instance = await persistence(t) - const client = { id: 'abcde' } - - await cleanSubscriptions(instance, client) - const resubs = await subscriptionsByTopic(instance, 'hello') - t.assert.deepEqual(resubs, [], 'no subscriptions') - const { resubs: resubs2 } = await subscriptionsByClient(instance, client) - t.assert.deepEqual(resubs2, null, 'no subscriptions') - const { subsCount, clientsCount } = await countOffline(instance) - t.assert.equal(subsCount, 0, 'no subscriptions added') - t.assert.equal(clientsCount, 0, 'no clients added') - doCleanup(t, instance) - }) - - test('same topic, different QoS', async (t) => { - t.plan(5) - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs = [{ - topic: 'hello', - qos: 0, - rh: 0, - rap: true, - nl: false - }, { - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }] - - const reClient = await addSubscriptions(instance, client, subs) - t.assert.equal(reClient, client, 'client must be the same') - const { resubs } = await subscriptionsByClient(instance, client) - t.assert.deepEqual(resubs, [{ - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - - const resubs2 = await subscriptionsByTopic(instance, 'hello') - t.assert.deepEqual(resubs2, [{ - clientId: 'abcde', - topic: 'hello', - qos: 1, - rh: 0, - rap: true, - nl: false - }]) - - const { subsCount, clientsCount } = await countOffline(instance) - t.assert.equal(subsCount, 1, 'one subscription added') - t.assert.equal(clientsCount, 1, 'one client added') - doCleanup(t, instance) - }) - - test('replace subscriptions', async (t) => { - t.plan(25) - const instance = await persistence(t) - const client = { id: 'abcde' } - const topic = 'hello' - const sub = { topic, rh: 0, rap: true, nl: false } - const subByTopic = { clientId: client.id, topic, rh: 0, rap: true, nl: false } - - async function check (qos) { - sub.qos = subByTopic.qos = qos - const reClient = await addSubscriptions(instance, client, [sub]) - t.assert.equal(reClient, client, 'client must be the same') - const { resubs } = await subscriptionsByClient(instance, client) - t.assert.deepEqual(resubs, [sub]) - const subsForTopic = await subscriptionsByTopic(instance, topic) - t.assert.deepEqual(subsForTopic, qos === 0 ? [] : [subByTopic]) - const { subsCount, clientsCount } = await countOffline(instance) - if (qos === 0) { - t.assert.equal(subsCount, 0, 'no subscriptions added') - } else { - t.assert.equal(subsCount, 1, 'one subscription added') - } - t.assert.equal(clientsCount, 1, 'one client added') - } - - await check(0) - await check(1) - await check(2) - await check(1) - await check(0) - doCleanup(t, instance) - }) - - test('replace subscriptions in same call', async (t) => { - t.plan(5) - const instance = await persistence(t) - const client = { id: 'abcde' } - const topic = 'hello' - const subs = [ - { topic, qos: 0, rh: 0, rap: true, nl: false }, - { topic, qos: 1, rh: 0, rap: true, nl: false }, - { topic, qos: 2, rh: 0, rap: true, nl: false }, - { topic, qos: 1, rh: 0, rap: true, nl: false }, - { topic, qos: 0, rh: 0, rap: true, nl: false } - ] - const reClient = await addSubscriptions(instance, client, subs) - t.assert.equal(reClient, client, 'client must be the same') - const { resubs: subsForClient } = await subscriptionsByClient(instance, client) - t.assert.deepEqual(subsForClient, [{ topic, qos: 0, rh: 0, rap: true, nl: false }]) - const subsForTopic = await subscriptionsByTopic(instance, topic) - t.assert.deepEqual(subsForTopic, []) - const { subsCount, clientsCount } = await countOffline(instance) - t.assert.equal(subsCount, 0, 'no subscriptions added') - t.assert.equal(clientsCount, 1, 'one client added') - doCleanup(t, instance) - }) - - test('store and count subscriptions', async (t) => { - t.plan(11) - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs = [{ - topic: 'hello', - qos: 1 - }, { - topic: 'matteo', - qos: 1 - }, { - topic: 'noqos', - qos: 0 - }] - - const reclient = await addSubscriptions(instance, client, subs) - t.assert.equal(reclient, client, 'client must be the same') - const { subsCount, clientsCount } = await countOffline(instance) - t.assert.equal(subsCount, 2, 'two subscriptions added') - t.assert.equal(clientsCount, 1, 'one client added') - await removeSubscriptions(instance, client, ['hello']) - const { subsCount: subsCount2, clientsCount: clientsCount2 } = await countOffline(instance) - t.assert.equal(subsCount2, 1, 'one subscription added') - t.assert.equal(clientsCount2, 1, 'one client added') - await removeSubscriptions(instance, client, ['matteo']) - const { subsCount: subsCount3, clientsCount: clientsCount3 } = await countOffline(instance) - t.assert.equal(subsCount3, 0, 'zero subscriptions added') - t.assert.equal(clientsCount3, 1, 'one client added') - await removeSubscriptions(instance, client, ['noqos']) - const { subsCount: subsCount4, clientsCount: clientsCount4 } = await countOffline(instance) - t.assert.equal(subsCount4, 0, 'zero subscriptions added') - t.assert.equal(clientsCount4, 0, 'zero clients added') - await removeSubscriptions(instance, client, ['noqos']) - const { subsCount: subsCount5, clientsCount: clientsCount5 } = await countOffline(instance) - t.assert.equal(subsCount5, 0, 'zero subscriptions added') - t.assert.equal(clientsCount5, 0, 'zero clients added') - doCleanup(t, instance) - }) - - test('count subscriptions with two clients', async (t) => { - t.plan(26) - const instance = await persistence(t) - const client1 = { id: 'abcde' } - const client2 = { id: 'fghij' } - const subs = [{ - topic: 'hello', - qos: 1 - }, { - topic: 'matteo', - qos: 1 - }, { - topic: 'noqos', - qos: 0 - }] - - async function remove (client, subs, expectedSubs, expectedClients) { - const reClient = await removeSubscriptions(instance, client, subs) - t.assert.equal(reClient, client, 'client must be the same') - const { subsCount, clientsCount } = await countOffline(instance) - t.assert.equal(subsCount, expectedSubs, 'subscriptions added') - t.assert.equal(clientsCount, expectedClients, 'clients added') - } - - const reClient1 = await addSubscriptions(instance, client1, subs) - t.assert.equal(reClient1, client1, 'client must be the same') - const reClient2 = await addSubscriptions(instance, client2, subs) - t.assert.equal(reClient2, client2, 'client must be the same') - await remove(client1, ['foobar'], 4, 2) - await remove(client1, ['hello'], 3, 2) - await remove(client1, ['hello'], 3, 2) - await remove(client1, ['matteo'], 2, 2) - await remove(client1, ['noqos'], 2, 1) - await remove(client2, ['hello'], 1, 1) - await remove(client2, ['matteo'], 0, 1) - await remove(client2, ['noqos'], 0, 0) - doCleanup(t, instance) - }) - - test('add duplicate subs to persistence for qos > 0', async (t) => { - t.plan(3) - const instance = await persistence(t) - const client = { id: 'abcde' } - const topic = 'hello' - const subs = [{ - topic, - qos: 1, - rh: 0, - rap: true, - nl: false - }] - - const reClient = await addSubscriptions(instance, client, subs) - t.assert.equal(reClient, client, 'client must be the same') - const reClient2 = await addSubscriptions(instance, client, subs) - t.assert.equal(reClient2, client, 'client must be the same') - subs[0].clientId = client.id - const subsForTopic = await subscriptionsByTopic(instance, topic) - t.assert.deepEqual(subsForTopic, subs) - doCleanup(t, instance) - }) - - test('add duplicate subs to persistence for qos 0', async (t) => { - t.plan(3) - const instance = await persistence(t) - const client = { id: 'abcde' } - const topic = 'hello' - const subs = [{ - topic, - qos: 0, - rh: 0, - rap: true, - nl: false - }] - - const reClient = await addSubscriptions(instance, client, subs) - t.assert.equal(reClient, client, 'client must be the same') - const reClient2 = await addSubscriptions(instance, client, subs) - t.assert.equal(reClient2, client, 'client must be the same') - const { resubs: subsForClient } = await subscriptionsByClient(instance, client) - t.assert.deepEqual(subsForClient, subs) - doCleanup(t, instance) - }) - - test('get topic list after concurrent subscriptions of a client', async (t) => { - t.plan(3) - const instance = await persistence(t) - const client = { id: 'abcde' } - const subs1 = [{ - topic: 'hello1', - qos: 1, - rh: 0, - rap: true, - nl: false - }] - const subs2 = [{ - topic: 'hello2', - qos: 1, - rh: 0, - rap: true, - nl: false - }] - let calls = 2 - - await new Promise((resolve, reject) => { - async function done () { - if (!--calls) { - const { resubs } = await subscriptionsByClient(instance, client) - resubs.sort((a, b) => a.topic.localeCompare(b.topic, 'en')) - t.assert.deepEqual(resubs, [subs1[0], subs2[0]]) - doCleanup(t, instance) - resolve() - } - } - - instance.addSubscriptions(client, subs1, err => { - t.assert.ok(!err, 'no error for hello1') - done() - }) - instance.addSubscriptions(client, subs2, err => { - t.assert.ok(!err, 'no error for hello2') - done() - }) - }) - }) - - test('add outgoing packet and stream it', async (t) => { - t.plan(2) - const instance = await persistence(t) - const sub = { - clientId: 'abcde', - topic: 'hello', - qos: 1 - } - const client = { - id: sub.clientId - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42 - } - const expected = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - retain: false, - dup: false, - brokerId: instance.broker.id, - brokerCounter: 42, - messageId: undefined - } - - await outgoingEnqueue(instance, sub, packet) - const stream = instance.outgoingStream(client) - const list = await getArrayFromStream(stream) - testPacket(t, list[0], expected) - doCleanup(t, instance) - }) - - test('add outgoing packet for multiple subs and stream to all', async (t) => { - t.plan(4) - const instance = await persistence(t) - const sub = { - clientId: 'abcde', - topic: 'hello', - qos: 1 - } - const sub2 = { - clientId: 'fghih', - topic: 'hello', - qos: 1 - } - const subs = [sub, sub2] - const client = { - id: sub.clientId - } - const client2 = { - id: sub2.clientId - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42 - } - const expected = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - retain: false, - dup: false, - brokerId: instance.broker.id, - brokerCounter: 42, - messageId: undefined - } - - await outgoingEnqueueCombi(instance, subs, packet) - const stream = instance.outgoingStream(client) - const list = await getArrayFromStream(stream) - testPacket(t, list[0], expected) - - const stream2 = instance.outgoingStream(client2) - const list2 = await getArrayFromStream(stream2) - testPacket(t, list2[0], expected) - doCleanup(t, instance) - }) - - test('add outgoing packet as a string and pump', async (t) => { - t.plan(7) - const instance = await persistence(t) - const sub = { - clientId: 'abcde', - topic: 'hello', - qos: 1 - } - const client = { - id: sub.clientId - } - const packet1 = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 10 - } - const packet2 = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('matteo'), - qos: 1, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 50 - } - const queue = [] - - const updated1 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet1, 42) - const updated2 = await asyncEnqueueAndUpdate(t, instance, client, sub, packet2, 43) - const stream = instance.outgoingStream(client) - - async function clearQueue (data) { - const { repacket } = await outgoingUpdate(instance, client, data) - t.diagnostic('packet received') - queue.push(repacket) - } - - await streamForEach(stream, clearQueue) - t.assert.equal(queue.length, 2) - t.assert.deepEqual(deClassed(queue[0]), deClassed(updated1)) - t.assert.deepEqual(deClassed(queue[1]), deClassed(updated2)) - doCleanup(t, instance) - }) - - test('add outgoing packet as a string and stream', async (t) => { - t.plan(2) - const instance = await persistence(t) - const sub = { - clientId: 'abcde', - topic: 'hello', - qos: 1 - } - const client = { - id: sub.clientId - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: 'world', - qos: 1, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42 - } - const expected = { - cmd: 'publish', - topic: 'hello', - payload: 'world', - qos: 1, - retain: false, - dup: false, - brokerId: instance.broker.id, - brokerCounter: 42, - messageId: undefined - } - - await outgoingEnqueueCombi(instance, [sub], packet) - const stream = instance.outgoingStream(client) - const list = await getArrayFromStream(stream) - testPacket(t, list[0], expected) - doCleanup(t, instance) - }) - - test('add outgoing packet and stream it twice', async (t) => { - const instance = await persistence(t) - const sub = { - clientId: 'abcde', - topic: 'hello', - qos: 1 - } - const client = { - id: sub.clientId - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42, - messageId: 4242 - } - const expected = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - retain: false, - dup: false, - brokerId: instance.broker.id, - brokerCounter: 42, - messageId: undefined - } - - await outgoingEnqueueCombi(instance, [sub], packet) - const stream = instance.outgoingStream(client) - const list = await getArrayFromStream(stream) - testPacket(t, list[0], expected) - const stream2 = instance.outgoingStream(client) - const list2 = await getArrayFromStream(stream2) - testPacket(t, list2[0], expected) - t.assert.notEqual(packet, expected, 'packet must be a different object') - doCleanup(t, instance) - }) - - async function enqueueAndUpdate (t, instance, client, sub, packet, messageId) { - await outgoingEnqueueCombi(instance, [sub], packet) - const updated = new Packet(packet) - updated.messageId = messageId - - const { reclient, repacket } = await outgoingUpdate(instance, client, updated) - t.assert.equal(reclient, client, 'client matches') - t.assert.equal(repacket, updated, 'packet matches') - return repacket - } - - async function outgoingUpdate (instance, client, packet) { - return new Promise((resolve, reject) => { - instance.outgoingUpdate(client, packet, (err, reclient, repacket) => { - if (err) { - reject(err) - } else { - resolve({ reclient, repacket }) - } - }) - }) - } - - async function asyncEnqueueAndUpdate (t, instance, client, sub, packet, messageId) { - await outgoingEnqueueCombi(instance, [sub], packet) - const updated = new Packet(packet) - updated.messageId = messageId - const { reclient, repacket } = await outgoingUpdate(instance, client, updated) - t.assert.equal(reclient, client, 'client matches') - t.assert.equal(repacket, updated, 'packet matches') - return updated - } - - test('add outgoing packet and update messageId', async (t) => { - t.plan(5) - const instance = await persistence(t) - const sub = { - clientId: 'abcde', topic: 'hello', qos: 1 - } - const client = { - id: sub.clientId - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42 - } - - const updated = await enqueueAndUpdate(t, instance, client, sub, packet, 42) - delete updated.messageId - const stream = instance.outgoingStream(client) - const list = await getArrayFromStream(stream) - delete list[0].messageId - t.assert.notEqual(list[0], updated, 'must not be the same object') - t.assert.deepEqual(deClassed(list[0]), deClassed(updated), 'must return the packet') - t.assert.equal(list.length, 1, 'must return only one packet') - doCleanup(t, instance) - }) - - test('add 2 outgoing packet and clear messageId', async (t) => { - const instance = await persistence(t) - const sub = { - clientId: 'abcde', topic: 'hello', qos: 1 - } - const client = { - id: sub.clientId - } - const packet1 = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42 - } - const packet2 = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('matteo'), - qos: 1, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 43 - } - - const updated1 = await enqueueAndUpdate(t, instance, client, sub, packet1, 42) - const updated2 = await enqueueAndUpdate(t, instance, client, sub, packet2, 43) - instance.outgoingClearMessageId(client, updated1, async (err, packet) => { - t.assert.ifError(err) - t.assert.deepEqual(packet.messageId, 42, 'must have the same messageId') - t.assert.deepEqual(packet.payload.toString(), packet1.payload.toString(), 'must have original payload') - t.assert.deepEqual(packet.topic, packet1.topic, 'must have original topic') - const stream = instance.outgoingStream(client) - delete updated2.messageId - const list = await getArrayFromStream(stream) - delete list[0].messageId - t.assert.notEqual(list[0], updated2, 'must not be the same object') - t.assert.deepEqual(deClassed(list[0]), deClassed(updated2), 'must return the packet') - t.assert.equal(list.length, 1, 'must return only one packet') - doCleanup(t, instance) - }) - }) - - test('add many outgoing packets and clear messageIds', async (t) => { - const instance = await persistence(t) - const sub = { - clientId: 'abcde', topic: 'hello', qos: 1 - } - const client = { - id: sub.clientId - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 1, - dup: false, - length: 14, - retain: false - } - - function outStream (instance, client) { - return iterableStream(instance.outgoingStream(client)) - } - - // we just need a stream to figure out the high watermark - const stream = outStream(instance, client) - const total = stream.readableHighWaterMark * 2 - - function submitMessage (id) { - return new Promise((resolve, reject) => { - const p = new Packet(packet, instance.broker) - p.messageId = id - instance.outgoingEnqueue(sub, p, (err) => { - if (err) { - return reject(err) - } - instance.outgoingUpdate(client, p, resolve) - }) - }) - } - - function clearMessage (p) { - return new Promise((resolve, reject) => { - instance.outgoingClearMessageId(client, p, (err, received) => { - t.assert.ifError(err) - t.assert.deepEqual(received, p, 'must return the packet') - resolve() - }) - }) - } - - for (let i = 0; i < total; i++) { - await submitMessage(i) - } - - let queued = 0 - for await (const p of outStream(instance, client)) { - if (p) { - queued++ - } - } - t.assert.equal(queued, total, `outgoing queue must hold ${total} items`) - - for await (const p of outStream(instance, client)) { - await clearMessage(p) - } - - let queued2 = 0 - for await (const p of outStream(instance, client)) { - if (p) { - queued2++ - } - } - t.assert.equal(queued2, 0, 'outgoing queue is empty') - doCleanup(t, instance) - }) - - test('update to publish w/ same messageId', async (t) => { - const instance = await persistence(t) - const sub = { - clientId: 'abcde', topic: 'hello', qos: 1 - } - const client = { - id: sub.clientId - } - const packet1 = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 2, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42, - messageId: 42 - } - const packet2 = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 2, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 50, - messageId: 42 - } - - instance.outgoingEnqueue(sub, packet1, () => { - instance.outgoingEnqueue(sub, packet2, () => { - instance.outgoingUpdate(client, packet1, () => { - instance.outgoingUpdate(client, packet2, () => { - const stream = instance.outgoingStream(client) - getArrayFromStream(stream).then(list => { - t.assert.equal(list.length, 2, 'must have two items in queue') - t.assert.equal(list[0].brokerCounter, packet1.brokerCounter, 'brokerCounter must match') - t.assert.equal(list[0].messageId, packet1.messageId, 'messageId must match') - t.assert.equal(list[1].brokerCounter, packet2.brokerCounter, 'brokerCounter must match') - t.assert.equal(list[1].messageId, packet2.messageId, 'messageId must match') - doCleanup(t, instance) - }) - }) - }) - }) - }) - }) - - test('update to pubrel', async (t) => { - const instance = await persistence(t) - const sub = { - clientId: 'abcde', topic: 'hello', qos: 1 - } - const client = { - id: sub.clientId - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 2, - dup: false, - length: 14, - retain: false, - brokerId: instance.broker.id, - brokerCounter: 42 - } - - instance.outgoingEnqueueCombi([sub], packet, err => { - t.assert.ifError(err) - const updated = new Packet(packet) - updated.messageId = 42 - - instance.outgoingUpdate(client, updated, (err, reclient, repacket) => { - t.assert.ifError(err) - t.assert.equal(reclient, client, 'client matches') - t.assert.equal(repacket, updated, 'packet matches') - - const pubrel = { - cmd: 'pubrel', - messageId: updated.messageId - } - - instance.outgoingUpdate(client, pubrel, err => { - t.assert.ifError(err) - - const stream = instance.outgoingStream(client) - - getArrayFromStream(stream).then(list => { - t.assert.deepEqual(list, [pubrel], 'must return the packet') - doCleanup(t, instance) - }) - }) - }) - }) - }) - - test('add incoming packet, get it, and clear with messageId', async (t) => { - const instance = await persistence(t) - const client = { - id: 'abcde' - } - const packet = { - cmd: 'publish', - topic: 'hello', - payload: Buffer.from('world'), - qos: 2, - dup: false, - length: 14, - retain: false, - messageId: 42 - } - - instance.incomingStorePacket(client, packet, err => { - t.assert.ifError(err) - - instance.incomingGetPacket(client, { - messageId: packet.messageId - }, (err, retrieved) => { - t.assert.ifError(err) - - // adjusting the objects so they match - delete retrieved.brokerCounter - delete retrieved.brokerId - delete packet.length - // strip the class identifier from the packet - const result = structuredClone(retrieved) - // Convert Uint8 to Buffer for comparison - result.payload = Buffer.from(result.payload) - t.assert.deepEqual(result, packet, 'retrieved packet must be deeply equal') - t.assert.notEqual(retrieved, packet, 'retrieved packet must not be the same object') - - instance.incomingDelPacket(client, retrieved, err => { - t.assert.ifError(err) - instance.incomingGetPacket(client, { - messageId: packet.messageId - }, (err, retrieved) => { - t.assert.ok(err, 'must error') - doCleanup(t, instance) - }) - }) - }) - }) - }) - - test('store, fetch and delete will message', async (t) => { - const instance = await persistence(t) - const client = { - id: '12345' - } - const expected = { - topic: 'hello/died', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - } - - instance.putWill(client, expected, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - instance.getWill(client, (err, packet, c) => { - t.assert.ifError(err, 'no error') - t.assert.deepEqual(packet, expected, 'will matches') - t.assert.equal(c, client, 'client matches') - client.brokerId = packet.brokerId - instance.delWill(client, (err, packet, c) => { - t.assert.ifError(err, 'no error') - t.assert.deepEqual(packet, expected, 'will matches') - t.assert.equal(c, client, 'client matches') - instance.getWill(client, (err, packet, c) => { - t.assert.ifError(err, 'no error') - t.assert.ok(!packet, 'no will after del') - t.assert.equal(c, client, 'client matches') - doCleanup(t, instance) - }) - }) - }) - }) - }) - - test('stream all will messages', async (t) => { - const instance = await persistence(t) - const client = { - id: '12345', - brokerId: instance.broker.id - } - const toWrite = { - topic: 'hello/died', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - } - - instance.putWill(client, toWrite, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - streamForEach(instance.streamWill(), (chunk) => { - t.assert.deepEqual(chunk, { - clientId: client.id, - brokerId: instance.broker.id, - topic: 'hello/died', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - }, 'packet matches') - instance.delWill(client, (err, result, client) => { - t.assert.ifError(err, 'no error') - doCleanup(t, instance) - }) - }) - }) - }) - - test('stream all will message for unknown brokers', async (t) => { - const instance = await persistence(t) - const originalId = instance.broker.id - const client = { - id: '42', - brokerId: instance.broker.id - } - const anotherClient = { - id: '24', - brokerId: instance.broker.id - } - const toWrite1 = { - topic: 'hello/died42', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - } - const toWrite2 = { - topic: 'hello/died24', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - } - - instance.putWill(client, toWrite1, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - instance.broker.id = 'anotherBroker' - instance.putWill(anotherClient, toWrite2, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, anotherClient, 'client matches') - streamForEach(instance.streamWill({ - anotherBroker: Date.now() - }), (chunk) => { - t.assert.deepEqual(chunk, { - clientId: client.id, - brokerId: originalId, - topic: 'hello/died42', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - }, 'packet matches') - instance.delWill(client, (err, result, client) => { - t.assert.ifError(err, 'no error') - doCleanup(t, instance) - }) - }) - }) - }) - }) - - test('delete wills from dead brokers', async (t) => { - const instance = await persistence(t) - const client = { - id: '42' - } - - const toWrite1 = { - topic: 'hello/died42', - payload: Buffer.from('muahahha'), - qos: 0, - retain: true - } - - instance.putWill(client, toWrite1, (err, c) => { - t.assert.ifError(err, 'no error') - t.assert.equal(c, client, 'client matches') - instance.broker.id = 'anotherBroker' - client.brokerId = instance.broker.id - instance.delWill(client, (err, result, client) => { - t.assert.ifError(err, 'no error') - doCleanup(t, instance) - }) - }) - }) - - test('do not error if unkown messageId in outoingClearMessageId', async (t) => { - const instance = await persistence(t) - const client = { - id: 'abc-123' - } - - instance.outgoingClearMessageId(client, 42, err => { - t.assert.ifError(err) - doCleanup(t, instance) - }) - }) -} - -module.exports = abstractPersistence From 1b9f2153f2a580fec444c085f8955f88ff5d3df5 Mon Sep 17 00:00:00 2001 From: Hans Klunder Date: Tue, 11 Mar 2025 20:36:05 +0100 Subject: [PATCH 4/5] fix missing assert wile testing with Redis --- abstract.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/abstract.js b/abstract.js index bcf49b6..c5b15e3 100644 --- a/abstract.js +++ b/abstract.js @@ -1423,16 +1423,16 @@ function abstractPersistence (opts) { t.assert.deepEqual(result, packet, 'retrieved packet must be deeply equal') t.assert.notEqual(retrieved, packet, 'retrieved packet must not be the same object') await incomingDelPacket(instance, client, retrieved) - incomingGetPacket(instance, client, { - messageId: packet.messageId - }) - .then(() => { - t.assert.ok(false, 'must error') - }) - .catch(async err => { - t.assert.ok(err, 'must error') - await doCleanup(t, instance) + + try { + await incomingGetPacket(instance, client, { + messageId: packet.messageId }) + t.assert.ok(false, 'must error') + } catch (err) { + t.assert.ok(err, 'must error') + await doCleanup(t, instance) + } }) test('store, fetch and delete will message', async (t) => { From e3ebdf24b16e1871674b27fc9d1670abd8910f75 Mon Sep 17 00:00:00 2001 From: Hans Klunder Date: Wed, 12 Mar 2025 06:56:06 +0100 Subject: [PATCH 5/5] remove promisify() --- abstract.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/abstract.js b/abstract.js index c5b15e3..c848887 100644 --- a/abstract.js +++ b/abstract.js @@ -1,5 +1,4 @@ const { Readable } = require('node:stream') -const { promisify } = require('node:util') const Packet = require('aedes-packet') // promisified versions of the instance methods @@ -219,8 +218,16 @@ function waitForEvent (obj, resolveEvt) { } async function doCleanup (t, instance) { - const instanceDestroy = promisify(instance.destroy).bind(instance) - await instanceDestroy() + const instanceDestroy = new Promise((resolve, reject) => { + instance.destroy((err) => { + if (err) { + reject(err) + return + } + resolve() + }) + }) + await instanceDestroy t.diagnostic('instance cleaned up') }