Просмотр исходного кода

Refactor the db; move to *Manager classes

Philipp Heckel 4 лет назад
Родитель
Сommit
08846e4cc2

+ 6 - 6
web/src/app/Api.js

@@ -1,17 +1,17 @@
 import {
-    topicUrlJsonPoll,
     fetchLinesIterator,
-    topicUrl,
-    topicUrlAuth,
     maybeWithBasicAuth,
     topicShortUrl,
+    topicUrl,
+    topicUrlAuth,
+    topicUrlJsonPoll,
     topicUrlJsonPollWithSince
 } from "./utils";
-import db from "./db";
+import userManager from "./UserManager";
 
 class Api {
     async poll(baseUrl, topic, since) {
-        const user = await db.users.get(baseUrl);
+        const user = await userManager.get(baseUrl);
         const shortUrl = topicShortUrl(baseUrl, topic);
         const url = (since)
             ? topicUrlJsonPollWithSince(baseUrl, topic, since)
@@ -27,7 +27,7 @@ class Api {
     }
 
     async publish(baseUrl, topic, message) {
-        const user = await db.users.get(baseUrl);
+        const user = await userManager.get(baseUrl);
         const url = topicUrl(baseUrl, topic);
         console.log(`[Api] Publishing message to ${url}`);
         await fetch(url, {

+ 3 - 1
web/src/app/NotificationManager.js

@@ -1,8 +1,10 @@
 import {formatMessage, formatTitleWithFallback, topicShortUrl} from "./utils";
 import prefs from "./Prefs";
+import subscriptionManager from "./SubscriptionManager";
 
 class NotificationManager {
-    async notify(subscription, notification, onClickFallback) {
+    async notify(subscriptionId, notification, onClickFallback) {
+        const subscription = await subscriptionManager.get(subscriptionId);
         const shouldNotify = await this.shouldNotify(subscription, notification);
         if (!shouldNotify) {
             return;

+ 16 - 6
web/src/app/Poller.js

@@ -1,5 +1,6 @@
 import db from "./db";
 import api from "./Api";
+import subscriptionManager from "./SubscriptionManager";
 
 const delayMillis = 3000; // 3 seconds
 const intervalMillis = 300000; // 5 minutes
@@ -19,7 +20,7 @@ class Poller {
 
     async pollAll() {
         console.log(`[Poller] Polling all subscriptions`);
-        const subscriptions = await db.subscriptions.toArray();
+        const subscriptions = await subscriptionManager.all();
         for (const s of subscriptions) {
             try {
                 await this.poll(s);
@@ -38,11 +39,20 @@ class Poller {
             console.log(`[Poller] No new notifications found for ${subscription.id}`);
             return;
         }
-        const notificationsWithSubscriptionId = notifications
-            .map(notification => ({ ...notification, subscriptionId: subscription.id }));
-        await db.notifications.bulkPut(notificationsWithSubscriptionId); // FIXME
-        await db.subscriptions.update(subscription.id, {last: notifications.at(-1).id}); // FIXME
-    };
+        console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`);
+        await subscriptionManager.addNotifications(subscription.id, notifications);
+    }
+
+    pollInBackground(subscription) {
+        const fn = async () => {
+            try {
+                await this.poll(subscription);
+            } catch (e) {
+                console.error(`[App] Error polling subscription ${subscription.id}`, e);
+            }
+        };
+        setTimeout(() => fn(), 0);
+    }
 }
 
 const poller = new Poller();

+ 2 - 4
web/src/app/Pruner.js

@@ -1,5 +1,5 @@
-import db from "./db";
 import prefs from "./Prefs";
+import subscriptionManager from "./SubscriptionManager";
 
 const delayMillis = 15000; // 15 seconds
 const intervalMillis = 1800000; // 30 minutes
@@ -26,9 +26,7 @@ class Pruner {
         }
         console.log(`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`);
         try {
-            await db.notifications
-                .where("time").below(pruneThresholdTimestamp)
-                .delete();
+            await subscriptionManager.pruneNotifications(pruneThresholdTimestamp);
         } catch (e) {
             console.log(`[Pruner] Error pruning old subscriptions`, e);
         }

+ 75 - 0
web/src/app/SubscriptionManager.js

@@ -0,0 +1,75 @@
+import db from "./db";
+
+class SubscriptionManager {
+    async all() {
+        return db.subscriptions.toArray();
+    }
+
+    async get(subscriptionId) {
+        return await db.subscriptions.get(subscriptionId)
+    }
+
+    async save(subscription) {
+        await db.subscriptions.put(subscription);
+    }
+
+    async remove(subscriptionId) {
+        await db.subscriptions.delete(subscriptionId);
+        await db.notifications
+            .where({subscriptionId: subscriptionId})
+            .delete();
+    }
+
+    async first() {
+        return db.subscriptions.toCollection().first(); // May be undefined
+    }
+
+    async getNotifications(subscriptionId) {
+        return db.notifications
+            .where({ subscriptionId: subscriptionId })
+            .toArray();
+    }
+
+    /** Adds notification, or returns false if it already exists */
+    async addNotification(subscriptionId, notification) {
+        const exists = await db.notifications.get(notification.id);
+        if (exists) {
+            return false;
+        }
+        await db.notifications.add({ ...notification, subscriptionId });
+        await db.subscriptions.update(subscriptionId, {
+            last: notification.id
+        });
+        return true;
+    }
+
+    /** Adds/replaces notifications, will not throw if they exist */
+    async addNotifications(subscriptionId, notifications) {
+        const notificationsWithSubscriptionId = notifications
+            .map(notification => ({ ...notification, subscriptionId }));
+        const lastNotificationId = notifications.at(-1).id;
+        await db.notifications.bulkPut(notificationsWithSubscriptionId);
+        await db.subscriptions.update(subscriptionId, {
+            last: lastNotificationId
+        });
+    }
+
+    async deleteNotification(notificationId) {
+        await db.notifications.delete(notificationId);
+    }
+
+    async deleteNotifications(subscriptionId) {
+        await db.notifications
+            .where({subscriptionId: subscriptionId})
+            .delete();
+    }
+
+    async pruneNotifications(thresholdTimestamp) {
+        await db.notifications
+            .where("time").below(thresholdTimestamp)
+            .delete();
+    }
+}
+
+const subscriptionManager = new SubscriptionManager();
+export default subscriptionManager;

+ 22 - 0
web/src/app/UserManager.js

@@ -0,0 +1,22 @@
+import db from "./db";
+
+class UserManager {
+    async all() {
+        return db.users.toArray();
+    }
+
+    async get(baseUrl) {
+        return db.users.get(baseUrl);
+    }
+
+    async save(user) {
+        await db.users.put(user);
+    }
+
+    async delete(baseUrl) {
+        await db.users.delete(baseUrl);
+    }
+}
+
+const userManager = new UserManager();
+export default userManager;

+ 2 - 2
web/src/components/ActionBar.js

@@ -4,7 +4,7 @@ import Toolbar from "@mui/material/Toolbar";
 import IconButton from "@mui/material/IconButton";
 import MenuIcon from "@mui/icons-material/Menu";
 import Typography from "@mui/material/Typography";
-import IconSubscribeSettings from "./IconSubscribeSettings";
+import SubscribeSettings from "./SubscribeSettings";
 import * as React from "react";
 import Box from "@mui/material/Box";
 import {topicShortUrl} from "../app/utils";
@@ -36,7 +36,7 @@ const ActionBar = (props) => {
                 <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
                     {title}
                 </Typography>
-                {props.selectedSubscription !== null && <IconSubscribeSettings
+                {props.selectedSubscription !== null && <SubscribeSettings
                     subscription={props.selectedSubscription}
                     onUnsubscribe={props.onUnsubscribe}
                 />}

+ 11 - 21
web/src/components/App.js

@@ -13,10 +13,11 @@ import ActionBar from "./ActionBar";
 import notificationManager from "../app/NotificationManager";
 import NoTopics from "./NoTopics";
 import Preferences from "./Preferences";
-import db from "../app/db";
 import {useLiveQuery} from "dexie-react-hooks";
 import poller from "../app/Poller";
 import pruner from "../app/Pruner";
+import subscriptionManager from "../app/SubscriptionManager";
+import userManager from "../app/UserManager";
 
 // TODO subscribe dialog:
 //  - check/use existing user
@@ -26,7 +27,6 @@ import pruner from "../app/Pruner";
 // TODO business logic with callbacks
 // TODO connection indicator in subscription list
 // TODO connectionmanager should react on users changes
-// TODO attachments
 
 const App = () => {
     console.log(`[App] Rendering main view`);
@@ -35,31 +35,21 @@ const App = () => {
     const [prefsOpen, setPrefsOpen] = useState(false);
     const [selectedSubscription, setSelectedSubscription] = useState(null);
     const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
-    const subscriptions = useLiveQuery(() => db.subscriptions.toArray());
-    const users = useLiveQuery(() => db.users.toArray());
+    const subscriptions = useLiveQuery(() => subscriptionManager.all());
+    const users = useLiveQuery(() => userManager.all());
     const handleSubscriptionClick = async (subscriptionId) => {
-        const subscription = await db.subscriptions.get(subscriptionId); // FIXME
+        const subscription = await subscriptionManager.get(subscriptionId);
         setSelectedSubscription(subscription);
         setPrefsOpen(false);
     }
     const handleSubscribeSubmit = async (subscription) => {
         console.log(`[App] New subscription: ${subscription.id}`, subscription);
-        await db.subscriptions.put(subscription); // FIXME
         setSelectedSubscription(subscription);
         handleRequestPermission();
-        try {
-            await poller.poll(subscription);
-        } catch (e) {
-            console.error(`[App] Error polling newly added subscription ${subscription.id}`, e);
-        }
     };
     const handleUnsubscribe = async (subscriptionId) => {
         console.log(`[App] Unsubscribing from ${subscriptionId}`);
-        await db.subscriptions.delete(subscriptionId); // FIXME
-        await db.notifications
-            .where({subscriptionId: subscriptionId})
-            .delete(); // FIXME
-        const newSelected = await db.subscriptions.toCollection().first(); // FIXME May be undefined
+        const newSelected = await subscriptionManager.first(); // May be undefined
         setSelectedSubscription(newSelected);
     };
     const handleRequestPermission = () => {
@@ -77,7 +67,7 @@ const App = () => {
         poller.startWorker();
         pruner.startWorker();
         const load = async () => {
-            const subs = await db.subscriptions.toArray(); // Cannot be 'subscriptions'
+            const subs = await subscriptionManager.all();             // FIXME this is broken
             const selectedSubscriptionId = await prefs.selectedSubscriptionId();
 
             // Set selected subscription
@@ -93,10 +83,10 @@ const App = () => {
         const notificationClickFallback = (subscription) => setSelectedSubscription(subscription);
         const handleNotification = async (subscriptionId, notification) => {
             try {
-                const subscription = await db.subscriptions.get(subscriptionId); // FIXME
-                await db.notifications.add({ ...notification, subscriptionId }); // FIXME, will throw if exists!
-                await db.subscriptions.update(subscriptionId, { last: notification.id });
-                await notificationManager.notify(subscription, notification, notificationClickFallback)
+                const added = await subscriptionManager.addNotification(subscriptionId, notification);
+                if (added) {
+                    await notificationManager.notify(subscriptionId, notification, notificationClickFallback)
+                }
             } catch (e) {
                 console.error(`[App] Error handling notification`, e);
             }

+ 5 - 8
web/src/components/Notifications.js

@@ -1,25 +1,22 @@
 import Container from "@mui/material/Container";
-import {ButtonBase, CardActions, CardContent, Fade, Link, Modal, Stack, styled} from "@mui/material";
+import {ButtonBase, CardActions, CardContent, Fade, Link, Modal, Stack} from "@mui/material";
 import Card from "@mui/material/Card";
 import Typography from "@mui/material/Typography";
 import * as React from "react";
+import {useState} from "react";
 import {formatBytes, formatMessage, formatShortDateTime, formatTitle, topicShortUrl, unmatchedTags} from "../app/utils";
 import IconButton from "@mui/material/IconButton";
 import CloseIcon from '@mui/icons-material/Close';
 import {Paragraph, VerticallyCenteredContainer} from "./styles";
 import {useLiveQuery} from "dexie-react-hooks";
-import db from "../app/db";
 import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
-import theme from "./theme";
-import {useState} from "react";
+import subscriptionManager from "../app/SubscriptionManager";
 
 const Notifications = (props) => {
     const subscription = props.subscription;
     const notifications = useLiveQuery(() => {
-        return db.notifications
-            .where({ subscriptionId: subscription.id })
-            .toArray();
+        return subscriptionManager.getNotifications(subscription.id);
     }, [subscription]);
     if (!notifications || notifications.length === 0) {
         return <NothingHereYet subscription={subscription}/>;
@@ -49,7 +46,7 @@ const NotificationItem = (props) => {
     const tags = (otherTags.length > 0) ? otherTags.join(', ') : null;
     const handleDelete = async () => {
         console.log(`[Notifications] Deleting notification ${notification.id} from ${subscriptionId}`);
-        await db.notifications.delete(notification.id); // FIXME
+        await subscriptionManager.deleteNotification(notification.id)
     }
     const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000;
     return (

+ 5 - 4
web/src/components/Preferences.js

@@ -32,6 +32,7 @@ import Dialog from "@mui/material/Dialog";
 import DialogTitle from "@mui/material/DialogTitle";
 import DialogContent from "@mui/material/DialogContent";
 import DialogActions from "@mui/material/DialogActions";
+import userManager from "../app/UserManager";
 
 const Preferences = (props) => {
     return (
@@ -165,7 +166,7 @@ const DefaultServer = (props) => {
 const Users = () => {
     const [dialogKey, setDialogKey] = useState(0);
     const [dialogOpen, setDialogOpen] = useState(false);
-    const users = useLiveQuery(() => db.users.toArray());
+    const users = useLiveQuery(() => userManager.all());
     const handleAddClick = () => {
         setDialogKey(prev => prev+1);
         setDialogOpen(true);
@@ -176,7 +177,7 @@ const Users = () => {
     const handleDialogSubmit = async (user) => {
         setDialogOpen(false);
         try {
-            await db.users.add(user);
+            await userManager.save(user);
             console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`);
         } catch (e) {
             console.log(`[Preferences] Error adding user.`, e);
@@ -224,7 +225,7 @@ const UserTable = (props) => {
     const handleDialogSubmit = async (user) => {
         setDialogOpen(false);
         try {
-            await db.users.put(user); // put() is an upsert
+            await userManager.save(user);
             console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`);
         } catch (e) {
             console.log(`[Preferences] Error updating user.`, e);
@@ -232,7 +233,7 @@ const UserTable = (props) => {
     };
     const handleDeleteClick = async (user) => {
         try {
-            await db.users.delete(user.baseUrl);
+            await userManager.delete(user.baseUrl);
             console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`);
         } catch (e) {
             console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);

+ 7 - 3
web/src/components/SubscribeDialog.js

@@ -12,7 +12,9 @@ import theme from "./theme";
 import api from "../app/Api";
 import {topicUrl, validTopic, validUrl} from "../app/utils";
 import Box from "@mui/material/Box";
-import db from "../app/db";
+import userManager from "../app/UserManager";
+import subscriptionManager from "../app/SubscriptionManager";
+import poller from "../app/Poller";
 
 const defaultBaseUrl = "http://127.0.0.1"
 //const defaultBaseUrl = "https://ntfy.sh"
@@ -22,7 +24,7 @@ const SubscribeDialog = (props) => {
     const [topic, setTopic] = useState("");
     const [showLoginPage, setShowLoginPage] = useState(false);
     const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
-    const handleSuccess = () => {
+    const handleSuccess = async () => {
         const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME
         const subscription = {
             id: topicUrl(actualBaseUrl, topic),
@@ -30,6 +32,8 @@ const SubscribeDialog = (props) => {
             topic: topic,
             last: null
         };
+        await subscriptionManager.save(subscription);
+        poller.pollInBackground(subscription); // Dangle!
         props.onSuccess(subscription);
     }
     return (
@@ -141,7 +145,7 @@ const LoginPage = (props) => {
             return;
         }
         console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
-        db.users.put(user);
+        await userManager.save(user);
         props.onSuccess();
     };
     return (

+ 8 - 9
web/src/components/IconSubscribeSettings.js → web/src/components/SubscribeSettings.js

@@ -9,10 +9,10 @@ import MenuList from '@mui/material/MenuList';
 import IconButton from "@mui/material/IconButton";
 import MoreVertIcon from "@mui/icons-material/MoreVert";
 import api from "../app/Api";
-import db from "../app/db";
+import subscriptionManager from "../app/SubscriptionManager";
 
 // Originally from https://mui.com/components/menus/#MenuListComposition.js
-const IconSubscribeSettings = (props) => {
+const SubscribeSettings = (props) => {
     const [open, setOpen] = useState(false);
     const anchorRef = useRef(null);
 
@@ -27,16 +27,15 @@ const IconSubscribeSettings = (props) => {
         setOpen(false);
     };
 
-    const handleClearAll = (event) => {
+    const handleClearAll = async (event) => {
         handleClose(event);
         console.log(`[IconSubscribeSettings] Deleting all notifications from ${props.subscription.id}`);
-        db.notifications
-            .where({subscriptionId: props.subscription.id})
-            .delete(); // FIXME
+        await subscriptionManager.deleteNotifications(props.subscription.id);
     };
 
-    const handleUnsubscribe = (event) => {
+    const handleUnsubscribe = async (event) => {
         handleClose(event);
+        await subscriptionManager.remove(props.subscription.id);
         props.onUnsubscribe(props.subscription.id);
     };
 
@@ -48,7 +47,7 @@ const IconSubscribeSettings = (props) => {
         setOpen(false);
     }
 
-    function handleListKeyDown(event) {
+    const handleListKeyDown = (event) => {
         if (event.key === 'Tab') {
             event.preventDefault();
             setOpen(false);
@@ -114,4 +113,4 @@ const IconSubscribeSettings = (props) => {
     );
 }
 
-export default IconSubscribeSettings;
+export default SubscribeSettings;