Switch everything to Dexie.js
This commit is contained in:
		
							parent
							
								
									39f4613719
								
							
						
					
					
						commit
						349872bdb3
					
				
					 16 changed files with 243 additions and 316 deletions
				
			
		|  | @ -10,11 +10,12 @@ class ConnectionManager { | |||
|             return; | ||||
|         } | ||||
|         console.log(`[ConnectionManager] Refreshing connections`); | ||||
|         const subscriptionIds = subscriptions.ids(); | ||||
|         const subscriptionIds = subscriptions.map(s => s.id); | ||||
|         const deletedIds = Array.from(this.connections.keys()).filter(id => !subscriptionIds.includes(id)); | ||||
| 
 | ||||
|         // Create and add new connections
 | ||||
|         subscriptions.forEach((id, subscription) => { | ||||
|         subscriptions.forEach(subscription => { | ||||
|             const id = subscription.id; | ||||
|             const added = !this.connections.get(id) | ||||
|             if (added) { | ||||
|                 const baseUrl = subscription.baseUrl; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {formatMessage, formatTitleWithFallback, topicShortUrl} from "./utils"; | ||||
| import repository from "./Repository"; | ||||
| import prefs from "./Prefs"; | ||||
| 
 | ||||
| class NotificationManager { | ||||
|     async notify(subscription, notification, onClickFallback) { | ||||
|  | @ -7,8 +7,11 @@ class NotificationManager { | |||
|         if (!shouldNotify) { | ||||
|             return; | ||||
|         } | ||||
|         const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); | ||||
|         const message = formatMessage(notification); | ||||
|         const title = formatTitleWithFallback(notification, topicShortUrl(subscription.baseUrl, subscription.topic)); | ||||
|         const title = formatTitleWithFallback(notification, shortUrl); | ||||
| 
 | ||||
|         console.log(`[NotificationManager, ${shortUrl}] Displaying notification ${notification.id}: ${message}`); | ||||
|         const n = new Notification(title, { | ||||
|             body: message, | ||||
|             icon: '/static/img/favicon.png' | ||||
|  | @ -35,7 +38,7 @@ class NotificationManager { | |||
| 
 | ||||
|     async shouldNotify(subscription, notification) { | ||||
|         const priority = (notification.priority) ? notification.priority : 3; | ||||
|         const minPriority = await repository.getMinPriority(); | ||||
|         const minPriority = await prefs.minPriority(); | ||||
|         if (priority < minPriority) { | ||||
|             return false; | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										49
									
								
								web/src/app/Poller.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								web/src/app/Poller.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| import db from "./db"; | ||||
| import api from "./Api"; | ||||
| 
 | ||||
| const delayMillis = 3000; // 3 seconds
 | ||||
| const intervalMillis = 300000; // 5 minutes
 | ||||
| 
 | ||||
| class Poller { | ||||
|     constructor() { | ||||
|         this.timer = null; | ||||
|     } | ||||
| 
 | ||||
|     startWorker() { | ||||
|         if (this.timer !== null) { | ||||
|             return; | ||||
|         } | ||||
|         this.timer = setInterval(() => this.pollAll(), intervalMillis); | ||||
|         setTimeout(() => this.pollAll(), delayMillis); | ||||
|     } | ||||
| 
 | ||||
|     async pollAll() { | ||||
|         console.log(`[Poller] Polling all subscriptions`); | ||||
|         const subscriptions = await db.subscriptions.toArray(); | ||||
|         for (const s of subscriptions) { | ||||
|             try { | ||||
|                 await this.poll(s); | ||||
|             } catch (e) { | ||||
|                 console.log(`[Poller] Error polling ${s.id}`, e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async poll(subscription) { | ||||
|         console.log(`[Poller] Polling ${subscription.id}`); | ||||
| 
 | ||||
|         const since = subscription.last; | ||||
|         const notifications = await api.poll(subscription.baseUrl, subscription.topic, since); | ||||
|         if (!notifications || notifications.length === 0) { | ||||
|             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
 | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| const poller = new Poller(); | ||||
| export default poller; | ||||
							
								
								
									
										33
									
								
								web/src/app/Prefs.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/src/app/Prefs.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| import db from "./db"; | ||||
| 
 | ||||
| class Prefs { | ||||
|     async setSelectedSubscriptionId(selectedSubscriptionId) { | ||||
|         db.prefs.put({key: 'selectedSubscriptionId', value: selectedSubscriptionId}); | ||||
|     } | ||||
| 
 | ||||
|     async selectedSubscriptionId() { | ||||
|         const selectedSubscriptionId = await db.prefs.get('selectedSubscriptionId'); | ||||
|         return (selectedSubscriptionId) ? selectedSubscriptionId.value : ""; | ||||
|     } | ||||
| 
 | ||||
|     async setMinPriority(minPriority) { | ||||
|         db.prefs.put({key: 'minPriority', value: minPriority.toString()}); | ||||
|     } | ||||
| 
 | ||||
|     async minPriority() { | ||||
|         const minPriority = await db.prefs.get('minPriority'); | ||||
|         return (minPriority) ? Number(minPriority.value) : 1; | ||||
|     } | ||||
| 
 | ||||
|     async setDeleteAfter(deleteAfter) { | ||||
|         db.prefs.put({key:'deleteAfter', value: deleteAfter.toString()}); | ||||
|     } | ||||
| 
 | ||||
|     async deleteAfter() { | ||||
|         const deleteAfter = await db.prefs.get('deleteAfter'); | ||||
|         return (deleteAfter) ? Number(deleteAfter.value) : 604800; // Default is one week
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const prefs = new Prefs(); | ||||
| export default prefs; | ||||
							
								
								
									
										39
									
								
								web/src/app/Pruner.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								web/src/app/Pruner.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| import db from "./db"; | ||||
| import prefs from "./Prefs"; | ||||
| 
 | ||||
| const delayMillis = 15000; // 15 seconds
 | ||||
| const intervalMillis = 1800000; // 30 minutes
 | ||||
| 
 | ||||
| class Pruner { | ||||
|     constructor() { | ||||
|         this.timer = null; | ||||
|     } | ||||
| 
 | ||||
|     startWorker() { | ||||
|         if (this.timer !== null) { | ||||
|             return; | ||||
|         } | ||||
|         this.timer = setInterval(() => this.prune(), intervalMillis); | ||||
|         setTimeout(() => this.prune(), delayMillis); | ||||
|     } | ||||
| 
 | ||||
|     async prune() { | ||||
|         const deleteAfterSeconds = await prefs.deleteAfter(); | ||||
|         const pruneThresholdTimestamp = Math.round(Date.now()/1000) - deleteAfterSeconds; | ||||
|         if (deleteAfterSeconds === 0) { | ||||
|             console.log(`[Pruner] Pruning is disabled. Skipping.`); | ||||
|             return; | ||||
|         } | ||||
|         console.log(`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`); | ||||
|         try { | ||||
|             await db.notifications | ||||
|                 .where("time").below(pruneThresholdTimestamp) | ||||
|                 .delete(); | ||||
|         } catch (e) { | ||||
|             console.log(`[Pruner] Error pruning old subscriptions`, e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const pruner = new Pruner(); | ||||
| export default pruner; | ||||
|  | @ -1,83 +0,0 @@ | |||
| import Subscription from "./Subscription"; | ||||
| import Subscriptions from "./Subscriptions"; | ||||
| import db from "./db"; | ||||
| 
 | ||||
| class Repository { | ||||
|     loadSubscriptions() { | ||||
|         console.log(`[Repository] Loading subscriptions from localStorage`); | ||||
|         const subscriptions = new Subscriptions(); | ||||
|         subscriptions.loaded = true; | ||||
|         const serialized = localStorage.getItem('subscriptions'); | ||||
|         if (serialized === null) { | ||||
|             return subscriptions; | ||||
|         } | ||||
|         try { | ||||
|             JSON.parse(serialized).forEach(s => { | ||||
|                 const subscription = new Subscription(s.baseUrl, s.topic); | ||||
|                 subscription.addNotifications(s.notifications); | ||||
|                 subscription.last = s.last; // Explicitly set, in case old notifications have been deleted
 | ||||
|                 subscriptions.add(subscription); | ||||
|             }); | ||||
|             console.log(`[Repository] Loaded ${subscriptions.size()} subscription(s) from localStorage`); | ||||
|             return subscriptions; | ||||
|         } catch (e) { | ||||
|             console.log(`[Repository] Unable to deserialize subscriptions: ${e.message}`); | ||||
|             return subscriptions; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     saveSubscriptions(subscriptions) { | ||||
|         if (!subscriptions.loaded) { | ||||
|             return; // Avoid saving invalid state, triggered by initial useEffect hook
 | ||||
|         } | ||||
|         console.log(`[Repository] Saving ${subscriptions.size()} subscription(s) to localStorage`); | ||||
|         const serialized = JSON.stringify(subscriptions.map( (id, subscription) => { | ||||
|             return { | ||||
|                 baseUrl: subscription.baseUrl, | ||||
|                 topic: subscription.topic, | ||||
|                 last: subscription.last | ||||
|             } | ||||
|         })); | ||||
|         localStorage.setItem('subscriptions', serialized); | ||||
|     } | ||||
| 
 | ||||
|     async setSelectedSubscriptionId(selectedSubscriptionId) { | ||||
|         console.log(`[Repository] Saving selected subscription ${selectedSubscriptionId}`); | ||||
|         db.prefs.put({key: 'selectedSubscriptionId', value: selectedSubscriptionId}); | ||||
|     } | ||||
| 
 | ||||
|     async getSelectedSubscriptionId() { | ||||
|         console.log(`[Repository] Loading selected subscription ID`); | ||||
|         const selectedSubscriptionId = await db.prefs.get('selectedSubscriptionId'); | ||||
|         return (selectedSubscriptionId) ? selectedSubscriptionId.value : ""; | ||||
|     } | ||||
| 
 | ||||
|     async setMinPriority(minPriority) { | ||||
|         db.prefs.put({key: 'minPriority', value: minPriority.toString()}); | ||||
|     } | ||||
| 
 | ||||
|     async getMinPriority() { | ||||
|         const minPriority = await db.prefs.get('minPriority'); | ||||
|         return (minPriority) ? Number(minPriority.value) : 1; | ||||
|     } | ||||
| 
 | ||||
|     minPriority() { | ||||
|         return db.prefs.get('minPriority'); | ||||
|     } | ||||
| 
 | ||||
|     async setDeleteAfter(deleteAfter) { | ||||
|         db.prefs.put({key:'deleteAfter', value: deleteAfter.toString()}); | ||||
|     } | ||||
| 
 | ||||
|     async getDeleteAfter() { | ||||
|         const deleteAfter = await db.prefs.get('deleteAfter'); | ||||
|         return (deleteAfter) ? Number(deleteAfter.value) : 604800; // Default is one week
 | ||||
|     } | ||||
| 
 | ||||
|     deleteAfter() { | ||||
|         return db.prefs.get('deleteAfter'); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const repository = new Repository(); | ||||
| export default repository; | ||||
|  | @ -1,33 +0,0 @@ | |||
| import {topicShortUrl, topicUrl} from './utils'; | ||||
| 
 | ||||
| class Subscription { | ||||
|     constructor(baseUrl, topic) { | ||||
|         this.id = topicUrl(baseUrl, topic); | ||||
|         this.baseUrl = baseUrl; | ||||
|         this.topic = topic; | ||||
|         this.last = null; // Last message ID
 | ||||
|     } | ||||
| 
 | ||||
|     addNotification(notification) { | ||||
|         if (!notification.event || notification.event !== 'message') { | ||||
|             return false; | ||||
|         } | ||||
|         this.last = notification.id; | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     addNotifications(notifications) { | ||||
|         notifications.forEach(n => this.addNotification(n)); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     url() { | ||||
|         return topicUrl(this.baseUrl, this.topic); | ||||
|     } | ||||
| 
 | ||||
|     shortUrl() { | ||||
|         return topicShortUrl(this.baseUrl, this.topic); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default Subscription; | ||||
|  | @ -1,56 +0,0 @@ | |||
| class Subscriptions { | ||||
|     constructor() { | ||||
|         this.loaded = false; // FIXME I hate this
 | ||||
|         this.subscriptions = new Map(); | ||||
|     } | ||||
| 
 | ||||
|     add(subscription) { | ||||
|         this.subscriptions.set(subscription.id, subscription); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     get(subscriptionId) { | ||||
|         const subscription = this.subscriptions.get(subscriptionId); | ||||
|         return (subscription) ? subscription : null; | ||||
|     } | ||||
| 
 | ||||
|     update(subscription) { | ||||
|         return this.add(subscription); | ||||
|     } | ||||
| 
 | ||||
|     remove(subscriptionId) { | ||||
|         this.subscriptions.delete(subscriptionId); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     forEach(cb) { | ||||
|         this.subscriptions.forEach((value, key) => cb(key, value)); | ||||
|     } | ||||
| 
 | ||||
|     map(cb) { | ||||
|         return Array.from(this.subscriptions.values()) | ||||
|             .map(subscription => cb(subscription.id, subscription)); | ||||
|     } | ||||
| 
 | ||||
|     ids() { | ||||
|         return Array.from(this.subscriptions.keys()); | ||||
|     } | ||||
| 
 | ||||
|     firstOrNull() { | ||||
|         const first = this.subscriptions.values().next().value; | ||||
|         return (first) ? first : null; | ||||
|     } | ||||
| 
 | ||||
|     size() { | ||||
|         return this.subscriptions.size; | ||||
|     } | ||||
| 
 | ||||
|     clone() { | ||||
|         const c = new Subscriptions(); | ||||
|         c.loaded = this.loaded; | ||||
|         c.subscriptions = new Map(this.subscriptions); | ||||
|         return c; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default Subscriptions; | ||||
|  | @ -9,8 +9,8 @@ import Dexie from 'dexie'; | |||
| const db = new Dexie('ntfy'); | ||||
| 
 | ||||
| db.version(1).stores({ | ||||
|     subscriptions: '&id', | ||||
|     notifications: '&id,subscriptionId', | ||||
|     subscriptions: '&id,baseUrl', | ||||
|     notifications: '&id,subscriptionId,time', | ||||
|     users: '&baseUrl,username', | ||||
|     prefs: '&key' | ||||
| }); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue