/** * @module * Volatile in-memory implementation of StorageProvider. * Completely decoupled from MQTT protocol logic. */ import type { ClientId, PacketId, Topic, TopicFilter } from "../deps.ts"; import type { ClientRegistrationResult, ClientSubscription, ExtPublishPacket, ShareName, } from "../persistence.ts"; import type { StorageProvider, StoredSubscription } from "../storage.ts"; import { PacketDirection } from "../storage.ts"; import { getOrInsert, joinTopicFilter, topicFilterToRegExp } from "../deps.ts"; type pendingTableEntry = { seqId: number; packet: ExtPublishPacket; createdAtMs: number; }; export class MemoryStorage implements StorageProvider { private seqId = 1; private sessionTable = new Map(); private subscriptionTable = new Map< ClientId, Map >(); private pendingIncomingTable = new Map< ClientId, Map >(); private pendingOutgoingTable = new Map< ClientId, Map >(); private pendingAckOutgoingTable = new Map>(); private retainedTable = new Map(); initialize(): Promise { return Promise.resolve(); } // --- Sessions --- saveSession( clientId: ClientId, session: ClientRegistrationResult, ): Promise { this.sessionTable.set(clientId, session); return Promise.resolve(); } getSession(clientId: ClientId): Promise { return Promise.resolve(this.sessionTable.get(clientId) ?? null); } deleteSession(clientId: ClientId): Promise { this.sessionTable.delete(clientId); this.pendingIncomingTable.delete(clientId); this.pendingOutgoingTable.delete(clientId); this.pendingAckOutgoingTable.delete(clientId); this.subscriptionTable.delete(clientId); return Promise.resolve(); } async *listAllSessions(): AsyncIterableIterator< { clientId: ClientId; session: ClientRegistrationResult } > { for (const [clientId, session] of this.sessionTable.entries()) { yield { clientId, session }; } } // --- Subscriptions --- saveSubscription(clientId: ClientId, sub: ClientSubscription): Promise { const key = joinTopicFilter(sub.topicFilter, sub.shareName); const clientSubs = getOrInsert(this.subscriptionTable, clientId, new Map()); clientSubs.set(key, sub); return Promise.resolve(); } deleteSubscription( clientId: ClientId, topicFilter: TopicFilter, shareName: ShareName, ): Promise { const key = joinTopicFilter(topicFilter, shareName); this.subscriptionTable.get(clientId)?.delete(key); return Promise.resolve(); } async *listSubscriptions( clientId: ClientId, ): AsyncIterableIterator { const clientSubs = this.subscriptionTable.get(clientId); if (clientSubs) { for (const sub of clientSubs.values()) { yield sub; } } } async *listAllSubscriptions(): AsyncIterableIterator { for (const [clientId, clientSubs] of this.subscriptionTable.entries()) { for (const sub of clientSubs.values()) { yield { ...sub, clientId }; } } } // --- Pending Packets (Incoming & Outgoing) --- private getPacketTable(clientId: ClientId, direction: PacketDirection) { const table = direction === PacketDirection.Incoming ? this.pendingIncomingTable : this.pendingOutgoingTable; return getOrInsert(table, clientId, new Map()); } savePendingPacket( clientId: ClientId, direction: PacketDirection, packet: ExtPublishPacket, ): Promise { const createdAtMs = Date.now(); if (packet.id !== undefined) { this.getPacketTable(clientId, direction).set(packet.id, { seqId: this.seqId++, packet, createdAtMs, }); } return Promise.resolve(); } getPendingPacket( clientId: ClientId, direction: PacketDirection, packetId: PacketId, ): Promise { const entry = this.getPacketTable(clientId, direction).get(packetId); return Promise.resolve(entry?.packet ?? null); } updatePendingPacket( clientId: ClientId, direction: PacketDirection, packetId: PacketId, dup: boolean, ): Promise { const entry = this.getPacketTable(clientId, direction).get(packetId); if (entry !== undefined) { entry.packet.dup = dup; } return Promise.resolve(entry !== undefined); } deletePendingPacket( clientId: ClientId, direction: PacketDirection, packetId: PacketId, ): Promise { const deleted = this.getPacketTable(clientId, direction).delete(packetId); return Promise.resolve(deleted); } async *listPendingPackets( clientId: ClientId, direction: PacketDirection, ): AsyncIterableIterator { const queue = this.getPacketTable(clientId, direction); if (!queue) return; for (const entry of queue.values()) { yield entry.packet; } } // --- ACKs --- private getAckSet(clientId: ClientId): Set { return getOrInsert(this.pendingAckOutgoingTable, clientId, new Set()); } savePendingAck(clientId: ClientId, packetId: PacketId): Promise { this.getAckSet(clientId).add(packetId); return Promise.resolve(); } hasPendingAck(clientId: ClientId, packetId: PacketId): Promise { const hasAck = this.getAckSet(clientId).has(packetId); return Promise.resolve(hasAck); } deletePendingAck(clientId: ClientId, packetId: PacketId): Promise { const deleted = this.getAckSet(clientId).delete(packetId); return Promise.resolve(deleted); } async *listPendingAcks(clientId: ClientId): AsyncIterableIterator { const ackSet = this.getAckSet(clientId); for (const packetId of ackSet) { yield packetId; } } // --- Retained Messages --- saveRetained(topic: Topic, packet: ExtPublishPacket): Promise { this.retainedTable.set(topic, packet); return Promise.resolve(); } deleteRetained(topic: Topic): Promise { this.retainedTable.delete(topic); return Promise.resolve(); } async *listRetainedMatches( topicFilter: TopicFilter, ): AsyncIterableIterator { const regex = topicFilterToRegExp(topicFilter); for (const [topic, packet] of this.retainedTable.entries()) { if (regex.test(topic)) { yield packet; } } } }