/*! * * Copyright (c) 2026 Diality Inc. - All Rights Reserved. * \copyright * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. * * \file MessageSpool.h * \author (original) Stephen Quong * \date (original) 28-Jul-2026 * */ #pragma once #include #include #include #include struct sqlite3; /*! * \brief One message held in, or destined for, the durable spool. * \details spoolId is the SQLite rowid; -1 means the message has not been * persisted yet. Callers hold the id returned by insert() so they can * remove() the row once delivery is confirmed. */ struct SpooledMessage { qint64 spoolId = -1; ///< SQLite rowid, or -1 when not yet spooled. qint64 timestampMs = 0; ///< Unix epoch ms; stamped by insert() when 0. quint16 sequence = 0; ///< Sequence number carried from the source message. QString topic; ///< Resolved outbound topic. QString deviceId; ///< Device identifier. QString msgId; ///< Source message identifier, e.g. "0x72A0". QByteArray payload; ///< Serialised payload bytes. }; /*! * \brief Durable SQLite store providing the store-and-forward guarantee. * \details Every outbound message is inserted here before any delivery is * attempted, and its row is removed only once delivery is confirmed. * A crash between the two causes redelivery, never loss. * * Not a QObject: this is a leaf persistence detail with no signals and * no thread affinity of its own. All public methods are mutex-guarded * and safe to call from any thread, including non-Qt threads such as * an MQTT client's event loop. * * Schema: * \code * CREATE TABLE spool ( * id INTEGER PRIMARY KEY AUTOINCREMENT, * timestamp_ms INTEGER NOT NULL, * topic TEXT NOT NULL, * payload BLOB NOT NULL, * sequence INTEGER NOT NULL DEFAULT 0, * device_id TEXT NOT NULL DEFAULT '', * msg_id TEXT NOT NULL DEFAULT '' * ); * \endcode */ class MessageSpool { public: /*! * \brief Tuning applied when the database is opened. * \details retentionHours defaults to 96 per the SRS minimum, not the 48 * used by the legacy agent implementation. */ struct Config { QString dbPath; ///< Absolute path to the SQLite file. quint32 retentionHours = 96; ///< Maximum message age before purge. }; explicit MessageSpool(Config config); ~MessageSpool(); MessageSpool(const MessageSpool &) = delete; MessageSpool &operator=(const MessageSpool &) = delete; bool open(); void close(); bool isOpen() const; qint64 insert(const SpooledMessage &msg); QVector fetchBatch(quint32 limit); bool remove(qint64 spoolId); quint64 depth() const; quint64 sizeBytes() const; int purgeExpired(); private: bool execSql(const char *sql); quint64 scalarQuery(const char *sql) const; Config _config; sqlite3 *_db = nullptr; mutable QMutex _mutex; };