import { Deferred, logger, MqttConn, MQTTLevel, PacketType, Timer, } from "./deps.ts"; import type { AnyPacket, AuthenticationResult, ConnectPacket, ConnNext, PacketId, ProtocolLevel, PublishPacket, ReasonCode, ReturnCodes, Store, SubscribePacket, UnsubscribePacket, } from "./deps.ts"; import { handlePacket } from "./handlers/handlePacket.ts"; import { ConnectionState } from "./ConnectionState.ts"; import type { Client } from "./client.ts"; import { assert } from "../utils/assert.ts"; /** * packets not allowed during authentication */ function blockedDuringAuthentication(pktType: PacketType) { return ( pktType === PacketType.publish || pktType === PacketType.subscribe || pktType === PacketType.unsubscribe ); } /** Possible results from authHandler */ export type AuthenticatedResult = { reasonCode: ReasonCode; reasonString?: string; authData?: Uint8Array; }; export class Context { mqttConn?: MqttConn; #connectionState: ConnectionState; protocolLevel: ProtocolLevel; pingTimer?: Timer; unresolvedConnect?: Deferred; unresolvedPublish: Map>; unresolvedSubscribe: Map>; unresolvedUnSubscribe: Map>; store: Store; #client: Client; constructor(store: Store, client: Client) { this.#client = client; this.store = store; this.#connectionState = ConnectionState.offline; this.protocolLevel = MQTTLevel.unknown; this.unresolvedPublish = new Map(); this.unresolvedSubscribe = new Map(); this.unresolvedUnSubscribe = new Map(); } get connectionState(): ConnectionState { return this.#connectionState; } set connectionState(state: ConnectionState) { this.#connectionState = state; switch (state) { case "connected": queueMicrotask(this.#client.onConnected); break; case "disconnected": queueMicrotask(this.#client.onDisconnected); break; } } authHandler( _ctx: Context, authMethod: string, authData: Uint8Array, ): AuthenticatedResult | Promise { return this.#client.onAuth(authMethod, authData); } setPingTimer(interval: number) { if (this.pingTimer) { this.pingTimer.clear(); } this.pingTimer = new Timer( this.sendPing.bind(this), interval * 1000, true, ); } async connect(packet: ConnectPacket) { this.connectionState = ConnectionState.connecting; if (packet.protocolLevel === MQTTLevel.unknown) { packet.protocolLevel = MQTTLevel.v4; } this.protocolLevel = packet.protocolLevel; await this.mqttConn?.send(packet); const keepAlive = packet.keepAlive || 0; if (keepAlive > 0) { this.setPingTimer(keepAlive); } } async disconnect() { if ( this.connectionState !== ConnectionState.connected && this.connectionState !== ConnectionState.authenticating ) { throw "Not connected"; } if (this.mqttConn) { this.connectionState = ConnectionState.disconnecting; await this.mqttConn.send({ type: PacketType.disconnect, protocolLevel: this.protocolLevel, }); this.mqttConn.close(); this.protocolLevel = MQTTLevel.unknown; } } async send(packet: AnyPacket) { logger.debug("client.ctx.send", this.#connectionState, { packet }); if (!this.mqttConn?.isClosed) { if (this.connectionState === ConnectionState.connected) { await this.mqttConn?.send(packet); this.pingTimer?.reset(); return; } if ( this.connectionState === ConnectionState.connecting && packet.type === PacketType.auth ) { await this.mqttConn?.send(packet); this.pingTimer?.reset(); return; } if ( this.connectionState === ConnectionState.authenticating && !blockedDuringAuthentication(packet.type) ) { await this.mqttConn?.send(packet); this.pingTimer?.reset(); return; } } logger.debug("not connected"); this.pingTimer?.clear(); } sendPing() { this.send({ type: PacketType.pingreq, protocolLevel: this.protocolLevel, }); } async handleConnection( conn: ConnNext, connectPacket: ConnectPacket, ): Promise { this.mqttConn = new MqttConn({ conn }); this.mqttConn.codecOpts.protocolLevel = connectPacket.protocolLevel; try { logger.debug("Send connect packet", connectPacket); await this.connect(connectPacket); logger.debug("Accepting packets"); for await (const packet of this.mqttConn) { await handlePacket(this, packet); } logger.debug("No more packets"); } catch (err) { assert( err instanceof Error, `Caught something that is not an instance of Error: ${err}`, ); queueMicrotask(() => this.#client.onError(err)); logger.debug("error", err); if (!this.mqttConn.isClosed) { this.mqttConn.close(); } } if (this.connectionState === ConnectionState.disconnecting) { return false; } return true; } close() { logger.debug("closing connection"); this.connectionState = ConnectionState.disconnected; this.pingTimer?.clear(); } async receivePublish(packet: PublishPacket) { await this.#client.onPacket(packet); } async publish(packet: PublishPacket): Promise { const qos = packet.qos || 0; if (qos === 0) { packet.id = 0; await this.send(packet); return; } packet.id = await this.store.nextId(); this.store.pendingOutgoing.set(packet.id, packet); const deferred = new Deferred(); this.unresolvedPublish.set(packet.id, deferred); await this.send(packet); return deferred.promise; } async subscribe(packet: SubscribePacket): Promise { packet.id = await this.store.nextId(); this.store.pendingOutgoing.set(packet.id, packet); const deferred = new Deferred(); this.unresolvedSubscribe.set(packet.id, deferred); await this.send(packet); return deferred.promise; } async unsubscribe(packet: UnsubscribePacket): Promise { packet.id = await this.store.nextId(); this.store.pendingOutgoing.set(packet.id, packet); const deferred = new Deferred(); this.unresolvedUnSubscribe.set(packet.id, deferred); await this.send(packet); return deferred.promise; } receivePuback(id: PacketId): boolean { const unresolvedMap = this.unresolvedPublish; if (unresolvedMap.has(id)) { const deferred = unresolvedMap.get(id); unresolvedMap.delete(id); deferred?.resolve(); return true; } return false; } // just an alias to clarify protocol flow receivePubcomp(id: PacketId): boolean { return this.receivePuback(id); } receiveSuback(id: PacketId, returnCodes: ReturnCodes): boolean { const unresolvedMap = this.unresolvedSubscribe; if (unresolvedMap.has(id)) { const deferred = unresolvedMap.get(id); unresolvedMap.delete(id); deferred?.resolve(returnCodes); return true; } return false; } receiveUnsuback(id: PacketId): boolean { const unresolvedMap = this.unresolvedUnSubscribe; if (unresolvedMap.has(id)) { const deferred = unresolvedMap.get(id); unresolvedMap.delete(id); deferred?.resolve(); return true; } return false; } }