Перейти к содержимому
Русский

MQTT

Это содержимое пока не доступно на вашем языке.

MqttActor integrates with MQTT brokers (Mosquitto, EMQ X, HiveMQ, AWS IoT Core). Supports both MQTT 3.1.1 and 5; topic wildcards (+, #); QoS 0/1/2; retained messages.

It is the MQTT counterpart to WebsocketClientActor: an abstract base class you extend. Declare subscriptions in the constructor, handle inbound traffic in onMessage, and publish with this.publish(...). It is still controllable from the outside via ref.tell(...).

import { ActorSystem, ActorSystemOptions, MqttActor, MqttOptions, type MqttMessage } from 'actor-ts';
type Reading = { sensor: string; celsius: number };
const actorSystemOptions = ActorSystemOptions.create().withConfig({
'actor-ts': { io: { broker: { mqtt: { brokerUrl: 'mqtt://localhost:1883' } } } },
});
class TemperatureHub extends MqttActor<Reading> {
constructor(options: MqttOptions) {
super(options.withQos(1).withClientId('temperature-hub'));
this.subscribe('sensors/+/temp');
this.subscribe('alerts/#', { qos: 2 });
}
override onMessage(message: MqttMessage<Reading>): void {
const { sensor, celsius } = message.payload.entity(); // decoded via the codec
this.log.info(`${sensor}: ${celsius}°C`);
this.publish(`ack/${sensor}`, 'ok'); // raw string
this.publish('rollup', { sensor, celsius }); // encoded entity
}
}
const system = ActorSystem.create('demo', actorSystemOptions);
system.spawn(() => new TemperatureHub(MqttOptions.create()), 'hub');

T (here Reading) types the inbound payload — message.payload.entity() returns a T. A second generic, TSelf, types application messages other actors may tell the ref (see External control below); it defaults to never.

Override these on your subclass. onReceive is sealed — the base class dispatches to the hooks below; do not override it.

HookWhen
onMessage(message)A message arrived on one of this actor’s own subscriptions. Required.
onConnected()The connection (re)opened; the subscription registry has been re-applied on the broker.
onDisconnected(cause?)The connection dropped; a reconnect cycle may follow (per settings).
onInvalidMessage(err, message)onMessage threw an MqttDecodeError (a lazy entity() on a malformed payload). Default: log + drop. Rethrow to escalate.
onSelfMessage(message)An application message (TSelf) was told to this ref.

Inbound and lifecycle events are delivered through the mailbox, so onMessage and the hooks always run on the actor thread (single-threaded, per-connection order preserved: connected → messages → disconnected).

Settings resolve with the usual precedence: constructor / builder > HOCON (actor-ts.io.broker.mqtt) > built-in defaults. Use the fluent MqttOptions builder, or pass a plain Partial<MqttOptionsType>.

const options = MqttOptions.create()
.withBrokerUrl('mqtts://mqtt.example.com:8883')
.withClientId('my-app')
.withCredentials(process.env.MQTT_USER, process.env.MQTT_PASS)
.withQos(1) // default QoS for publish/subscribe
.withProtocolVersion(5) // opt in to MQTT 5.0
.withCleanSession(false)
.withKeepAlive(30);
interface MqttOptionsType extends BrokerCommonOptionsType {
brokerUrl?: string; // mqtt:// mqtts:// ws:// wss://
clientId?: string;
credentials?: { username?: string; password?: string };
qos?: 0 | 1 | 2; // default QoS for publish/subscribe
cleanSession?: boolean; // default true
keepAlive?: number; // seconds; default 60
protocolVersion?: 4 | 5; // default 4 (= MQTT 3.1.1)
codec?: MqttCodec<unknown>; // default mqttJsonCodec()
will?: { topic: string; payload: string | Uint8Array; qos?: 0 | 1 | 2; retain?: boolean };
}

Common patterns:

  • withCleanSession(false) — session persists; the broker delivers messages missed during disconnection (subject to broker config).
  • withWill({ ... }) — the broker publishes this when the client disconnects ungracefully. Useful for presence (“device-42-offline”).
  • withProtocolVersion(5) — enables MQTT 5 features (user properties, reason codes on MqttMessage). Requires a v5-capable broker.

Inbound MqttMessage<T> carries a lazily-decoding MqttPayload<T>:

override onMessage(message: MqttMessage<Reading>): void {
message.payload.bytes; // raw Uint8Array
message.payload.text(); // UTF-8 string (cached)
message.payload.entity(); // decoded T via the codec (cached)
message.payload.entity<Acknowledgment>(); // decode as a different type on a specific topic
}

Decoding is lazy — it only happens when you call text() / entity(). A malformed payload makes entity() throw an MqttDecodeError; because that happens inside onMessage, the base class catches it and routes it to onInvalidMessage (default: log + drop, no restart):

protected override onInvalidMessage(err: MqttDecodeError, message: MqttMessage<Reading>): void {
this.log.warn(`bad payload on ${err.topic}: ${err.message}`);
}

this.publish(topic, payload, options?) returns false if the message was dropped (encode failure or outbound-buffer overflow):

  • a string or Uint8Array payload is sent raw;
  • any other value is encoded via the codec (JSON by default).
this.publish('ack/1', 'ok'); // raw bytes: ok
this.publish('rollup', { sensor, celsius }); // JSON: {"sensor":...,"celsius":...}
this.publish('cfg', data, { qos: 1, retain: true });

To publish a bare string as a JSON entity (the wire bytes "pong" rather than pong), encode it explicitly:

this.publish('topic', this.codec().encode('pong'));

The codec turns entities into bytes and back. The default is mqttJsonCodec() (plain JSON over UTF-8). Supply your own via withCodec(...) — e.g. a JSON codec with runtime validation:

import { mqttJsonCodec, MqttOptions } from 'actor-ts';
const codec = mqttJsonCodec<Reading>({
validate: (v) => ReadingSchema.parse(v), // zod, etc.; throws → MqttDecodeError
});
MqttOptions.create().withBrokerUrl('mqtt://localhost:1883').withCodec(codec);

Beyond the subclass API, any actor can drive an MqttActor by telling it an MqttCommand:

ref.tell({ kind: 'publish', publish: { topic: 't', payload: 'hi', qos: 1 } });
// Subscribe and route to this actor's own onMessage (no target):
ref.tell({ kind: 'subscribe', topic: 'x/#', qos: 1 });
// Subscribe and fan out to another actor (external target):
ref.tell({ kind: 'subscribe', topic: 'y/#', target: someHandler });
ref.tell({ kind: 'unsubscribe', topic: 'y/#', target: someHandler });
  • A subscribe with no target delivers matching messages to the actor’s own onMessage; with a target, it fans out to that actor. Overlapping patterns deliver to each ref at most once.
  • An unsubscribe with a target removes that target. With no target, it removes all foreign targets but leaves the actor’s own subscription intact — an external controller can’t silence the subclass’s constructor-declared subscription.
  • Fan-out targets are deathwatched: when a target actor stops, it is pruned automatically (and a broker UNSUBSCRIBE fires once a pattern has no consumers left).

Since these commands are plain objects, keep any TSelf you define off the kind values publish / subscribe / unsubscribe — otherwise it would be dispatched as a command instead of reaching onSelfMessage.

QoSDeliveryCost
0At-most-once. Fire-and-forget.Cheapest; messages may be lost.
1At-least-once. Broker stores until acked.Default for most use cases. Duplicates possible.
2Exactly-once. Full handshake.Slowest; rarely needed.

For IoT telemetry, QoS 1 is the typical balance. Commands to devices: also QoS 1 (or QoS 2 if duplicates would be harmful). Status updates that are quickly superseded (sensor readings): QoS 0 is fine.

this.subscribe('sensors/+/temp'); // matches sensors/dev1/temp, sensors/dev2/temp, ...
this.subscribe('devices/#'); // matches devices/anything/anywhere/...
WildcardMatches
+Exactly one level.
#Multiple levels (must be the last segment).
this.publish('config/device-42', { mode: 'eco' }, { retain: true });

A retained message is stored by the broker and delivered immediately to any new subscriber. Use for configuration (devices reading the latest config on connect) or last-known-state. Keep retain unset for normal pub/sub — otherwise the broker keeps every old message visible forever.

Terminal window
npm install mqtt
# or: bun add mqtt
  1. IoT / telemetry — devices publishing sensor data, subscribing to commands.
  2. Bridging from existing MQTT infrastructure — when the broker is already there.
  3. Lightweight pub/sub — when Kafka is overkill (small payloads, no replay history needed).

For cluster-internal pub/sub, DistributedPubSub is simpler (no external broker).