Reconnect on failure, with backoff; Deduping notifications
This commit is contained in:
		
							parent
							
								
									3fac1c3432
								
							
						
					
					
						commit
						1536201e9a
					
				
					 6 changed files with 62 additions and 45 deletions
				
			
		|  | @ -1,18 +1,31 @@ | ||||||
|  | import {shortTopicUrl, topicUrlWs} from "./utils"; | ||||||
|  | 
 | ||||||
|  | const retryBackoffSeconds = [5, 10, 15, 20, 30, 45, 60, 120]; | ||||||
|  | 
 | ||||||
| class Connection { | class Connection { | ||||||
|     constructor(wsUrl, subscriptionId, onNotification) { |     constructor(subscriptionId, baseUrl, topic, since, onNotification) { | ||||||
|         this.wsUrl = wsUrl; |  | ||||||
|         this.subscriptionId = subscriptionId; |         this.subscriptionId = subscriptionId; | ||||||
|  |         this.baseUrl = baseUrl; | ||||||
|  |         this.topic = topic; | ||||||
|  |         this.since = since; | ||||||
|  |         this.shortUrl = shortTopicUrl(baseUrl, topic); | ||||||
|         this.onNotification = onNotification; |         this.onNotification = onNotification; | ||||||
|         this.ws = null; |         this.ws = null; | ||||||
|  |         this.retryCount = 0; | ||||||
|  |         this.retryTimeout = null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     start() { |     start() { | ||||||
|         const socket = new WebSocket(this.wsUrl); |         const since = (this.since === 0) ? "all" : this.since.toString(); | ||||||
|         socket.onopen = (event) => { |         const wsUrl = topicUrlWs(this.baseUrl, this.topic, since); | ||||||
|             console.log(`[Connection] [${this.subscriptionId}] Connection established`); |         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) => { |         this.ws.onmessage = (event) => { | ||||||
|             console.log(`[Connection] [${this.subscriptionId}] Message received from server: ${event.data}`); |             console.log(`[Connection, ${this.shortUrl}] Message received from server: ${event.data}`); | ||||||
|             try { |             try { | ||||||
|                 const data = JSON.parse(event.data); |                 const data = JSON.parse(event.data); | ||||||
|                 const relevantAndValid = |                 const relevantAndValid = | ||||||
|  | @ -21,31 +34,43 @@ class Connection { | ||||||
|                     'time' in data && |                     'time' in data && | ||||||
|                     'message' in data; |                     'message' in data; | ||||||
|                 if (!relevantAndValid) { |                 if (!relevantAndValid) { | ||||||
|  |                     console.log(`[Connection, ${this.shortUrl}] Message irrelevant or invalid. Ignoring.`); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|  |                 this.since = data.time; | ||||||
|                 this.onNotification(this.subscriptionId, data); |                 this.onNotification(this.subscriptionId, data); | ||||||
|             } catch (e) { |             } 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) { |             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 { |             } 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) => { |         this.ws.onerror = (event) => { | ||||||
|             console.log(this.subscriptionId, `[Connection] [${this.subscriptionId}] ${event.message}`); |             console.log(`[Connection, ${this.shortUrl}] Error occurred: ${event}`, event); | ||||||
|         }; |         }; | ||||||
|         this.ws = socket; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     cancel() { |     close() { | ||||||
|         if (this.ws !== null) { |         console.log(`[Connection, ${this.shortUrl}] Closing connection`); | ||||||
|             this.ws.close(); |         const socket = this.ws; | ||||||
|             this.ws = null; |         const retryTimeout = this.retryTimeout; | ||||||
|  |         if (socket !== null) { | ||||||
|  |             socket.close(); | ||||||
|         } |         } | ||||||
|  |         if (retryTimeout !== null) { | ||||||
|  |             clearTimeout(retryTimeout); | ||||||
|  |         } | ||||||
|  |         this.retryTimeout = null; | ||||||
|  |         this.ws = null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,10 +14,12 @@ export class ConnectionManager { | ||||||
|         subscriptions.forEach((id, subscription) => { |         subscriptions.forEach((id, subscription) => { | ||||||
|             const added = !this.connections.get(id) |             const added = !this.connections.get(id) | ||||||
|             if (added) { |             if (added) { | ||||||
|                 const wsUrl = subscription.wsUrl(); |                 const baseUrl = subscription.baseUrl; | ||||||
|                 const connection = new Connection(wsUrl, id, onNotification); |                 const topic = subscription.topic; | ||||||
|  |                 const since = 0; | ||||||
|  |                 const connection = new Connection(id, baseUrl, topic, since, onNotification); | ||||||
|                 this.connections.set(id, connection); |                 this.connections.set(id, connection); | ||||||
|                 console.log(`[ConnectionManager] Starting new connection ${id} using URL ${wsUrl}`); |                 console.log(`[ConnectionManager] Starting new connection ${id}`); | ||||||
|                 connection.start(); |                 connection.start(); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  | @ -27,7 +29,7 @@ export class ConnectionManager { | ||||||
|             console.log(`[ConnectionManager] Closing connection ${id}`); |             console.log(`[ConnectionManager] Closing connection ${id}`); | ||||||
|             const connection = this.connections.get(id); |             const connection = this.connections.get(id); | ||||||
|             this.connections.delete(id); |             this.connections.delete(id); | ||||||
|             connection.cancel(); |             connection.close(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,24 +1,15 @@ | ||||||
| import {topicUrl, shortTopicUrl, topicUrlWs} from './utils'; | import {topicUrl, shortTopicUrl, topicUrlWs} from './utils'; | ||||||
| 
 | 
 | ||||||
| export default class Subscription { | export default class Subscription { | ||||||
|     id = ''; |  | ||||||
|     baseUrl = ''; |  | ||||||
|     topic = ''; |  | ||||||
|     notifications = []; |  | ||||||
|     lastActive = null; |  | ||||||
| 
 |  | ||||||
|     constructor(baseUrl, topic) { |     constructor(baseUrl, topic) { | ||||||
|         this.id = topicUrl(baseUrl, topic); |         this.id = topicUrl(baseUrl, topic); | ||||||
|         this.baseUrl = baseUrl; |         this.baseUrl = baseUrl; | ||||||
|         this.topic = topic; |         this.topic = topic; | ||||||
|  |         this.notifications = new Map(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addNotification(notification) { |     addNotification(notification) { | ||||||
|         if (notification.time === null) { |         this.notifications.set(notification.id, notification); | ||||||
|             return this; |  | ||||||
|         } |  | ||||||
|         this.notifications.push(notification); |  | ||||||
|         this.lastActive = notification.time; |  | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -27,12 +18,8 @@ export default class Subscription { | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     url() { |     getNotifications() { | ||||||
|         return this.id; |         return Array.from(this.notifications.values()); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     wsUrl() { |  | ||||||
|         return topicUrlWs(this.baseUrl, this.topic); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     shortUrl() { |     shortUrl() { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; | 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("https://", "wss://") | ||||||
|     .replaceAll("http://", "ws://"); |     .replaceAll("http://", "ws://"); | ||||||
| export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; | export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; | ||||||
|  |  | ||||||
|  | @ -9,7 +9,8 @@ import DialogTitle from '@mui/material/DialogTitle'; | ||||||
| import {useState} from "react"; | import {useState} from "react"; | ||||||
| import Subscription from "../app/Subscription"; | 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 AddDialog = (props) => { | ||||||
|     const [topic, setTopic] = useState(""); |     const [topic, setTopic] = useState(""); | ||||||
|  |  | ||||||
|  | @ -101,7 +101,7 @@ const SubscriptionNavItem = (props) => { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const App = () => { | const App = () => { | ||||||
|     console.log("Launching App component"); |     console.log(`[App] Rendering main view`); | ||||||
| 
 | 
 | ||||||
|     const [drawerOpen, setDrawerOpen] = useState(true); |     const [drawerOpen, setDrawerOpen] = useState(true); | ||||||
|     const [subscriptions, setSubscriptions] = useState(new Subscriptions()); |     const [subscriptions, setSubscriptions] = useState(new Subscriptions()); | ||||||
|  | @ -114,6 +114,7 @@ const App = () => { | ||||||
|         }); |         }); | ||||||
|     }; |     }; | ||||||
|     const handleSubscribeSubmit = (subscription) => { |     const handleSubscribeSubmit = (subscription) => { | ||||||
|  |         console.log(`[App] New subscription: ${subscription.id}`); | ||||||
|         setSubscribeDialogOpen(false); |         setSubscribeDialogOpen(false); | ||||||
|         setSubscriptions(prev => prev.add(subscription).clone()); |         setSubscriptions(prev => prev.add(subscription).clone()); | ||||||
|         setSelectedSubscription(subscription); |         setSelectedSubscription(subscription); | ||||||
|  | @ -126,10 +127,11 @@ const App = () => { | ||||||
|             }); |             }); | ||||||
|     }; |     }; | ||||||
|     const handleSubscribeCancel = () => { |     const handleSubscribeCancel = () => { | ||||||
|         console.log(`Cancel clicked`); |         console.log(`[App] Cancel clicked`); | ||||||
|         setSubscribeDialogOpen(false); |         setSubscribeDialogOpen(false); | ||||||
|     }; |     }; | ||||||
|     const handleUnsubscribe = (subscriptionId) => { |     const handleUnsubscribe = (subscriptionId) => { | ||||||
|  |         console.log(`[App] Unsubscribing from ${subscriptionId}`); | ||||||
|         setSubscriptions(prev => { |         setSubscriptions(prev => { | ||||||
|             const newSubscriptions = prev.remove(subscriptionId).clone(); |             const newSubscriptions = prev.remove(subscriptionId).clone(); | ||||||
|             setSelectedSubscription(newSubscriptions.firstOrNull()); |             setSelectedSubscription(newSubscriptions.firstOrNull()); | ||||||
|  | @ -137,10 +139,10 @@ const App = () => { | ||||||
|         }); |         }); | ||||||
|     }; |     }; | ||||||
|     const handleSubscriptionClick = (subscriptionId) => { |     const handleSubscriptionClick = (subscriptionId) => { | ||||||
|         console.log(`Selected subscription ${subscriptionId}`); |         console.log(`[App] Selected ${subscriptionId}`); | ||||||
|         setSelectedSubscription(subscriptions.get(subscriptionId)); |         setSelectedSubscription(subscriptions.get(subscriptionId)); | ||||||
|     }; |     }; | ||||||
|     const notifications = (selectedSubscription !== null) ? selectedSubscription.notifications : []; |     const notifications = (selectedSubscription !== null) ? selectedSubscription.getNotifications() : []; | ||||||
|     const toggleDrawer = () => { |     const toggleDrawer = () => { | ||||||
|         setDrawerOpen(!drawerOpen); |         setDrawerOpen(!drawerOpen); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue