feat: add redis persistence

This commit is contained in:
Morten Olsen
2025-10-17 00:21:47 +02:00
parent fca51c44af
commit 7eae7d96ec
10 changed files with 203 additions and 29 deletions

View File

@@ -48,7 +48,7 @@ const client = mqtt.connect('ws://localhost:8883/ws')
Backbone can be configured using environment variables:
| Variable | Description | Default |
| -------------------- | ---------------------------------------- | ----------- |
| -------------------- | ---------------------------------------- | ------------ |
| `ADMIN_TOKEN` | Admin token for API requests | `undefined` |
| `JWT_SECRET` | JWT signing secret for authentication | `undefined` |
| `K8S_ENABLED` | Enable Kubernetes operator mode | `false` |
@@ -66,6 +66,11 @@ Backbone can be configured using environment variables:
| `OIDC_ADMIN_GROUP` | JWT group for admins | `undefined` |
| `OIDC_WRITER_GROUP` | JWT group with publish access to queue | `undefined` |
| `OIDC_READER_GROUP` | JWT group with read-only access to queue | `undefined` |
| `REDIS_ENABLED` | Enable redis persistance | `false` |
| `REDIS_HOST` | Redis hostname | `'localhost` |
| `REDIS_PORT` | Redis port | `6379` |
| `REDIS_PASSWORD` | Redis password | `undefined` |
| `REDIS_DB` | Redis database | `0` |
### Example Configuration

View File

@@ -1,12 +1,16 @@
name: backbone
services:
app:
build:
context: .
environment:
HTTP_ENABLED: 'true'
TCP_ENABLED: 'true'
TOKEN_SECRET: 'test'
redis:
image: redis
ports:
- 1883:1883
- 8883:8883
- 6379:6379
# app:
# build:
# context: .
# environment:
# HTTP_ENABLED: 'true'
# TCP_ENABLED: 'true'
# TOKEN_SECRET: 'test'
# ports:
# - 1883:1883
# - 8883:8883

View File

@@ -49,6 +49,7 @@
"aedes": "^0.51.3",
"aedes-packet": "^3.0.0",
"aedes-persistence": "^10.2.2",
"aedes-persistence-redis": "^11.2.2",
"ajv": "^8.17.1",
"better-sqlite3": "^12.4.1",
"fastify": "^5.6.1",

