85 lines
2.5 KiB
QML
85 lines
2.5 KiB
QML
|
|
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|