All checks were successful
CI / Quality (push) Successful in 5s
MercureClient is a single-topic SSE subscriber: opens a long-lived GET on the hub URL with the topic query and Accept: text/event-stream, parses the line protocol into update(data, id) signals, and reconnects with 1s→2s→…→30s exponential backoff on drop. Tracks lastEventId across reconnects and sends it as Last-Event-ID so the hub can replay missed messages — backing the "Sleep / wake" path in PLAN.md §3 *Edge cases*. One client per topic by design; multi-topic aggregation is Phase 2. RestClient.qml is a Promise-style XMLHttpRequest wrapper. Auto-attaches an RFC4122-v4 Idempotency-Key to every non-GET request (PLAN.md §4 and §7) so retries are safe by default. Maps application/problem+json error bodies into structured rejections for downstream UI. Standalone CMake build remains green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
2.7 KiB
C++
96 lines
2.7 KiB
C++
#pragma once
|
|
|
|
#include <QByteArray>
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QtQmlIntegration>
|
|
|
|
class QNetworkAccessManager;
|
|
class QNetworkReply;
|
|
class QTimer;
|
|
|
|
namespace PhpQml::Bridge {
|
|
|
|
/// Single-topic Mercure SSE subscriber.
|
|
///
|
|
/// Implements the `text/event-stream` line protocol on top of QNetworkReply,
|
|
/// with exponential reconnect (1s → 2s → … → cap 30s) and `Last-Event-ID`
|
|
/// resume across reconnects. See PLAN.md §7 (MercureClient) and §3
|
|
/// *Edge cases — Sleep / wake* for the resume contract.
|
|
///
|
|
/// One client = one topic. Application code instantiates one per
|
|
/// subscribed topic; multi-topic aggregation is a Phase 2 concern.
|
|
class MercureClient : public QObject
|
|
{
|
|
Q_OBJECT
|
|
QML_ELEMENT
|
|
|
|
Q_PROPERTY(QString hubUrl READ hubUrl WRITE setHubUrl NOTIFY hubUrlChanged)
|
|
Q_PROPERTY(QString topic READ topic WRITE setTopic NOTIFY topicChanged)
|
|
Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged)
|
|
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
|
|
Q_PROPERTY(QString lastEventId READ lastEventId NOTIFY lastEventIdChanged)
|
|
|
|
public:
|
|
explicit MercureClient(QObject* parent = nullptr);
|
|
~MercureClient() override;
|
|
|
|
QString hubUrl() const { return m_hubUrl; }
|
|
void setHubUrl(const QString& url);
|
|
QString topic() const { return m_topic; }
|
|
void setTopic(const QString& t);
|
|
QString token() const { return m_token; }
|
|
void setToken(const QString& t);
|
|
bool active() const noexcept { return m_active; }
|
|
QString lastEventId() const { return m_lastEventId; }
|
|
|
|
Q_INVOKABLE void start();
|
|
Q_INVOKABLE void stop();
|
|
|
|
signals:
|
|
void hubUrlChanged();
|
|
void topicChanged();
|
|
void tokenChanged();
|
|
void activeChanged();
|
|
void lastEventIdChanged();
|
|
|
|
/// Emitted for every dispatched SSE event with a `data:` field.
|
|
/// `data` is the joined data lines as the hub sent them; `id` is
|
|
/// the SSE event id (often a UUIDv7).
|
|
void update(const QString& data, const QString& id);
|
|
void error(const QString& detail);
|
|
|
|
private slots:
|
|
void onReadyRead();
|
|
void onFinished();
|
|
void doConnect();
|
|
|
|
private:
|
|
void scheduleReconnect();
|
|
void teardownReply();
|
|
void emitMessage();
|
|
void setActive(bool a);
|
|
|
|
QNetworkAccessManager* m_nam = nullptr;
|
|
QNetworkReply* m_reply = nullptr;
|
|
QTimer* m_reconnectTimer = nullptr;
|
|
|
|
QString m_hubUrl;
|
|
QString m_topic;
|
|
QString m_token;
|
|
QString m_lastEventId;
|
|
|
|
QStringList m_dataLines;
|
|
QByteArray m_pendingEventId;
|
|
|
|
bool m_active = false;
|
|
bool m_userStopped = false;
|
|
int m_currentBackoffMs = kInitialBackoffMs;
|
|
|
|
static constexpr int kInitialBackoffMs = 1000;
|
|
static constexpr int kMaxBackoffMs = 30000;
|
|
};
|
|
|
|
} // namespace PhpQml::Bridge
|