112
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
aedes-persistence:
specifier: ^10.2.2
version: 10.2.2
aedes-persistence-redis:
specifier: ^11.2.2
version: 11.2.2
ajv:
specifier: ^8.17.1
version: 8.17.1
@@ -394,6 +397,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@ioredis/commands@1.4.0':
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -911,6 +917,10 @@ packages:
resolution: {integrity: sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==}
engines: {node: '>=14'}
aedes-persistence-redis@11.2.2:
resolution: {integrity: sha512-kJRHG6FiykSdkniWJYDNYW892buif3D8iu5FrkgXsVuoF7A7rX3I1n4w6BV4Ocvz8thFaKBjWe3EAtOzOdEPIA==}
engines: {node: '>=20'}
aedes-persistence@10.2.2:
resolution: {integrity: sha512-lbiViUGXxOyUakU01xvD758LGOLJ8T7PzpT3q2q8enMSMFSigy0dYQc0b7bgHcz96AYJjEraQSfVpXH7L546TA==}
engines: {node: '>=20'}
@@ -1204,6 +1214,10 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1318,6 +1332,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -1506,6 +1524,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
event-lite@0.1.3:
resolution: {integrity: sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
@@ -1766,6 +1787,9 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hashlru@2.3.0:
resolution: {integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -1823,6 +1847,9 @@ packages:
resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
int64-buffer@0.1.10:
resolution: {integrity: sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==}
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -1831,6 +1858,10 @@ packages:
resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==}
engines: {node: '>= 0.10'}
ioredis@5.8.1:
resolution: {integrity: sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==}
engines: {node: '>=12.22.0'}
ip-address@10.0.1:
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
engines: {node: '>= 12'}
@@ -1962,6 +1993,9 @@ packages:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -2118,9 +2152,15 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
@@ -2250,6 +2290,10 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
msgpack-lite@0.1.26:
resolution: {integrity: sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==}
hasBin: true
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -2602,6 +2646,14 @@ packages:
resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
engines: {node: '>= 10.13.0'}
redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -2799,6 +2851,9 @@ packages:
stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
@@ -3482,6 +3537,8 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@ioredis/commands@1.4.0': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -4137,6 +4194,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
aedes-persistence-redis@11.2.2:
dependencies:
aedes-persistence: 10.2.2
hashlru: 2.3.0
ioredis: 5.8.1
msgpack-lite: 0.1.26
qlobber: 8.0.1
transitivePeerDependencies:
- supports-color
aedes-persistence@10.2.2:
dependencies:
aedes-packet: 3.0.0
@@ -4480,6 +4547,8 @@ snapshots:
clone@1.0.4: {}
cluster-key-slot@1.1.2: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -4580,6 +4649,8 @@ snapshots:
delayed-stream@1.0.0: {}
denque@2.1.0: {}
depd@2.0.0: {}
dequal@2.0.3: {}
@@ -4871,6 +4942,8 @@ snapshots:
esutils@2.0.3: {}
event-lite@0.1.3: {}
event-target-shim@5.0.1: {}
events-universal@1.0.1:
@@ -5151,6 +5224,8 @@ snapshots:
dependencies:
has-symbols: 1.1.0
hashlru@2.3.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -5198,6 +5273,8 @@ snapshots:
ini@3.0.1: {}
int64-buffer@0.1.10: {}
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -5206,6 +5283,20 @@ snapshots:
interpret@2.2.0: {}
ioredis@5.8.1:
dependencies:
'@ioredis/commands': 1.4.0
cluster-key-slot: 1.1.2
debug: 4.4.3
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
ip-address@10.0.1: {}
ipaddr.js@2.2.0: {}
@@ -5332,6 +5423,8 @@ snapshots:
is-windows@1.0.2: {}
isarray@1.0.0: {}
isarray@2.0.5: {}
isexe@2.0.0: {}
@@ -5495,8 +5588,12 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash.defaults@4.2.0: {}
lodash.includes@4.3.0: {}
lodash.isarguments@3.1.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
@@ -5629,6 +5726,13 @@ snapshots:
ms@2.1.3: {}
msgpack-lite@0.1.26:
dependencies:
event-lite: 0.1.3
ieee754: 1.2.1
int64-buffer: 0.1.10
isarray: 1.0.0
nanoid@3.3.11: {}
nanoid@5.1.5: {}
@@ -5967,6 +6071,12 @@ snapshots:
dependencies:
resolve: 1.22.10
redis-errors@1.2.0: {}
redis-parser@3.0.0:
dependencies:
redis-errors: 1.2.0
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -6204,6 +6314,8 @@ snapshots:
as-table: 1.0.55
get-source: 2.0.12
standard-as-callback@2.1.0: {}
statuses@2.0.1: {}
std-env@3.10.0: {}

View File

@@ -49,7 +49,7 @@ class Backbone {
const http = await this.server.getHttpServer();
http.listen({ port: this.config.http.port, host: '0.0.0.0' });
}
if (this.config.tcp) {
if (this.config.tcp.enabled) {
const tcp = this.server.getTcpServer();
tcp.listen(this.config.tcp.port);
}

View File

@@ -30,6 +30,21 @@ class Config {
};
}
public get redis() {
const enabled = process.env.REDIS_ENABLED === 'true';
const host = process.env.REDIS_HOST;
const port = process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 6379;
const password = process.env.REDIS_PASSWORD;
const db = process.env.REDIS_DB ? parseInt(process.env.REDIS_DB) : 0;
return {
enabled,
host,
port,
password,
db,
};
}
public get k8s() {
const enabled = process.env.K8S_ENABLED === 'true';
return {

View File

@@ -2,6 +2,8 @@ import { Backbone } from './backbone.ts';
process.env.JWT_SECRET = 'test';
process.env.ADMIN_TOKEN = 'admin';
process.env.API_ENABLED = 'true';
process.env.WS_ENABLED = 'true';
process.env.TCP_ENABLED = 'true';
const backbone = new Backbone();
await backbone.start();

12
src/global.d.ts vendored
View File

@@ -1,2 +1,14 @@
// import 'aedes-persistence';
// eslint-disable-next-line
declare type ExplicitAny = any;
declare module 'aedes-persistence-redis' {
// eslint-disable-next-line
export default any;
}
declare module 'aedes-persist' {
// eslint-disable-next-line
export default any;
}

View File

@@ -16,6 +16,8 @@ import fastify, { type FastifyInstance } from 'fastify';
import fastifyWebSocket from '@fastify/websocket';
import { createWebSocketStream } from 'ws';
import fastifySensible from '@fastify/sensible';
import redis from 'aedes-persistence-redis';
import memory from 'aedes-persistence';
import { api } from '../api/api.ts';
@@ -49,6 +51,7 @@ class MqttServer {
constructor(services: Services) {
this.#services = services;
this.#server = aedes.createBroker({
persistence: this.#getPersistance(),
authenticate: this.#authenticate,
authorizePublish: this.#authorizePublish,
authorizeSubscribe: this.#authorizeSubscribe,
@@ -61,6 +64,19 @@ class MqttServer {
return this.#server;
}
#getPersistance = () => {
const config = this.#services.get(Config);
if (config.redis.enabled) {
return redis({
host: config.redis.host,
port: config.redis.port,
password: config.redis.password,
db: config.redis.db,
});
}
return (memory as ExplicitAny)();
};
#authenticate: AuthenticateHandler = async (client, username, password, callback) => {
try {
if (!username || !password) {

View File

@@ -43,6 +43,13 @@ const createWorld = async (options: WorldOptions) => {
enabled: false,
port: 1883,
},
redis: {
enabled: false,
host: undefined,
password: undefined,
db: 0,
port: 0,
},
});
const accessTokens = backbone.services.get(JwtAuth);
backbone.sessionProvider.register('token', accessTokens);