#pragma once #include #include #include #include #include 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