Refactor the db; move to *Manager classes

This commit is contained in:
Philipp Heckel 2022-03-03 16:52:07 -05:00
parent f9219d2d96
commit 08846e4cc2
12 changed files with 162 additions and 64 deletions

View file

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

View file

@ -1,8 +1,10 @@
import {formatMessage, formatTitleWithFallback, topicShortUrl} from "./utils"; import {formatMessage, formatTitleWithFallback, topicShortUrl} from "./utils";
import prefs from "./Prefs"; import prefs from "./Prefs";
import subscriptionManager from "./SubscriptionManager";
class NotificationManager { 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); const shouldNotify = await this.shouldNotify(subscription, notification);
if (!shouldNotify) { if (!shouldNotify) {
return; return;

View file

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

View file

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

View file

@ -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;

View file

@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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