From 1536201e9a008e31f3a62dd5af453ca62f6c4821 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 24 Feb 2022 09:52:49 -0500 Subject: [PATCH] Reconnect on failure, with backoff; Deduping notifications --- web/src/app/Connection.js | 61 ++++++++++++++++++++++---------- web/src/app/ConnectionManager.js | 10 +++--- web/src/app/Subscription.js | 21 +++-------- web/src/app/utils.js | 2 +- web/src/components/AddDialog.js | 3 +- web/src/components/App.js | 10 +++--- 6 files changed, 62 insertions(+), 45 deletions(-) diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index d5ec790..6d1b9ac 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -1,18 +1,31 @@ +import {shortTopicUrl, topicUrlWs} from "./utils"; + +const retryBackoffSeconds = [5, 10, 15, 20, 30, 45, 60, 120]; + class Connection { - constructor(wsUrl, subscriptionId, onNotification) { - this.wsUrl = wsUrl; + constructor(subscriptionId, baseUrl, topic, since, onNotification) { this.subscriptionId = subscriptionId; + this.baseUrl = baseUrl; + this.topic = topic; + this.since = since; + this.shortUrl = shortTopicUrl(baseUrl, topic); this.onNotification = onNotification; this.ws = null; + this.retryCount = 0; + this.retryTimeout = null; } start() { - const socket = new WebSocket(this.wsUrl); - socket.onopen = (event) => { - console.log(`[Connection] [${this.subscriptionId}] Connection established`); + const since = (this.since === 0) ? "all" : this.since.toString(); + const wsUrl = topicUrlWs(this.baseUrl, this.topic, since); + console.log(`[Connection, ${this.shortUrl}] Opening connection to ${wsUrl}`); + this.ws = new WebSocket(wsUrl); + this.ws.onopen = (event) => { + console.log(`[Connection, ${this.shortUrl}] Connection established`, event); + this.retryCount = 0; } - socket.onmessage = (event) => { - console.log(`[Connection] [${this.subscriptionId}] Message received from server: ${event.data}`); + this.ws.onmessage = (event) => { + console.log(`[Connection, ${this.shortUrl}] Message received from server: ${event.data}`); try { const data = JSON.parse(event.data); const relevantAndValid = @@ -21,31 +34,43 @@ class Connection { 'time' in data && 'message' in data; if (!relevantAndValid) { + console.log(`[Connection, ${this.shortUrl}] Message irrelevant or invalid. Ignoring.`); return; } + this.since = data.time; this.onNotification(this.subscriptionId, data); } catch (e) { - console.log(`[Connection] [${this.subscriptionId}] Error handling message: ${e}`); + console.log(`[Connection, ${this.shortUrl}] Error handling message: ${e}`); } }; - socket.onclose = (event) => { + this.ws.onclose = (event) => { if (event.wasClean) { - console.log(`[Connection] [${this.subscriptionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`); + console.log(`[Connection, ${this.shortUrl}] Connection closed cleanly, code=${event.code} reason=${event.reason}`); + this.ws = null; } else { - console.log(`[Connection] [${this.subscriptionId}] Connection died`); + const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length-1)]; + this.retryCount++; + console.log(`[Connection, ${this.shortUrl}] Connection died, retrying in ${retrySeconds} seconds`); + this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000); } }; - socket.onerror = (event) => { - console.log(this.subscriptionId, `[Connection] [${this.subscriptionId}] ${event.message}`); + this.ws.onerror = (event) => { + console.log(`[Connection, ${this.shortUrl}] Error occurred: ${event}`, event); }; - this.ws = socket; } - cancel() { - if (this.ws !== null) { - this.ws.close(); - this.ws = null; + close() { + console.log(`[Connection, ${this.shortUrl}] Closing connection`); + const socket = this.ws; + const retryTimeout = this.retryTimeout; + if (socket !== null) { + socket.close(); } + if (retryTimeout !== null) { + clearTimeout(retryTimeout); + } + this.retryTimeout = null; + this.ws = null; } } diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js index 9018c61..374bba2 100644 --- a/web/src/app/ConnectionManager.js +++ b/web/src/app/ConnectionManager.js @@ -14,10 +14,12 @@ export class ConnectionManager { subscriptions.forEach((id, subscription) => { const added = !this.connections.get(id) if (added) { - const wsUrl = subscription.wsUrl(); - const connection = new Connection(wsUrl, id, onNotification); + const baseUrl = subscription.baseUrl; + const topic = subscription.topic; + const since = 0; + const connection = new Connection(id, baseUrl, topic, since, onNotification); this.connections.set(id, connection); - console.log(`[ConnectionManager] Starting new connection ${id} using URL ${wsUrl}`); + console.log(`[ConnectionManager] Starting new connection ${id}`); connection.start(); } }); @@ -27,7 +29,7 @@ export class ConnectionManager { console.log(`[ConnectionManager] Closing connection ${id}`); const connection = this.connections.get(id); this.connections.delete(id); - connection.cancel(); + connection.close(); }); } } diff --git a/web/src/app/Subscription.js b/web/src/app/Subscription.js index 8046cff..3a1268f 100644 --- a/web/src/app/Subscription.js +++ b/web/src/app/Subscription.js @@ -1,24 +1,15 @@ import {topicUrl, shortTopicUrl, topicUrlWs} from './utils'; export default class Subscription { - id = ''; - baseUrl = ''; - topic = ''; - notifications = []; - lastActive = null; - constructor(baseUrl, topic) { this.id = topicUrl(baseUrl, topic); this.baseUrl = baseUrl; this.topic = topic; + this.notifications = new Map(); } addNotification(notification) { - if (notification.time === null) { - return this; - } - this.notifications.push(notification); - this.lastActive = notification.time; + this.notifications.set(notification.id, notification); return this; } @@ -27,12 +18,8 @@ export default class Subscription { return this; } - url() { - return this.id; - } - - wsUrl() { - return topicUrlWs(this.baseUrl, this.topic); + getNotifications() { + return Array.from(this.notifications.values()); } shortUrl() { diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 794af81..3f8573c 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -1,5 +1,5 @@ export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; -export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws` +export const topicUrlWs = (baseUrl, topic, since) => `${topicUrl(baseUrl, topic)}/ws?since=${since}` .replaceAll("https://", "wss://") .replaceAll("http://", "ws://"); export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; diff --git a/web/src/components/AddDialog.js b/web/src/components/AddDialog.js index 837133c..c1b1fe2 100644 --- a/web/src/components/AddDialog.js +++ b/web/src/components/AddDialog.js @@ -9,7 +9,8 @@ import DialogTitle from '@mui/material/DialogTitle'; import {useState} from "react"; import Subscription from "../app/Subscription"; -const defaultBaseUrl = "https://ntfy.sh" +const defaultBaseUrl = "http://127.0.0.1" +//const defaultBaseUrl = "https://ntfy.sh" const AddDialog = (props) => { const [topic, setTopic] = useState(""); diff --git a/web/src/components/App.js b/web/src/components/App.js index 1181b38..d63b928 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -101,7 +101,7 @@ const SubscriptionNavItem = (props) => { } const App = () => { - console.log("Launching App component"); + console.log(`[App] Rendering main view`); const [drawerOpen, setDrawerOpen] = useState(true); const [subscriptions, setSubscriptions] = useState(new Subscriptions()); @@ -114,6 +114,7 @@ const App = () => { }); }; const handleSubscribeSubmit = (subscription) => { + console.log(`[App] New subscription: ${subscription.id}`); setSubscribeDialogOpen(false); setSubscriptions(prev => prev.add(subscription).clone()); setSelectedSubscription(subscription); @@ -126,10 +127,11 @@ const App = () => { }); }; const handleSubscribeCancel = () => { - console.log(`Cancel clicked`); + console.log(`[App] Cancel clicked`); setSubscribeDialogOpen(false); }; const handleUnsubscribe = (subscriptionId) => { + console.log(`[App] Unsubscribing from ${subscriptionId}`); setSubscriptions(prev => { const newSubscriptions = prev.remove(subscriptionId).clone(); setSelectedSubscription(newSubscriptions.firstOrNull()); @@ -137,10 +139,10 @@ const App = () => { }); }; const handleSubscriptionClick = (subscriptionId) => { - console.log(`Selected subscription ${subscriptionId}`); + console.log(`[App] Selected ${subscriptionId}`); setSelectedSubscription(subscriptions.get(subscriptionId)); }; - const notifications = (selectedSubscription !== null) ? selectedSubscription.notifications : []; + const notifications = (selectedSubscription !== null) ? selectedSubscription.getNotifications() : []; const toggleDrawer = () => { setDrawerOpen(!drawerOpen); };