首次提交
This commit is contained in:
+50
@@ -0,0 +1,50 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x, '*']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Check licenses
|
||||
run: |
|
||||
npm run license-checker
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
npm run test:ci
|
||||
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel: true
|
||||
|
||||
coverage:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Aedes Contributors
|
||||
Copyright (c) 2015-2020 Matteo Collina, http://matteocollina.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
# aedes-persistence
|
||||

|
||||
[](https://david-dm.org/moscajs/aedes-persistence)
|
||||
[](https://david-dm.org/moscajs/aedes-persistence?type=dev)
|
||||
<br/>
|
||||
[](https://snyk.io/test/github/moscajs/aedes-persistence)
|
||||
[](https://coveralls.io/github/moscajs/aedes-persistence?branch=master)
|
||||
[](https://www.npmjs.com/package/aedes-persistence)
|
||||
[](https://www.npmjs.com/package/aedes-persistence)
|
||||
|
||||
The spec for an [Aedes](http://npm.im/aedes) persistence, with abstract
|
||||
tests and a fast in-memory implementation.
|
||||
|
||||
* [Install](#install)
|
||||
* [API](#api)
|
||||
* [Implement another persistence](#implement)
|
||||
* [License](#license)
|
||||
|
||||
<a name="install"></a>
|
||||
## Install
|
||||
To install aedes-persistence, simply use npm:
|
||||
|
||||
```
|
||||
npm install aedes-persistence --save
|
||||
```
|
||||
|
||||
<a name="api"></a>
|
||||
## API
|
||||
|
||||
* <a href="#constructor"><code><b>persistence()</b></code></a>
|
||||
* <a href="#storeRetained"><code>instance.<b>storeRetained()</b></code></a>
|
||||
* <a href="#createRetainedStream"><code>instance.<b>createRetainedStream()</b></code></a>
|
||||
* <a href="#createRetainedStreamCombi"><code>instance.<b>createRetainedStreamCombi()</b></code></a>
|
||||
* <a href="#addSubscriptions"><code>instance.<b>addSubscriptions()</b></code></a>
|
||||
* <a href="#removeSubscriptions"><code>instance.<b>removeSubscriptions()</b></code></a>
|
||||
* <a href="#subscriptionsByClient"><code>instance.<b>subscriptionsByClient()</b></code></a>
|
||||
* <a href="#countOffline"><code>instance.<b>countOffline()</b></code></a>
|
||||
* <a href="#subscriptionsByTopic"><code>instance.<b>subscriptionsByTopic()</b></code></a>
|
||||
* <a href="#cleanSubscriptions"><code>instance.<b>cleanSubscriptions()</b></code></a>
|
||||
* <a href="#outgoingEnqueue"><code>instance.<b>outgoingEnqueue()</b></code></a>
|
||||
* <a href="#outgoingEnqueueCombi"><code>instance.<b>outgoingEnqueueCombi()</b></code></a>
|
||||
* <a href="#outgoingUpdate"><code>instance.<b>outgoingUpdate()</b></code></a>
|
||||
* <a href="#outgoingClearMessageId"><code>instance.<b>outgoingClearMessageId()</b></code></a>
|
||||
* <a href="#outgoingStream"><code>instance.<b>outgoingStream()</b></code></a>
|
||||
* <a href="#incomingStorePacket"><code>instance.<b>incomingStorePacket()</b></code></a>
|
||||
* <a href="#incomingGetPacket"><code>instance.<b>incomingGetPacket()</b></code></a>
|
||||
* <a href="#incomingDelPacket"><code>instance.<b>incomingDelPacket()</b></code></a>
|
||||
* <a href="#putWill"><code>instance.<b>putWill()</b></code></a>
|
||||
* <a href="#getWill"><code>instance.<b>getWill()</b></code></a>
|
||||
* <a href="#delWill"><code>instance.<b>delWill()</b></code></a>
|
||||
* <a href="#streamWill"><code>instance.<b>streamWill()</b></code></a>
|
||||
* <a href="#getClientList"><code>instance.<b>getClientList()</b></code></a>
|
||||
* <a href="#destroy"><code>instance.<b>destroy()</b></code></a>
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="constructor"></a>
|
||||
### persistence([opts])
|
||||
|
||||
Creates a new instance of a persistence, that is already ready to
|
||||
operate. The default implementation is in-memory only.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="storeRetained"></a>
|
||||
### instance.storeRetained(packet, callback(err))
|
||||
|
||||
Store a retained message, calls the callback when it was saved.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="createRetainedStream"></a>
|
||||
### instance.createRetainedStream(pattern)
|
||||
|
||||
Return a stream that will load all retained messages matching the given
|
||||
pattern (according to the MQTT spec) asynchronously. Deprecated.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="createRetainedStreamCombi"></a>
|
||||
### instance.createRetainedStreamCombi(patterns)
|
||||
|
||||
Return a stream that will load all retained messages matching given
|
||||
patterns (according to the MQTT spec) asynchronously.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="addSubscriptions"></a>
|
||||
### instance.addSubscriptions(client, subscriptions, callback(err, client))
|
||||
|
||||
Add the given offline subscriptions for the given
|
||||
[Client](https://github.com/moscajs/aedes#client). The client __must__
|
||||
have connected with `clean: false`, as this is not checked here.
|
||||
This is called when a client issue a SUBSCRIBE packet.
|
||||
|
||||
`subscriptions` is in the same format of the `subscribe` property in the
|
||||
[SUBSCRIBE](https://github.com/mqttjs/mqtt-packet#subscribe) packet:
|
||||
|
||||
```js
|
||||
[{
|
||||
topic: 'hello/world',
|
||||
qos: 1,
|
||||
}, {
|
||||
topic: 'hello/#',
|
||||
qos: 2,
|
||||
}]
|
||||
```
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="removeSubscriptions"></a>
|
||||
### instance.removeSubscriptions(client, subscriptions, callback(err, client))
|
||||
|
||||
The inverse of [`addSubscriptions`](#addSubscriptions) but subscriptions is an array of topic names.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="subscriptionsByClient"></a>
|
||||
### instance.subscriptionsByClient(client, callback(err, subscriptions, client))
|
||||
|
||||
Returns all the offline subscriptions for the given client. Called when
|
||||
a client with `clean: false` connects to restore its subscriptions.
|
||||
|
||||
`subscriptions` is in the same format of the `subscribe` property in the
|
||||
[SUBSCRIBE](https://github.com/mqttjs/mqtt-packet#subscribe) packet:
|
||||
|
||||
```js
|
||||
[{
|
||||
topic: 'hello/world',
|
||||
qos: 1,
|
||||
}, {
|
||||
topic: 'hello/#',
|
||||
qos: 2,
|
||||
}]
|
||||
```
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="countOffline"></a>
|
||||
### instance.countOffline(cb(err, numOfSubscriptions, numOfClients))
|
||||
|
||||
Returns the number of offline subscriptions and the number of offline
|
||||
clients.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="subscriptionsByTopic"></a>
|
||||
### instance.subscriptionsByTopic(pattern, callback(err, subscriptions))
|
||||
|
||||
Returns all the offline subscriptions matching the given pattern. Called when
|
||||
a PUBLISH with `qos: 1` or `qos: 2` is received.
|
||||
|
||||
The subscriptions are in the format:
|
||||
|
||||
```js
|
||||
{
|
||||
clientId: client.id,
|
||||
topic: sub.topic,
|
||||
qos: sub.qos
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="cleanSubscriptions"></a>
|
||||
### instance.cleanSubscriptions(client, callback(err, client))
|
||||
|
||||
Removes all offline subscriptions for a given client.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="outgoingEnqueue"></a>
|
||||
### instance.outgoingEnqueue(subscription, packet, callback(err))
|
||||
|
||||
Enqueue a potentially offline delivery. `subscription` is one of the
|
||||
objects returned by [`subscriptionsByTopic`](#subscriptionsByTopic). Deprecated.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="outgoingEnqueueCombi"></a>
|
||||
### instance.outgoingEnqueueCombi(subscriptions, packet, callback(err))
|
||||
|
||||
Enqueue a potentially offline delivery. `subscriptions` is the whole subscriptions
|
||||
objects returned by [`subscriptionsByTopic`](#subscriptionsByTopic).
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="outgoingUpdate"></a>
|
||||
### instance.outgoingUpdate(client, packet, callback(err))
|
||||
|
||||
Called before a (potentially) offline packet is delivered, the caller
|
||||
should update the `packet.messageId` before updating.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="outgoingClearMessageId"></a>
|
||||
### instance.outgoingClearMessageId(client, packet, callback(err, packet))
|
||||
|
||||
Removes a packet with the given `messageId` (passing a PUBACK is ok)
|
||||
from the persistence. Passes back original packet to the callback.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="outgoingStream"></a>
|
||||
### instance.outgoingStream(client)
|
||||
|
||||
Return a stream that will load all offline messages for the given client asynchronously.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="incomingStorePacket"></a>
|
||||
### instance.incomingStorePacket(client, packet, cb(err, packet))
|
||||
|
||||
Store an incoming packet for the given client. Used for QoS 2.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="incomingGetPacket"></a>
|
||||
### instance.incomingGetPacket(client, packet, cb(err, packet))
|
||||
|
||||
Retrieve an incoming packet with the same `messageId` for the given client. Used for QoS 2.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="incomingDelPacket"></a>
|
||||
### instance.incomingDelPacket(client, packet, cb(err, packet))
|
||||
|
||||
Deletes incoming packet with the same `messageId` for the given client. Used for QoS 2.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="putWill"></a>
|
||||
### instance.putWill(client, packet, cb(err))
|
||||
|
||||
Stores the will of a client. Used to support multi-broker environments
|
||||
and to not lose wills in case of a crash.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="getWill"></a>
|
||||
### instance.getWill(client, packet, cb(err))
|
||||
|
||||
Retrieves the will of a client. Used to support multi-broker environments
|
||||
and to not lose wills in case of a crash.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="delWill"></a>
|
||||
### instance.delWill(client, packet, cb(err))
|
||||
|
||||
Removes the will of a client. Used to support multi-broker environments
|
||||
and to not lose wills in case of a crash.
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="streamWill"></a>
|
||||
### instance.streamWill(brokers)
|
||||
|
||||
Streams all the wills for the given brokers. The brokers are in the
|
||||
format:
|
||||
|
||||
```js
|
||||
{
|
||||
mybroker: {
|
||||
brokerId: 'mybroker'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------
|
||||
<a name="getCLientList"></a>
|
||||
### instance.getClientList(topic)
|
||||
|
||||
Returns a stream which has all the clientIds subscribed to the
|
||||
specified topic
|
||||
|
||||
<a name="destroy"></a>
|
||||
### instance.destroy(cb(err))
|
||||
|
||||
Destroy current persistence. Use callback `cb(err)` to catch errors if any
|
||||
|
||||
<a name="implement"></a>
|
||||
## Implement another persistence
|
||||
|
||||
A persistence needs to pass all tests defined in
|
||||
[./abstract.js](./abstract.js). You can import and use that test suite
|
||||
in the following manner:
|
||||
|
||||
```js
|
||||
const test = require('tape').test
|
||||
const myperst = require('./')
|
||||
const abs = require('aedes-persistence/abstract')
|
||||
|
||||
abs({
|
||||
test: test,
|
||||
persistence: myperst
|
||||
})
|
||||
```
|
||||
|
||||
If you require some async stuff before returning, a callback is also
|
||||
supported:
|
||||
|
||||
```js
|
||||
const test = require('tape').test
|
||||
const myperst = require('./')
|
||||
const abs = require('aedes-persistence/abstract')
|
||||
const clean = require('./clean') // invented module
|
||||
|
||||
abs({
|
||||
test: test,
|
||||
buildEmitter: require('mymqemitter'), // optional
|
||||
persistence: function build (cb) {
|
||||
clean(function (err) {
|
||||
cb(err, myperst())
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Collaborators
|
||||
|
||||
* [__Gnought__](https://github.com/gnought)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+1596
File diff suppressed because it is too large
Load Diff
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "aedes-persistence",
|
||||
"version": "9.1.2",
|
||||
"description": "The spec for an Aedes persistence, with abstract tests and a fast in-memory implementation.",
|
||||
"main": "persistence.js",
|
||||
"types": "types/index.d.ts",
|
||||
"scripts": {
|
||||
"lint": "standard --verbose | snazzy",
|
||||
"lint-fix": "standard --fix",
|
||||
"unit": "tape test.js | faucet",
|
||||
"test:typescript": "tsd",
|
||||
"test": "npm run lint && npm run unit && tsd",
|
||||
"coverage": "nyc --reporter=lcov tape test.js",
|
||||
"test:ci": "npm run lint && npm run coverage && npm run test:typescript",
|
||||
"license-checker": "license-checker --production --onlyAllow='MIT;ISC;BSD-3-Clause;BSD-2-Clause'",
|
||||
"release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it --disable-metrics"
|
||||
},
|
||||
"release-it": {
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"git": {
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": [
|
||||
"npm run test"
|
||||
]
|
||||
},
|
||||
"npm": {
|
||||
"publish": true
|
||||
}
|
||||
},
|
||||
"pre-commit": [
|
||||
"test"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/moscajs/aedes-persistence.git"
|
||||
},
|
||||
"keywords": [
|
||||
"mqtt",
|
||||
"broker",
|
||||
"persistence",
|
||||
"aedes"
|
||||
],
|
||||
"author": "Matteo Collina <hello@matteocollina.com>",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Gnought",
|
||||
"url": "https://github.com/gnought"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/moscajs/aedes-persistence/issues"
|
||||
},
|
||||
"homepage": "https://github.com/moscajs/aedes-persistence#readme",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.29",
|
||||
"aedes": "^0.46.3",
|
||||
"faucet": "0.0.1",
|
||||
"license-checker": "^25.0.1",
|
||||
"mqemitter": "^4.5.0",
|
||||
"nyc": "^15.1.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"release-it": "^14.14.2",
|
||||
"snazzy": "^9.0.0",
|
||||
"standard": "^17.0.0",
|
||||
"tape": "^5.5.3",
|
||||
"tsd": "^0.20.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"aedes-packet": "^3.0.0",
|
||||
"qlobber": "^7.0.0"
|
||||
}
|
||||
}
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
const { Readable } = require('stream')
|
||||
const QlobberSub = require('qlobber/aedes/qlobber-sub')
|
||||
const { QlobberTrue } = require('qlobber')
|
||||
const Packet = require('aedes-packet')
|
||||
const QlobberOpts = {
|
||||
wildcard_one: '+',
|
||||
wildcard_some: '#',
|
||||
separator: '/'
|
||||
}
|
||||
const CREATE_ON_EMPTY = true
|
||||
|
||||
function * multiIterables (iterables) {
|
||||
for (const iter of iterables) {
|
||||
yield * iter
|
||||
}
|
||||
}
|
||||
|
||||
function * retainedMessagesByPattern (retained, pattern) {
|
||||
const qlobber = new QlobberTrue(QlobberOpts)
|
||||
qlobber.add(pattern)
|
||||
|
||||
for (const [topic, packet] of retained) {
|
||||
if (qlobber.test(topic)) {
|
||||
yield packet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function * willsByBrokers (wills, brokers) {
|
||||
for (const will of wills.values()) {
|
||||
if (!brokers[will.brokerId]) {
|
||||
yield will
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function * clientListbyTopic (subscriptions, topic) {
|
||||
for (const [clientId, topicMap] of subscriptions) {
|
||||
if (topicMap.has(topic)) {
|
||||
yield clientId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryPersistence {
|
||||
// private class members start with #
|
||||
#retained
|
||||
#subscriptions
|
||||
#outgoing
|
||||
#incoming
|
||||
#wills
|
||||
#clientsCount
|
||||
#trie
|
||||
|
||||
constructor () {
|
||||
// using Maps for convenience and security (risk on prototype polution)
|
||||
// Map ( topic -> packet )
|
||||
this.#retained = new Map()
|
||||
// Map ( clientId -> Map( topic -> { qos, rh, rap, nl } ))
|
||||
this.#subscriptions = new Map()
|
||||
// Map ( clientId > [ packet ] }
|
||||
this.#outgoing = new Map()
|
||||
// Map ( clientId -> { packetId -> Packet } )
|
||||
this.#incoming = new Map()
|
||||
// Map( clientId -> will )
|
||||
this.#wills = new Map()
|
||||
this.#clientsCount = 0
|
||||
this.#trie = new QlobberSub(QlobberOpts)
|
||||
}
|
||||
|
||||
storeRetained (pkt, cb) {
|
||||
const packet = Object.assign({}, pkt)
|
||||
if (packet.payload.length === 0) {
|
||||
this.#retained.delete(packet.topic)
|
||||
} else {
|
||||
this.#retained.set(packet.topic, packet)
|
||||
}
|
||||
cb(null)
|
||||
}
|
||||
|
||||
createRetainedStreamCombi (patterns) {
|
||||
const iterables = patterns.map((p) => {
|
||||
return retainedMessagesByPattern(this.#retained, p)
|
||||
})
|
||||
return Readable.from(multiIterables(iterables))
|
||||
}
|
||||
|
||||
createRetainedStream (pattern) {
|
||||
return Readable.from(retainedMessagesByPattern(this.#retained, pattern))
|
||||
}
|
||||
|
||||
addSubscriptions (client, subs, cb) {
|
||||
let stored = this.#subscriptions.get(client.id)
|
||||
const trie = this.#trie
|
||||
|
||||
if (!stored) {
|
||||
stored = new Map()
|
||||
this.#subscriptions.set(client.id, stored)
|
||||
this.#clientsCount++
|
||||
}
|
||||
|
||||
for (const sub of subs) {
|
||||
const storedSub = stored.get(sub.topic)
|
||||
if (sub.qos > 0) {
|
||||
trie.add(sub.topic, {
|
||||
clientId: client.id,
|
||||
topic: sub.topic,
|
||||
qos: sub.qos,
|
||||
rh: sub.rh,
|
||||
rap: sub.rap,
|
||||
nl: sub.nl
|
||||
})
|
||||
} else if (storedSub?.qos > 0) {
|
||||
trie.remove(sub.topic, {
|
||||
clientId: client.id,
|
||||
topic: sub.topic
|
||||
})
|
||||
}
|
||||
stored.set(sub.topic, { qos: sub.qos, rh: sub.rh, rap: sub.rap, nl: sub.nl })
|
||||
}
|
||||
|
||||
cb(null, client)
|
||||
}
|
||||
|
||||
removeSubscriptions (client, subs, cb) {
|
||||
const stored = this.#subscriptions.get(client.id)
|
||||
const trie = this.#trie
|
||||
|
||||
if (stored) {
|
||||
for (const topic of subs) {
|
||||
const storedSub = stored.get(topic)
|
||||
if (storedSub !== undefined) {
|
||||
if (storedSub.qos > 0) {
|
||||
trie.remove(topic, { clientId: client.id, topic })
|
||||
}
|
||||
stored.delete(topic)
|
||||
}
|
||||
}
|
||||
|
||||
if (stored.size === 0) {
|
||||
this.#clientsCount--
|
||||
this.#subscriptions.delete(client.id)
|
||||
}
|
||||
}
|
||||
|
||||
cb(null, client)
|
||||
}
|
||||
|
||||
subscriptionsByClient (client, cb) {
|
||||
let subs = null
|
||||
const stored = this.#subscriptions.get(client.id)
|
||||
if (stored) {
|
||||
subs = []
|
||||
for (const [topic, storedSub] of stored) {
|
||||
subs.push({ topic, ...storedSub })
|
||||
}
|
||||
}
|
||||
cb(null, subs, client)
|
||||
}
|
||||
|
||||
countOffline (cb) {
|
||||
return cb(null, this.#trie.subscriptionsCount, this.#clientsCount)
|
||||
}
|
||||
|
||||
subscriptionsByTopic (pattern, cb) {
|
||||
cb(null, this.#trie.match(pattern))
|
||||
}
|
||||
|
||||
cleanSubscriptions (client, cb) {
|
||||
const trie = this.#trie
|
||||
const stored = this.#subscriptions.get(client.id)
|
||||
|
||||
if (stored) {
|
||||
for (const [topic, storedSub] of stored) {
|
||||
if (storedSub.qos > 0) {
|
||||
trie.remove(topic, { clientId: client.id, topic })
|
||||
}
|
||||
}
|
||||
|
||||
this.#clientsCount--
|
||||
this.#subscriptions.delete(client.id)
|
||||
}
|
||||
|
||||
cb(null, client)
|
||||
}
|
||||
|
||||
#outgoingEnqueuePerSub (sub, packet) {
|
||||
const id = sub.clientId
|
||||
const queue = getMapRef(this.#outgoing, id, [], CREATE_ON_EMPTY)
|
||||
queue[queue.length] = new Packet(packet)
|
||||
}
|
||||
|
||||
outgoingEnqueue (sub, packet, cb) {
|
||||
this.#outgoingEnqueuePerSub(sub, packet)
|
||||
process.nextTick(cb)
|
||||
}
|
||||
|
||||
outgoingEnqueueCombi (subs, packet, cb) {
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
this.#outgoingEnqueuePerSub(subs[i], packet)
|
||||
}
|
||||
process.nextTick(cb)
|
||||
}
|
||||
|
||||
outgoingUpdate (client, packet, cb) {
|
||||
const outgoing = getMapRef(this.#outgoing, client.id, [], CREATE_ON_EMPTY)
|
||||
|
||||
let temp
|
||||
for (let i = 0; i < outgoing.length; i++) {
|
||||
temp = outgoing[i]
|
||||
if (temp.brokerId === packet.brokerId) {
|
||||
if (temp.brokerCounter === packet.brokerCounter) {
|
||||
temp.messageId = packet.messageId
|
||||
return cb(null, client, packet)
|
||||
}
|
||||
/*
|
||||
Maximum of messageId (packet identifier) is 65535 and will be rotated,
|
||||
brokerCounter is to ensure the packet identifier be unique.
|
||||
The for loop is going to search which packet messageId should be updated
|
||||
in the #outgoing queue.
|
||||
If there is a case that brokerCounter is different but messageId is same,
|
||||
we need to let the loop keep searching
|
||||
*/
|
||||
} else if (temp.messageId === packet.messageId) {
|
||||
outgoing[i] = packet
|
||||
return cb(null, client, packet)
|
||||
}
|
||||
}
|
||||
|
||||
cb(new Error('no such packet'), client, packet)
|
||||
}
|
||||
|
||||
outgoingClearMessageId (client, packet, cb) {
|
||||
const outgoing = getMapRef(this.#outgoing, client.id, [], CREATE_ON_EMPTY)
|
||||
|
||||
let temp
|
||||
for (let i = 0; i < outgoing.length; i++) {
|
||||
temp = outgoing[i]
|
||||
if (temp.messageId === packet.messageId) {
|
||||
outgoing.splice(i, 1)
|
||||
return cb(null, temp)
|
||||
}
|
||||
}
|
||||
|
||||
cb()
|
||||
}
|
||||
|
||||
outgoingStream (client) {
|
||||
// shallow clone the outgoing queue for this client to avoid race conditions
|
||||
const outgoing = [].concat(getMapRef(this.#outgoing, client.id, []))
|
||||
return Readable.from(outgoing)
|
||||
}
|
||||
|
||||
incomingStorePacket (client, packet, cb) {
|
||||
const id = client.id
|
||||
const store = getMapRef(this.#incoming, id, {}, CREATE_ON_EMPTY)
|
||||
|
||||
store[packet.messageId] = new Packet(packet)
|
||||
store[packet.messageId].messageId = packet.messageId
|
||||
|
||||
cb(null)
|
||||
}
|
||||
|
||||
incomingGetPacket (client, packet, cb) {
|
||||
const id = client.id
|
||||
const store = getMapRef(this.#incoming, id, {})
|
||||
let err = null
|
||||
|
||||
this.#incoming.set(id, store)
|
||||
|
||||
if (!store[packet.messageId]) {
|
||||
err = new Error('no such packet')
|
||||
}
|
||||
|
||||
cb(err, store[packet.messageId])
|
||||
}
|
||||
|
||||
incomingDelPacket (client, packet, cb) {
|
||||
const id = client.id
|
||||
const store = getMapRef(this.#incoming, id, {})
|
||||
const toDelete = store[packet.messageId]
|
||||
let err = null
|
||||
|
||||
if (!toDelete) {
|
||||
err = new Error('no such packet')
|
||||
} else {
|
||||
delete store[packet.messageId]
|
||||
}
|
||||
|
||||
cb(err)
|
||||
}
|
||||
|
||||
putWill (client, packet, cb) {
|
||||
packet.brokerId = this.broker.id
|
||||
packet.clientId = client.id
|
||||
this.#wills.set(client.id, packet)
|
||||
cb(null, client)
|
||||
}
|
||||
|
||||
getWill (client, cb) {
|
||||
cb(null, this.#wills.get(client.id), client)
|
||||
}
|
||||
|
||||
delWill (client, cb) {
|
||||
const will = this.#wills.get(client.id)
|
||||
this.#wills.delete(client.id)
|
||||
cb(null, will, client)
|
||||
}
|
||||
|
||||
streamWill (brokers = {}) {
|
||||
return Readable.from(willsByBrokers(this.#wills, brokers))
|
||||
}
|
||||
|
||||
getClientList (topic) {
|
||||
return Readable.from(clientListbyTopic(this.#subscriptions, topic))
|
||||
}
|
||||
|
||||
destroy (cb) {
|
||||
this.#retained = null
|
||||
if (cb) {
|
||||
cb(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMapRef (map, key, ifEmpty, createOnEmpty = false) {
|
||||
const value = map.get(key)
|
||||
if (value === undefined && createOnEmpty) {
|
||||
map.set(key, ifEmpty)
|
||||
}
|
||||
return value || ifEmpty
|
||||
}
|
||||
|
||||
module.exports = () => { return new MemoryPersistence() }
|
||||
module.exports.Packet = Packet
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
'use strict'
|
||||
|
||||
const test = require('tape').test
|
||||
const memory = require('./')
|
||||
const abs = require('./abstract')
|
||||
|
||||
abs({
|
||||
test,
|
||||
persistence: memory
|
||||
})
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
import type { Brokers, Client, Subscription } from "aedes";
|
||||
import type { AedesPacket } from "aedes-packet";
|
||||
import type { Readable } from "stream";
|
||||
|
||||
export type { AedesPacket as Packet } from "aedes-packet";
|
||||
|
||||
type ClientId = Client["id"];
|
||||
type MessageId = AedesPacket["messageId"];
|
||||
type Topic = Subscription["topic"];
|
||||
type TopicPattern = Subscription["topic"];
|
||||
type QoS = Subscription["qos"];
|
||||
|
||||
type Incoming = {
|
||||
// tsc accepts:
|
||||
// [messageId: MessageId]: AedesPacket;
|
||||
// Workaround for tsd 0.20:
|
||||
[messageId: number]: AedesPacket;
|
||||
};
|
||||
|
||||
export interface AedesPersistenceSubscription {
|
||||
clientId: ClientId;
|
||||
topic: Topic;
|
||||
qos?: QoS;
|
||||
}
|
||||
|
||||
export type CallbackError = Error | null | undefined;
|
||||
|
||||
export type WillPacket = AedesPacket & { [key: string]: any };
|
||||
|
||||
export interface AedesPersistence {
|
||||
storeRetained: (
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
createRetainedStream: (pattern: TopicPattern) => Readable;
|
||||
|
||||
createRetainedStreamCombi: (patterns: TopicPattern[]) => Readable;
|
||||
|
||||
addSubscriptions: (
|
||||
client: Client,
|
||||
subs: Subscription[],
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
removeSubscriptions: (
|
||||
client: Client,
|
||||
subs: Subscription[],
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
subscriptionsByClient: (
|
||||
client: Client,
|
||||
cb: (
|
||||
error: CallbackError,
|
||||
subs: { topic: Topic; qos: QoS }[],
|
||||
client: Client,
|
||||
) => void,
|
||||
) => void;
|
||||
|
||||
countOffline: (
|
||||
cb: (
|
||||
error: CallbackError,
|
||||
subscriptionsCount: number,
|
||||
clientsCount: number,
|
||||
) => void,
|
||||
) => void;
|
||||
|
||||
subscriptionsByTopic: (
|
||||
pattern: TopicPattern,
|
||||
cb: (error: CallbackError, subs: AedesPersistenceSubscription[]) => void,
|
||||
) => void;
|
||||
|
||||
cleanSubscriptions: (
|
||||
client: Client,
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
outgoingEnqueue: (
|
||||
sub: { clientId: ClientId },
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
outgoingEnqueueCombi: (
|
||||
subs: { clientId: ClientId }[],
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
outgoingUpdate: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError, client: Client, packet: AedesPacket) => void,
|
||||
) => void;
|
||||
|
||||
outgoingClearMessageId: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error?: CallbackError, packet?: AedesPacket) => void,
|
||||
) => void;
|
||||
|
||||
outgoingStream: (client: Client) => Readable;
|
||||
|
||||
incomingStorePacket: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
incomingGetPacket: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError, packet: AedesPacket) => void,
|
||||
) => void;
|
||||
|
||||
incomingDelPacket: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
putWill: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
getWill: (
|
||||
client: Client,
|
||||
cb: (error: CallbackError, will: WillPacket, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
delWill: (
|
||||
client: Client,
|
||||
cb: (error: CallbackError, will: WillPacket, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
streamWill: (brokers: Brokers) => Readable;
|
||||
|
||||
getClientList: (topic: string) => Readable;
|
||||
|
||||
destroy: (cb?: (error: CallbackError) => void) => void;
|
||||
}
|
||||
|
||||
export class AedesMemoryPersistence implements AedesPersistence {
|
||||
#retained: Map<Topic, AedesPacket>;
|
||||
#subscriptions: Map<
|
||||
ClientId,
|
||||
Map<
|
||||
Topic,
|
||||
QoS
|
||||
>
|
||||
>;
|
||||
#clientsCount: number;
|
||||
#trie: any;
|
||||
#outgoing: Map<ClientId, AedesPacket[]>;
|
||||
#incoming: Map<ClientId, Incoming>;
|
||||
#wills: Map<ClientId, WillPacket>;
|
||||
|
||||
constructor();
|
||||
|
||||
storeRetained: (
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
createRetainedStream: (pattern: TopicPattern) => Readable;
|
||||
|
||||
createRetainedStreamCombi: (patterns: TopicPattern[]) => Readable;
|
||||
|
||||
addSubscriptions: (
|
||||
client: Client,
|
||||
subs: Subscription[],
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
removeSubscriptions: (
|
||||
client: Client,
|
||||
subs: Subscription[],
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
subscriptionsByClient: (
|
||||
client: Client,
|
||||
cb: (
|
||||
error: CallbackError,
|
||||
subs: { topic: string; qos: QoS }[],
|
||||
client: Client,
|
||||
) => void,
|
||||
) => void;
|
||||
|
||||
countOffline: (
|
||||
cb: (
|
||||
error: CallbackError,
|
||||
subscriptionsCount: number,
|
||||
clientsCount: number,
|
||||
) => void,
|
||||
) => void;
|
||||
|
||||
subscriptionsByTopic: (
|
||||
pattern: string,
|
||||
cb: (error: CallbackError, subs: AedesPersistenceSubscription[]) => void,
|
||||
) => void;
|
||||
|
||||
cleanSubscriptions: (
|
||||
client: Client,
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
#outgoingEnqueuePerSub: (
|
||||
sub: { clientId: ClientId },
|
||||
packet: AedesPacket,
|
||||
) => void;
|
||||
|
||||
outgoingEnqueue: (
|
||||
sub: { clientId: ClientId },
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
outgoingEnqueueCombi: (
|
||||
sub: { clientId: ClientId }[],
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
outgoingUpdate: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError, client: Client, packet: AedesPacket) => void,
|
||||
) => void;
|
||||
|
||||
outgoingClearMessageId: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error?: CallbackError, packet?: AedesPacket) => void,
|
||||
) => void;
|
||||
|
||||
outgoingStream: (client: Client) => Readable;
|
||||
|
||||
incomingStorePacket: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
incomingGetPacket: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError, packet: AedesPacket) => void,
|
||||
) => void;
|
||||
|
||||
incomingDelPacket: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError) => void,
|
||||
) => void;
|
||||
|
||||
putWill: (
|
||||
client: Client,
|
||||
packet: AedesPacket,
|
||||
cb: (error: CallbackError, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
getWill: (
|
||||
client: Client,
|
||||
cb: (error: CallbackError, will: WillPacket, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
delWill: (
|
||||
client: Client,
|
||||
cb: (error: CallbackError, will: WillPacket, client: Client) => void,
|
||||
) => void;
|
||||
|
||||
streamWill: (brokers: Brokers) => Readable;
|
||||
|
||||
getClientList: (topic: string) => Readable;
|
||||
|
||||
destroy: (cb?: (error: CallbackError) => void) => void;
|
||||
}
|
||||
|
||||
export default function aedesMemoryPersistence(): AedesMemoryPersistence;
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import type { Brokers, Client, Subscription } from 'aedes';
|
||||
import type { AedesPacket } from 'aedes-packet';
|
||||
import type { QoS } from 'mqtt-packet';
|
||||
import type { Readable } from 'stream';
|
||||
import { expectType } from 'tsd';
|
||||
import aedesMemoryPersistence, {
|
||||
AedesMemoryPersistence,
|
||||
AedesPersistenceSubscription,
|
||||
CallbackError,
|
||||
WillPacket,
|
||||
} from '.';
|
||||
|
||||
expectType<AedesMemoryPersistence>(aedesMemoryPersistence());
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().storeRetained(
|
||||
{
|
||||
brokerId: '',
|
||||
brokerCounter: 1,
|
||||
cmd: 'publish',
|
||||
qos: 0,
|
||||
dup: false,
|
||||
retain: false,
|
||||
topic: 'test',
|
||||
payload: 'test',
|
||||
},
|
||||
(err: CallbackError) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().addSubscriptions(
|
||||
{} as Client,
|
||||
[] as Subscription[],
|
||||
(err: CallbackError) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().removeSubscriptions(
|
||||
{} as Client,
|
||||
[] as Subscription[],
|
||||
(err: CallbackError) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().subscriptionsByClient(
|
||||
{} as Client,
|
||||
(
|
||||
error: CallbackError,
|
||||
subs: { topic: string; qos: QoS }[],
|
||||
client: Client
|
||||
) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().countOffline(
|
||||
(
|
||||
error: CallbackError,
|
||||
subscriptionsCount: number,
|
||||
clientsCount: number
|
||||
) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().subscriptionsByTopic(
|
||||
'pattern',
|
||||
(error: CallbackError, subs: AedesPersistenceSubscription[]) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().cleanSubscriptions(
|
||||
{} as Client,
|
||||
(error: CallbackError, client: Client) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().outgoingEnqueue(
|
||||
{ clientId: '' },
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().outgoingEnqueueCombi(
|
||||
[{ clientId: '' }],
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().outgoingUpdate(
|
||||
{} as Client,
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError, client: Client, packet: AedesPacket) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().outgoingClearMessageId(
|
||||
{} as Client,
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError, packet?: AedesPacket) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<Readable>(aedesMemoryPersistence().outgoingStream({} as Client));
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().incomingStorePacket(
|
||||
{} as Client,
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().incomingGetPacket(
|
||||
{} as Client,
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError, packet: AedesPacket) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().incomingDelPacket(
|
||||
{} as Client,
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().putWill(
|
||||
{} as Client,
|
||||
{} as AedesPacket,
|
||||
(error: CallbackError, client: Client) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().getWill(
|
||||
{} as Client,
|
||||
(error: CallbackError, will: WillPacket, client: Client) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<void>(
|
||||
aedesMemoryPersistence().delWill(
|
||||
{} as Client,
|
||||
(error: CallbackError, will: WillPacket, client: Client) => {}
|
||||
)
|
||||
);
|
||||
|
||||
expectType<Readable>(aedesMemoryPersistence().streamWill({} as Brokers));
|
||||
|
||||
expectType<Readable>(aedesMemoryPersistence().getClientList('topic'));
|
||||
|
||||
expectType<void>(aedesMemoryPersistence().destroy());
|
||||
|
||||
expectType<void>(aedesMemoryPersistence().destroy(() => {}));
|
||||
Reference in New Issue
Block a user