Flows

Latent applications (MQTT)

Assert on messages produced asynchronously, out of band.

Some effects do not come back in the response: an HTTP call triggers a job that eventually publishes an MQTT message. Latent applications let you assert on those. (Only MQTT is supported at the moment.)

Declare the client in the flow's frontmatter, so it is connected and subscribed before the flow starts:

latentApplications:
  - application: "mqtt"
    client: "client1"
    connection:
      host: "1234567890-ats.iot.eu-west-1.amazonaws.com"
      port: 8883
      protocol: "mqtts"
      key: "/path/private.key"
      cert: "/path/cert.crt"
      ca: "/path/ca1.pem"
    subscribe:
      - topic: "client/1"

username and password are there for a broker that asks for them, and rejectUnauthorized: false for one with a self-signed certificate.

Then assert on it from any step:

test:
  latentApplications:
    - application: "mqtt"
      client: "client1"
      test:
        - topic: "client/1"
          message:
            status: "switched_to_on"
      retry:
        attempts: 1
        delay: 1

The step passes when every message listed under test has been seen since the flow started. retry is what makes that workable: attempts says how many times to look, delay how many seconds to wait between two looks.

Which message counts as the one you meant

Only the keys you name The comparison is a subset, at any depth: name hdf.cat and everything else in the envelope is ignored.
Expressions A value written as "$expr: value.length > 0" is a JavaScript expression over the actual value — the same as in a body assertion. Use it for a list whose order you do not control.
Wildcards in the topic The expected topic is an MQTT filter: + stands for one level, # for the rest. msg/cloud/+/command matches whichever device answered.
Memory in the topic Unlike the rest of a test, the topic is interpolated: msg/cloud/{{ memory.device }}/command reads the id a step above looked up.
Anything, JSON or not A JSON payload arrives parsed; anything else arrives as text, so a device publishing a bare string can still be asserted on.

Keeping what arrived

A message often carries the only copy of something the rest of the flow needs — an order a device created, the compartment it picked. A memory mapping on the assertion keeps it:

test:
  latentApplications:
    - application: "mqtt"
      client: "client1"
      test:
        - topic: "msg/cloud/{{ memory.device }}/command"
          message:
            hdf:
              cat: "order-status"
          memory:
            orderId: "{{ message.bdy.0.id }}"
            compartmentId: "{{ message.bdy.0.cmp }}"
      retry:
        attempts: 30
        delay: 2

It follows the rules of a step's own memory block (Passing data between steps) — a lone expression keeps its type, a key that resolves to nothing is not written — and reads message (the payload that matched) and topic (the one it came in on). Nothing is written when the message never arrived.

Publishing, from an application

Listening has a mirror: an application method can publish, which is how a flow plays a device that is not there.

import { applications, mqttClient } from 'ronsel';

export const scan = applications.handler([
  (ctx, parameters) => mqttClient.publish(
    ctx,
    `msg/device/${parameters.body.device}/command`,
    { hdf: { cat: 'barcode' }, bdy: [{ bcd: parameters.body.barcode }] }
  )
], 'scan');

The broker comes from the application's environment — MQTT_HOST, and optionally MQTT_PORT, MQTT_PROTOCOL, MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD, MQTT_KEY, MQTT_CERT, MQTT_CA, MQTT_REJECT_UNAUTHORIZED and MQTT_QOS — so the same flow runs against a local broker or a mutually authenticated one by swapping env files. The message is published as JSON unless the method passes its own encode, and the connection is closed again straight after.