import QtQuick // Tiny SSE client implemented in pure QML. // Caveat: relies on QML's XMLHttpRequest delivering partial responses // during readyState === LOADING. If that proves unreliable, this gets // replaced by a C++ class in Phase 1 (per PLAN.md ยง7, MercureClient). QtObject { id: root property string url: "" property bool active: false signal eventReceived(string data, string id) signal stateChanged(string state) property var _xhr: null property int _offset: 0 property string _accumulated: "" function connect() { if (active) return _offset = 0 _accumulated = "" const xhr = new XMLHttpRequest() xhr.open("GET", url) xhr.setRequestHeader("Accept", "text/event-stream") xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.LOADING || xhr.readyState === XMLHttpRequest.DONE) { const text = xhr.responseText if (text.length > _offset) { const fresh = text.substring(_offset) _offset = text.length _ingest(fresh) } } if (xhr.readyState === XMLHttpRequest.DONE) { active = false stateChanged("disconnected") } } xhr.send() _xhr = xhr active = true stateChanged("connected") } function disconnect() { if (!active) return if (_xhr) _xhr.abort() active = false stateChanged("disconnected") } function _ingest(chunk) { _accumulated += chunk const parts = _accumulated.split("\n\n") _accumulated = parts.pop() for (let i = 0; i < parts.length; ++i) { _emit(parts[i]) } } function _emit(message) { const lines = message.split("\n") let id = "" let dataLines = [] for (let i = 0; i < lines.length; ++i) { const line = lines[i] if (line.length === 0 || line.charAt(0) === ":") continue const colon = line.indexOf(":") if (colon < 0) continue const field = line.substring(0, colon) let value = line.substring(colon + 1) if (value.length > 0 && value.charAt(0) === " ") value = value.substring(1) if (field === "data") dataLines.push(value) else if (field === "id") id = value } if (dataLines.length > 0) { eventReceived(dataLines.join("\n"), id) } } }