Make connections react on changes of users; this works wonderfully

This commit is contained in:
Philipp Heckel 2022-03-03 20:07:35 -05:00
parent 08846e4cc2
commit 695e029147
4 changed files with 49 additions and 31 deletions

View file

@ -3,7 +3,8 @@ import {basicAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils";
const retryBackoffSeconds = [5, 10, 15, 20, 30, 45]; const retryBackoffSeconds = [5, 10, 15, 20, 30, 45];
class Connection { class Connection {
constructor(subscriptionId, baseUrl, topic, user, since, onNotification) { constructor(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification) {
this.connectionId = connectionId;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.topic = topic; this.topic = topic;
@ -21,15 +22,15 @@ class Connection {
// we don't want to re-trigger the main view re-render potentially hundreds of times. // we don't want to re-trigger the main view re-render potentially hundreds of times.
const wsUrl = this.wsUrl(); const wsUrl = this.wsUrl();
console.log(`[Connection, ${this.shortUrl}] Opening connection to ${wsUrl}`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`);
this.ws = new WebSocket(wsUrl); this.ws = new WebSocket(wsUrl);
this.ws.onopen = (event) => { this.ws.onopen = (event) => {
console.log(`[Connection, ${this.shortUrl}] Connection established`, event); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, event);
this.retryCount = 0; this.retryCount = 0;
} }
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
console.log(`[Connection, ${this.shortUrl}] Message received from server: ${event.data}`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`);
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.event === 'open') { if (data.event === 'open') {
@ -41,33 +42,33 @@ class Connection {
'time' in data && 'time' in data &&
'message' in data; 'message' in data;
if (!relevantAndValid) { if (!relevantAndValid) {
console.log(`[Connection, ${this.shortUrl}] Unexpected message. Ignoring.`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`);
return; return;
} }
this.since = data.id; this.since = data.id;
this.onNotification(this.subscriptionId, data); this.onNotification(this.subscriptionId, data);
} catch (e) { } catch (e) {
console.log(`[Connection, ${this.shortUrl}] Error handling message: ${e}`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`);
} }
}; };
this.ws.onclose = (event) => { this.ws.onclose = (event) => {
if (event.wasClean) { if (event.wasClean) {
console.log(`[Connection, ${this.shortUrl}] Connection closed cleanly, code=${event.code} reason=${event.reason}`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
this.ws = null; this.ws = null;
} else { } else {
const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length-1)]; const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length-1)];
this.retryCount++; this.retryCount++;
console.log(`[Connection, ${this.shortUrl}] Connection died, retrying in ${retrySeconds} seconds`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`);
this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000); this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000);
} }
}; };
this.ws.onerror = (event) => { this.ws.onerror = (event) => {
console.log(`[Connection, ${this.shortUrl}] Error occurred: ${event}`, event); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event);
}; };
} }
close() { close() {
console.log(`[Connection, ${this.shortUrl}] Closing connection`); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`);
const socket = this.ws; const socket = this.ws;
const retryTimeout = this.retryTimeout; const retryTimeout = this.retryTimeout;
if (socket !== null) { if (socket !== null) {

View file

@ -1,30 +1,39 @@
import Connection from "./Connection"; import Connection from "./Connection";
import {sha256} from "./utils";
class ConnectionManager { class ConnectionManager {
constructor() { constructor() {
this.connections = new Map(); this.connections = new Map(); // ConnectionId -> Connection (hash, see below)
} }
refresh(subscriptions, users, onNotification) { async refresh(subscriptions, users, onNotification) {
if (!subscriptions || !users) { if (!subscriptions || !users) {
return; return;
} }
console.log(`[ConnectionManager] Refreshing connections`); console.log(`[ConnectionManager] Refreshing connections`);
const subscriptionIds = subscriptions.map(s => s.id); const subscriptionsWithUsersAndConnectionId = await Promise.all(subscriptions
const deletedIds = Array.from(this.connections.keys()).filter(id => !subscriptionIds.includes(id)); .map(async s => {
const [user] = users.filter(u => u.baseUrl === s.baseUrl);
const connectionId = await makeConnectionId(s, user);
return {...s, user, connectionId};
}));
const activeIds = subscriptionsWithUsersAndConnectionId.map(s => s.connectionId);
const deletedIds = Array.from(this.connections.keys()).filter(id => !activeIds.includes(id));
console.log(subscriptionsWithUsersAndConnectionId);
// Create and add new connections // Create and add new connections
subscriptions.forEach(subscription => { subscriptionsWithUsersAndConnectionId.forEach(subscription => {
const id = subscription.id; const subscriptionId = subscription.id;
const added = !this.connections.get(id) const connectionId = subscription.connectionId;
const added = !this.connections.get(connectionId)
if (added) { if (added) {
const baseUrl = subscription.baseUrl; const baseUrl = subscription.baseUrl;
const topic = subscription.topic; const topic = subscription.topic;
const [user] = users.filter(user => user.baseUrl === baseUrl); const user = subscription.user;
const since = subscription.last; const since = subscription.last;
const connection = new Connection(id, baseUrl, topic, user, since, onNotification); const connection = new Connection(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification);
this.connections.set(id, connection); this.connections.set(connectionId, connection);
console.log(`[ConnectionManager] Starting new connection ${id}`); console.log(`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`);
connection.start(); connection.start();
} }
}); });
@ -39,5 +48,12 @@ class ConnectionManager {
} }
} }
const makeConnectionId = async (subscription, user) => {
const hash = (user)
? await sha256(`${subscription.id}|${user.username}|${user.password}`)
: await sha256(`${subscription.id}`);
return hash.substring(0, 10);
}
const connectionManager = new ConnectionManager(); const connectionManager = new ConnectionManager();
export default connectionManager; export default connectionManager;

View file

@ -90,6 +90,12 @@ export const encodeBase64Url = (s) => {
.replaceAll('=', ''); .replaceAll('=', '');
} }
// https://jameshfisher.com/2017/10/30/web-cryptography-api-hello-world/
export const sha256 = async (s) => {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder("utf-8").encode(s));
return Array.prototype.map.call(new Uint8Array(buf), x=>(('00'+x.toString(16)).slice(-2))).join('');
}
export const formatShortDateTime = (timestamp) => { export const formatShortDateTime = (timestamp) => {
return new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'}) return new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'})
.format(new Date(timestamp * 1000)); .format(new Date(timestamp * 1000));

View file

@ -19,14 +19,11 @@ import pruner from "../app/Pruner";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
// TODO subscribe dialog: // TODO subscribe dialog check/use existing user
// - check/use existing user
// - add baseUrl
// TODO embed into ntfy server
// TODO make default server functional // TODO make default server functional
// TODO business logic with callbacks // TODO routing
// TODO embed into ntfy server
// TODO connection indicator in subscription list // TODO connection indicator in subscription list
// TODO connectionmanager should react on users changes
const App = () => { const App = () => {
console.log(`[App] Rendering main view`); console.log(`[App] Rendering main view`);
@ -53,9 +50,7 @@ const App = () => {
setSelectedSubscription(newSelected); setSelectedSubscription(newSelected);
}; };
const handleRequestPermission = () => { const handleRequestPermission = () => {
notificationManager.maybeRequestPermission((granted) => { notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted));
setNotificationsGranted(granted);
})
}; };
const handlePrefsClick = () => { const handlePrefsClick = () => {
setPrefsOpen(true); setPrefsOpen(true);
@ -91,7 +86,7 @@ const App = () => {
console.error(`[App] Error handling notification`, e); console.error(`[App] Error handling notification`, e);
} }
}; };
connectionManager.refresh(subscriptions, users, handleNotification); connectionManager.refresh(subscriptions, users, handleNotification); // Dangle
}, [subscriptions, users]); }, [subscriptions, users]);
useEffect(() => { useEffect(() => {
const subscriptionId = (selectedSubscription) ? selectedSubscription.id : ""; const subscriptionId = (selectedSubscription) ? selectedSubscription.id : "";