diff --git a/html/js/app.js b/html/js/app.js
index 7750cb8..319b5b4 100644
--- a/html/js/app.js
+++ b/html/js/app.js
@@ -1,97 +1,197 @@
+// Websocket transport between the web UI and xTeVe.
+//
+// One socket stays open for the life of the page. Requests are queued and
+// sent one at a time; each carries an id that the server echoes back, and
+// the response is matched on it (or, for older servers that close after a
+// single reply, to the oldest in-flight request). Connection loss triggers
+// a reconnect with exponential backoff, and queued requests survive it.
+class WSClient {
+ constructor() {
+ this.socket = null;
+ this.queue = [];
+ this.inFlight = null;
+ this.nextID = 1;
+ this.reconnectDelay = 500;
+ this.reconnectTimer = 0;
+ this.closedByUs = false;
+ this.requestTimeoutMs = 12000;
+ }
+ url() {
+ var protocol = window.location.protocol == "https:" ? "wss://" : "ws://";
+ var host = window.location.host;
+ if (host == undefined || host.length < 1) {
+ host = window.location.hostname;
+ }
+ return protocol + host + "/data/";
+ }
+ // send queues a command and resolves with the parsed response.
+ send(payload) {
+ var client = this;
+ return new Promise(function (resolve, reject) {
+ var req = {
+ id: String(client.nextID++),
+ payload: payload,
+ resolve: resolve,
+ reject: reject,
+ timer: 0,
+ sent: false
+ };
+ client.queue.push(req);
+ client.pump();
+ });
+ }
+ // hasQueued reports whether a command of this kind is already waiting.
+ hasQueued(cmd) {
+ if (this.inFlight != null && this.inFlight.payload["cmd"] == cmd) {
+ return true;
+ }
+ for (var i = 0; i < this.queue.length; i++) {
+ if (this.queue[i].payload["cmd"] == cmd) {
+ return true;
+ }
+ }
+ return false;
+ }
+ connect() {
+ if (this.socket != null && (this.socket.readyState == WebSocket.OPEN || this.socket.readyState == WebSocket.CONNECTING)) {
+ return;
+ }
+ var client = this;
+ var socket = new WebSocket(this.url());
+ this.socket = socket;
+ socket.onopen = function () {
+ WS_AVAILABLE = true;
+ client.reconnectDelay = 500;
+ client.pump();
+ };
+ socket.onmessage = function (e) {
+ client.handleMessage(e.data);
+ };
+ socket.onerror = function () {
+ // onclose follows; nothing to do here.
+ };
+ socket.onclose = function () {
+ if (client.socket == socket) {
+ client.socket = null;
+ }
+ // An in-flight request that never got its answer goes back to the
+ // front of the queue and is retried on the next connection.
+ if (client.inFlight != null) {
+ var req = client.inFlight;
+ client.inFlight = null;
+ req.sent = false;
+ client.queue.unshift(req);
+ }
+ client.scheduleReconnect();
+ };
+ }
+ scheduleReconnect() {
+ if (this.closedByUs || this.reconnectTimer != 0) {
+ return;
+ }
+ if (this.queue.length == 0) {
+ // Idle: reconnect lazily on the next request.
+ return;
+ }
+ var client = this;
+ this.reconnectTimer = window.setTimeout(function () {
+ client.reconnectTimer = 0;
+ client.connect();
+ }, this.reconnectDelay);
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 10000);
+ }
+ // pump sends the next queued request if the socket is open and idle.
+ pump() {
+ if (this.inFlight != null || this.queue.length == 0) {
+ return;
+ }
+ if (this.socket == null || this.socket.readyState != WebSocket.OPEN) {
+ this.connect();
+ return;
+ }
+ var req = this.queue.shift();
+ this.inFlight = req;
+ req.sent = true;
+ req.payload["id"] = req.id;
+ var client = this;
+ req.timer = window.setTimeout(function () {
+ if (client.inFlight == req) {
+ client.inFlight = null;
+ req.reject(new Error("timeout"));
+ // A socket that stopped answering is not trusted any more.
+ client.resetSocket();
+ client.pump();
+ }
+ }, this.requestTimeoutMs);
+ try {
+ this.socket.send(JSON.stringify(req.payload));
+ }
+ catch (err) {
+ window.clearTimeout(req.timer);
+ this.inFlight = null;
+ req.sent = false;
+ this.queue.unshift(req);
+ this.resetSocket();
+ }
+ }
+ resetSocket() {
+ var socket = this.socket;
+ this.socket = null;
+ if (socket != null) {
+ socket.onclose = null;
+ try {
+ socket.close();
+ }
+ catch (err) { /* ignore */ }
+ }
+ this.scheduleReconnect();
+ }
+ handleMessage(raw) {
+ var response;
+ try {
+ response = JSON.parse(raw);
+ }
+ catch (err) {
+ return;
+ }
+ var req = this.inFlight;
+ if (req == null) {
+ return;
+ }
+ // Servers that echo the id let us drop stray late answers.
+ if (response.hasOwnProperty("id") && response["id"] != req.id) {
+ return;
+ }
+ window.clearTimeout(req.timer);
+ this.inFlight = null;
+ req.resolve(response);
+ this.pump();
+ }
+}
+var WS = new WSClient();
class Server {
constructor(cmd) {
this.cmd = cmd;
}
request(data) {
- if (SERVER_CONNECTION == true) {
+ var isLogUpdate = this.cmd == "updateLog";
+ // Log polling must never pile up behind a stalled connection.
+ if (isLogUpdate && WS.hasQueued("updateLog")) {
return;
}
- SERVER_CONNECTION = true;
- if (this.cmd != "updateLog") {
+ if (!isLogUpdate) {
showElement("loading", true);
UNDO = new Object();
setConnectionState("busy");
}
- switch (window.location.protocol) {
- case "http:":
- this.protocol = "ws://";
- break;
- case "https:":
- this.protocol = "wss://";
- break;
- }
- var wsHost = window.location.host;
- if (wsHost == undefined || wsHost.length < 1) {
- wsHost = window.location.hostname;
- }
- // The session cookie (HttpOnly) is sent with the websocket handshake.
- var url = this.protocol + wsHost + "/data/";
data["cmd"] = this.cmd;
- var requestCmd = data["cmd"];
- var ws = new WebSocket(url);
- var isLogUpdate = data["cmd"] == "updateLog";
- var responseReceived = false;
- var requestFinished = false;
- var timeoutMs = 12000;
- var requestTimeout;
- var finishRequest = function (state, responseSuccess = false) {
- if (requestFinished == true) {
- return;
- }
- requestFinished = true;
- SERVER_CONNECTION = false;
- window.clearTimeout(requestTimeout);
- if (responseSuccess == true) {
- if (state == "online") {
- WS_FAILURE_COUNT = 0;
- }
- }
- else {
- WS_FAILURE_COUNT++;
- }
- if (isLogUpdate == false) {
+ var requestCmd = this.cmd;
+ WS.send(data).then(function (response) {
+ WS_FAILURE_COUNT = 0;
+ if (!isLogUpdate) {
showElement("loading", false);
}
- if (state != "") {
- setConnectionState(state);
- }
- };
- requestTimeout = window.setTimeout(function () {
- console.log("Websocket request timed out.");
- var timeoutState = "offline";
- if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
- timeoutState = "idle";
- }
- finishRequest(timeoutState, false);
- try {
- ws.close();
- }
- catch (err) {
- console.log(err);
- }
- }, timeoutMs);
- ws.onopen = function () {
- WS_AVAILABLE = true;
- if (data["cmd"] != "updateLog") {
- setConnectionState("busy");
- }
- this.send(JSON.stringify(data));
- };
- ws.onerror = function (e) {
- console.log("No websocket connection to xTeVe could be established. Check your network configuration.");
- var errorState = "offline";
- if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
- errorState = "idle";
- }
- finishRequest(errorState, false);
- if (WS_AVAILABLE == false && isLogUpdate == false && requestCmd != "getServerConfig") {
- alert("No websocket connection to xTeVe could be established. Check your network configuration.");
- }
- };
- ws.onmessage = function (e) {
- responseReceived = true;
- finishRequest("online", true);
- var response = JSON.parse(e.data);
+ setConnectionState("online");
if (response["status"] == false) {
setConnectionState("offline");
alert(response["err"]);
@@ -106,16 +206,14 @@ class Server {
div.className = "changed";
return;
}
- switch (data["cmd"]) {
+ switch (requestCmd) {
case "updateLog":
SERVER["log"] = response["log"];
if (document.getElementById("content_log")) {
showLogs(false);
}
return;
- break;
default:
- SERVER = new Object();
SERVER = response;
break;
}
@@ -139,17 +237,20 @@ class Server {
return;
}
createLayout();
- };
- ws.onclose = function () {
- if (responseReceived == true) {
- return;
+ }, function (err) {
+ WS_FAILURE_COUNT++;
+ if (!isLogUpdate) {
+ showElement("loading", false);
}
- var closeState = "offline";
- if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
- closeState = "idle";
+ var state = "offline";
+ if (isLogUpdate && WS_FAILURE_COUNT < 2) {
+ state = "idle";
}
- finishRequest(closeState, false);
- };
+ setConnectionState(state);
+ if (WS_AVAILABLE == false && !isLogUpdate && requestCmd != "getServerConfig") {
+ alert("No websocket connection to xTeVe could be established. Check your network configuration.");
+ }
+ });
}
}
var WS_FAILURE_COUNT = 0;
@@ -961,7 +1062,7 @@ function PageReady() {
window.clearInterval(bootstrapTimer);
return;
}
- if (SERVER_CONNECTION == true) {
+ if (WS.hasQueued("getServerConfig")) {
return;
}
bootstrapAttempts++;
@@ -2688,7 +2789,6 @@ var BULK_EDIT = false;
var COLUMN_TO_SORT;
var SEARCH_MAPPING = new Object();
var UNDO = new Object();
-var SERVER_CONNECTION = false;
var WS_AVAILABLE = false;
var ACTIVE_MENU_ID = "";
var LAST_BULK_CHECKBOX = null;
@@ -3236,7 +3336,6 @@ function sortSelect(elem) {
return;
}
function updateLog() {
- console.log("TOKEN");
var server = new Server("updateLog");
server.request(new Object());
}
diff --git a/src/struct-webserver.go b/src/struct-webserver.go
index e6f7cb2..baa7dce 100644
--- a/src/struct-webserver.go
+++ b/src/struct-webserver.go
@@ -5,6 +5,10 @@ type RequestStruct struct {
// Befehle an xTeVe
Cmd string `json:"cmd"`
+ // Client-chosen request id, echoed in the response so the web UI can
+ // match answers on a long-lived connection.
+ ID string `json:"id,omitempty"`
+
// Benutzer
DeleteUser bool `json:"deleteUser,omitempty"`
UserData map[string]any `json:"userData,omitempty"`
@@ -73,6 +77,8 @@ type RequestStruct struct {
// ResponseStruct : Antworten an den Client (WEB)
type ResponseStruct struct {
+ ID string `json:"id,omitempty"`
+
ClientInfo struct {
ARCH string `json:"arch"`
Branch string `json:"branch,omitempty"`
diff --git a/src/webserver.go b/src/webserver.go
index a302603..625397e 100644
--- a/src/webserver.go
+++ b/src/webserver.go
@@ -330,22 +330,18 @@ func WS(w http.ResponseWriter, r *http.Request) {
var newToken string
- /*
- if r.Header.Get("Origin") != "http://"+r.Host {
- httpStatusError(w, r, 403)
- return
- }
- */
-
// Upgrade writes its own error response (e.g. 403 for a bad Origin).
conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
ShowError(err, 0)
return
}
+ defer conn.Close()
setGlobalDomain(r.Host)
+ // One connection serves any number of commands, one at a time, until the
+ // client goes away or a write fails.
for {
// Fresh structs per command: a failed command must not leak its
@@ -360,6 +356,8 @@ func WS(w http.ResponseWriter, r *http.Request) {
return
}
+ response.ID = request.ID
+
if !System.ConfigurationWizard {
switch Settings.AuthenticationWEB {
@@ -398,10 +396,9 @@ func WS(w http.ResponseWriter, r *http.Request) {
response = setDefaultResponseData(response, false)
if err = conn.WriteJSON(response); err != nil {
ShowError(err, 1022)
- } else {
return
}
- return
+ continue
case "loadFiles":
//response.Response = Settings.Files
@@ -572,8 +569,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
if err = conn.WriteJSON(response); err != nil {
ShowError(err, 1022)
- } else {
- break
+ return
}
}
diff --git a/src/websocket_test.go b/src/websocket_test.go
index 611b01a..0e6bf58 100644
--- a/src/websocket_test.go
+++ b/src/websocket_test.go
@@ -5,6 +5,7 @@ import (
"net/http/httptest"
"strings"
"testing"
+ "time"
"github.com/gorilla/websocket"
@@ -137,3 +138,33 @@ func TestSetCookieTokenFlags(t *testing.T) {
t.Error("logout must clear the cookie (negative MaxAge)")
}
}
+
+// A browser keeps one socket open and sends commands one after another; the
+// server must answer each on the same connection and echo the request id.
+func TestWSServesMultipleCommandsPerConnection(t *testing.T) {
+ srv, token := newAuthenticatedWSServer(t)
+
+ conn, _, err := websocket.DefaultDialer.Dial(wsURL(srv), http.Header{"Cookie": {"Token=" + token}})
+ if err != nil {
+ t.Fatalf("dial: %v", err)
+ }
+ defer conn.Close()
+
+ for i := 1; i <= 3; i++ {
+ id := strings.Repeat("x", i)
+ if err := conn.WriteJSON(map[string]any{"cmd": "noop", "id": id}); err != nil {
+ t.Fatalf("write %d: %v", i, err)
+ }
+ conn.SetReadDeadline(time.Now().Add(2 * time.Second))
+ var response ResponseStruct
+ if err := conn.ReadJSON(&response); err != nil {
+ t.Fatalf("read %d: %v (server stopped answering on the same connection)", i, err)
+ }
+ if !response.Status {
+ t.Fatalf("command %d refused: %s", i, response.Error)
+ }
+ if response.ID != id {
+ t.Fatalf("command %d: id %q not echoed, got %q", i, id, response.ID)
+ }
+ }
+}
diff --git a/tasks/todo.md b/tasks/todo.md
index 745b63c..c4cfd17 100644
--- a/tasks/todo.md
+++ b/tasks/todo.md
@@ -1,7 +1,7 @@
# xTeVe improvement checklist
Detailed rationale, file references and effort estimates: `tasks/improvement-plan.md`.
-Status: Phase 0 committed on branch `improvements` 2026-09-26.
+Status: Phases 0, 1, 2, 3 done on branch `improvements` (2026-09-26); Phase 5 in progress.
## Phase 0: Hygiene
- [x] `.gitignore` (`.gocache/`), deleted `.gocache/`, extended `.dockerignore`
@@ -35,12 +35,12 @@ Status: Phase 0 committed on branch `improvements` 2026-09-26.
- [x] `streamingURLsMu` around `Data.Cache.StreamingURLS`; persisted from a snapshot
- [x] No more `defer` inside the buffer read/redirect loops (compression done in Phase 1; imgcache in 2b)
- [x] `thirdPartyBuffer` rewritten: `exec.CommandContext` cancelled when the last client leaves or the 20 s startup watchdog fires; `buildFFmpegArgs`/`buildVLCArgs` tested; no panic, `cmd.Start` checked, one file handle per segment. Client loop uses `r.Context()` instead of `CloseNotifier`
-- [ ] Real mutex for screen log
-- [ ] `http.Server` with timeouts; timeouts on all outbound clients
-- [ ] imgcache: download outside the lock
-- [ ] Atomic write helper (temp + fsync + rename)
-- [x] Buffer: headers set before `WriteHeader`, bogus `Content-Length:` header gone, segments flushed to the client as they arrive. Others in 2b
-- [ ] Error hygiene: no-op closures, `os.Exit`/`panic` outside main, ignored results
+- [x] `logMu` guards `WebScreenLog` and notifications; accessors `logAppend`/`webScreenLogSnapshot`/`notificationsSnapshot`; ring buffer keeps the newest N (was dropping them)
+- [x] `http.Server` with `ReadHeaderTimeout` 15 s and `IdleTimeout` 120 s (no write timeout, streams are long-lived); `providerHTTPClient` 5 min, `apiHTTPClient` 30 s, imgcache client 30 s, stream client with dial/TLS/header timeouts
+- [x] imgcache downloads outside the lock; cache URL bug fixed (was a filesystem path); query-string URLs no longer produce unservable file names
+- [x] `writeFileAtomic` (temp + fsync + rename) behind `writeByteToFile`, `saveMapToJSONFile`, `writePrivateFile` and the auth database
+- [x] Buffer: headers set before `WriteHeader`, bogus `Content-Length:` header gone, segments flushed as they arrive. xepg file-removal bug, dead range mutation, API double write, WS struct reuse, unchecked assertions in data/backup/provider/screen, notification eviction by age: all fixed with tests
+- [x] Error hygiene: no `panic`/`os.Exit`/`log.Fatal` outside `main`; SIGINT/SIGTERM handled in main via `src.Shutdown()`; fatal start-up errors exit 1
## Phase 3: Build, embed, CI, Docker
- [x] `//go:embed` via `html/embed.go`; deleted `webUI.go`, `html-build.go`, `cmd/webui-gen`; ETag + Cache-Control on static assets
@@ -58,7 +58,7 @@ Status: Phase 0 committed on branch `improvements` 2026-09-26.
Not planned (lineup is ~170 channels, decided 2026-09-25). See plan for the reference list.
## Phase 5: Frontend architecture
-- [ ] Persistent websocket with request IDs, queue, backoff reconnect
+- [x] Persistent websocket: one socket per page, queued requests with ids echoed by the server, 12 s per-request timeout, exponential-backoff reconnect, log-poll dedupe. Server keeps serving on one connection and closes it on exit (upstream leaked one open socket per request). Test: `TestWSServesMultipleCommandsPerConnection`
- [ ] Section-level re-render instead of full `createLayout()`
- [ ] Virtualised mapping table
- [ ] Split `menu_ts.ts`; `addEventListener`; data-driven settings rows
diff --git a/ts/base_ts.ts b/ts/base_ts.ts
index 2b801df..293b9b1 100644
--- a/ts/base_ts.ts
+++ b/ts/base_ts.ts
@@ -3,7 +3,6 @@ var BULK_EDIT:Boolean = false
var COLUMN_TO_SORT:number
var SEARCH_MAPPING = new Object()
var UNDO = new Object()
-var SERVER_CONNECTION = false
var WS_AVAILABLE = false
var ACTIVE_MENU_ID:string = ""
var LAST_BULK_CHECKBOX:HTMLInputElement = null
@@ -761,7 +760,6 @@ function sortSelect(elem) {
function updateLog() {
- console.log("TOKEN")
var server:Server = new Server("updateLog")
server.request(new Object())
diff --git a/ts/menu_ts.ts b/ts/menu_ts.ts
index 54f5d96..012a55d 100644
--- a/ts/menu_ts.ts
+++ b/ts/menu_ts.ts
@@ -1023,7 +1023,7 @@ function PageReady() {
return
}
- if (SERVER_CONNECTION == true) {
+ if (WS.hasQueued("getServerConfig")) {
return
}
diff --git a/ts/network_ts.ts b/ts/network_ts.ts
index dac98ba..419c0ec 100644
--- a/ts/network_ts.ts
+++ b/ts/network_ts.ts
@@ -1,136 +1,240 @@
-class Server {
- protocol:string
- cmd:string
+// Websocket transport between the web UI and xTeVe.
+//
+// One socket stays open for the life of the page. Requests are queued and
+// sent one at a time; each carries an id that the server echoes back, and
+// the response is matched on it (or, for older servers that close after a
+// single reply, to the oldest in-flight request). Connection loss triggers
+// a reconnect with exponential backoff, and queued requests survive it.
- constructor(cmd:string) {
- this.cmd = cmd
+interface PendingRequest {
+ id: string
+ payload: Object
+ resolve: (response: any) => void
+ reject: (err: Error) => void
+ timer: number
+ sent: boolean
+}
+
+class WSClient {
+ private socket: WebSocket = null
+ private queue: PendingRequest[] = []
+ private inFlight: PendingRequest = null
+ private nextID: number = 1
+ private reconnectDelay: number = 500
+ private reconnectTimer: number = 0
+ private closedByUs: boolean = false
+
+ readonly requestTimeoutMs: number = 12000
+
+ private url(): string {
+ var protocol = window.location.protocol == "https:" ? "wss://" : "ws://"
+ var host = window.location.host
+ if (host == undefined || host.length < 1) {
+ host = window.location.hostname
+ }
+ return protocol + host + "/data/"
}
- request(data:Object):any {
+ // send queues a command and resolves with the parsed response.
+ send(payload: Object): Promise {
+ var client = this
+ return new Promise(function(resolve, reject) {
+ var req: PendingRequest = {
+ id: String(client.nextID++),
+ payload: payload,
+ resolve: resolve,
+ reject: reject,
+ timer: 0,
+ sent: false
+ }
+ client.queue.push(req)
+ client.pump()
+ })
+ }
- if (SERVER_CONNECTION == true) {
+ // hasQueued reports whether a command of this kind is already waiting.
+ hasQueued(cmd: string): boolean {
+ if (this.inFlight != null && this.inFlight.payload["cmd"] == cmd) {
+ return true
+ }
+ for (var i = 0; i < this.queue.length; i++) {
+ if (this.queue[i].payload["cmd"] == cmd) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private connect(): void {
+ if (this.socket != null && (this.socket.readyState == WebSocket.OPEN || this.socket.readyState == WebSocket.CONNECTING)) {
return
}
- SERVER_CONNECTION = true
-
- if (this.cmd != "updateLog") {
+ var client = this
+ var socket = new WebSocket(this.url())
+ this.socket = socket
+
+ socket.onopen = function() {
+ WS_AVAILABLE = true
+ client.reconnectDelay = 500
+ client.pump()
+ }
+
+ socket.onmessage = function(e: MessageEvent) {
+ client.handleMessage(e.data)
+ }
+
+ socket.onerror = function() {
+ // onclose follows; nothing to do here.
+ }
+
+ socket.onclose = function() {
+ if (client.socket == socket) {
+ client.socket = null
+ }
+ // An in-flight request that never got its answer goes back to the
+ // front of the queue and is retried on the next connection.
+ if (client.inFlight != null) {
+ var req = client.inFlight
+ client.inFlight = null
+ req.sent = false
+ client.queue.unshift(req)
+ }
+ client.scheduleReconnect()
+ }
+ }
+
+ private scheduleReconnect(): void {
+ if (this.closedByUs || this.reconnectTimer != 0) {
+ return
+ }
+ if (this.queue.length == 0) {
+ // Idle: reconnect lazily on the next request.
+ return
+ }
+ var client = this
+ this.reconnectTimer = window.setTimeout(function() {
+ client.reconnectTimer = 0
+ client.connect()
+ }, this.reconnectDelay)
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 10000)
+ }
+
+ // pump sends the next queued request if the socket is open and idle.
+ private pump(): void {
+ if (this.inFlight != null || this.queue.length == 0) {
+ return
+ }
+ if (this.socket == null || this.socket.readyState != WebSocket.OPEN) {
+ this.connect()
+ return
+ }
+
+ var req = this.queue.shift()
+ this.inFlight = req
+ req.sent = true
+ req.payload["id"] = req.id
+
+ var client = this
+ req.timer = window.setTimeout(function() {
+ if (client.inFlight == req) {
+ client.inFlight = null
+ req.reject(new Error("timeout"))
+ // A socket that stopped answering is not trusted any more.
+ client.resetSocket()
+ client.pump()
+ }
+ }, this.requestTimeoutMs)
+
+ try {
+ this.socket.send(JSON.stringify(req.payload))
+ } catch (err) {
+ window.clearTimeout(req.timer)
+ this.inFlight = null
+ req.sent = false
+ this.queue.unshift(req)
+ this.resetSocket()
+ }
+ }
+
+ private resetSocket(): void {
+ var socket = this.socket
+ this.socket = null
+ if (socket != null) {
+ socket.onclose = null
+ try { socket.close() } catch (err) { /* ignore */ }
+ }
+ this.scheduleReconnect()
+ }
+
+ private handleMessage(raw: string): void {
+ var response: any
+ try {
+ response = JSON.parse(raw)
+ } catch (err) {
+ return
+ }
+
+ var req = this.inFlight
+ if (req == null) {
+ return
+ }
+ // Servers that echo the id let us drop stray late answers.
+ if (response.hasOwnProperty("id") && response["id"] != req.id) {
+ return
+ }
+
+ window.clearTimeout(req.timer)
+ this.inFlight = null
+ req.resolve(response)
+ this.pump()
+ }
+}
+
+var WS = new WSClient()
+
+class Server {
+ cmd: string
+
+ constructor(cmd: string) {
+ this.cmd = cmd
+ }
+
+ request(data: Object): any {
+
+ var isLogUpdate: boolean = this.cmd == "updateLog"
+
+ // Log polling must never pile up behind a stalled connection.
+ if (isLogUpdate && WS.hasQueued("updateLog")) {
+ return
+ }
+
+ if (!isLogUpdate) {
showElement("loading", true)
UNDO = new Object()
setConnectionState("busy")
}
-
- switch(window.location.protocol) {
- case "http:":
- this.protocol = "ws://"
- break
- case "https:":
- this.protocol = "wss://"
- break
- }
-
- var wsHost:string = window.location.host
- if (wsHost == undefined || wsHost.length < 1) {
- wsHost = window.location.hostname
- }
- // The session cookie (HttpOnly) is sent with the websocket handshake.
- var url = this.protocol + wsHost + "/data/"
data["cmd"] = this.cmd
- var requestCmd:string = data["cmd"]
- var ws = new WebSocket(url)
- var isLogUpdate:boolean = data["cmd"] == "updateLog"
- var responseReceived:boolean = false
- var requestFinished:boolean = false
- var timeoutMs:number = 12000
- var requestTimeout:number
+ var requestCmd: string = this.cmd
- var finishRequest = function(state:string, responseSuccess:boolean = false):void {
- if (requestFinished == true) {
- return
- }
+ WS.send(data).then(function(response) {
- requestFinished = true
- SERVER_CONNECTION = false
- window.clearTimeout(requestTimeout)
-
- if (responseSuccess == true) {
- if (state == "online") {
- WS_FAILURE_COUNT = 0
- }
- } else {
- WS_FAILURE_COUNT++
- }
-
- if (isLogUpdate == false) {
+ WS_FAILURE_COUNT = 0
+ if (!isLogUpdate) {
showElement("loading", false)
}
-
- if (state != "") {
- setConnectionState(state)
- }
- }
-
- requestTimeout = window.setTimeout(function() {
- console.log("Websocket request timed out.")
- var timeoutState:string = "offline"
- if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
- timeoutState = "idle"
- }
- finishRequest(timeoutState, false)
- try {
- ws.close()
- } catch (err) {
- console.log(err)
- }
- }, timeoutMs)
-
- ws.onopen = function() {
-
- WS_AVAILABLE = true
- if (data["cmd"] != "updateLog") {
- setConnectionState("busy")
- }
-
- this.send(JSON.stringify(data));
-
- }
-
- ws.onerror = function(e) {
-
- console.log("No websocket connection to xTeVe could be established. Check your network configuration.")
- var errorState:string = "offline"
- if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
- errorState = "idle"
- }
- finishRequest(errorState, false)
-
- if (WS_AVAILABLE == false && isLogUpdate == false && requestCmd != "getServerConfig") {
- alert("No websocket connection to xTeVe could be established. Check your network configuration.")
- }
-
- }
-
-
- ws.onmessage = function (e) {
- responseReceived = true
- finishRequest("online", true)
-
- var response = JSON.parse(e.data);
+ setConnectionState("online")
if (response["status"] == false) {
setConnectionState("offline")
-
alert(response["err"])
-
if (response.hasOwnProperty("reload")) {
location.reload()
}
-
return
}
-
if (response.hasOwnProperty("logoURL")) {
var div = (document.getElementById("channel-icon") as HTMLInputElement)
div.value = response["logoURL"]
@@ -138,19 +242,17 @@ class Server {
return
}
- switch (data["cmd"]) {
+ switch (requestCmd) {
case "updateLog":
SERVER["log"] = response["log"]
if (document.getElementById("content_log")) {
showLogs(false)
}
return
- break;
-
+
default:
- SERVER = new Object()
SERVER = response
- break;
+ break
}
if (response.hasOwnProperty("openMenu")) {
@@ -171,7 +273,6 @@ class Server {
location.reload()
}
-
if (response.hasOwnProperty("wizard")) {
createLayout()
configurationWizard[response["wizard"]].createWizard()
@@ -180,23 +281,25 @@ class Server {
createLayout()
- }
+ }, function(err) {
- ws.onclose = function() {
- if (responseReceived == true) {
- return
+ WS_FAILURE_COUNT++
+ if (!isLogUpdate) {
+ showElement("loading", false)
}
- var closeState:string = "offline"
- if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
- closeState = "idle"
+ var state: string = "offline"
+ if (isLogUpdate && WS_FAILURE_COUNT < 2) {
+ state = "idle"
}
- finishRequest(closeState, false)
- }
-
+ setConnectionState(state)
+
+ if (WS_AVAILABLE == false && !isLogUpdate && requestCmd != "getServerConfig") {
+ alert("No websocket connection to xTeVe could be established. Check your network configuration.")
+ }
+
+ })
}
-
}
-var WS_FAILURE_COUNT:number = 0
-
+var WS_FAILURE_COUNT: number = 0