at main 132 lines 3.9 kB view raw
1const NOTIFICATIONS = 'notifications'; 2 3export const SECONDARIES = ['all', 'source', 'group', 'app']; 4 5export const getDB = ((upgrade, v) => { 6 let instance; 7 return () => { 8 if (instance) return instance; 9 const req = indexedDB.open('atproto-notifs', v); 10 instance = new Promise((resolve, reject) => { 11 req.onerror = () => reject(req.error); 12 req.onupgradeneeded = () => upgrade(req.result); 13 req.onsuccess = () => resolve(req.result); 14 }); 15 return instance; 16 }; 17})(function dbUpgrade(db) { 18 19 // primary store for notifications 20 try { 21 // upgrade is a reset: entirely remove the store (ignore errors if it didn't exist) 22 db.deleteObjectStore(NOTIFICATIONS); 23 } catch (e) {} 24 const notifStore = db.createObjectStore(NOTIFICATIONS, { 25 keyPath: 'id', 26 autoIncrement: true, 27 }); 28 // subject prob doesn't need an index, could just query constellation 29 notifStore.createIndex('subject', 'subject', { unique: false }); 30 // specific notification (not unique bc spacedust doens't emit deletes yet) 31 notifStore.createIndex('source_record', 'source_record', { unique: false }); 32 // filter by source user of notifications because why not 33 notifStore.createIndex('source_did', 'source_did', { unique: false }); 34 // notifications of an exact type 35 notifStore.createIndex('source', 'source', { unique: false }); 36 // by nsid group 37 notifStore.createIndex('group', 'group', { unique: false }); 38 // by nsid tld+1 39 notifStore.createIndex('app', 'app', { unique: false }); 40 41 // secondary indexes: notification counts 42 for (const secondary of SECONDARIES) { 43 try { 44 // upgrade is hard reset 45 db.deleteObjectStore(secondary); 46 } catch (e) {} 47 const store = db.createObjectStore(secondary, { 48 keyPath: 'k', 49 }); 50 store.createIndex('total', 'total', { unique: false }); 51 store.createIndex('unread', 'unread', { unique: false }); 52 } 53 54}, 4); 55 56export async function insertNotification(notif: { 57 subject: String, 58 source_record: String, 59 source_did: String, 60 source: String, 61 group: String, 62 app: String, 63}) { 64 const db = await getDB(); 65 const tx = db.transaction([NOTIFICATIONS, ...SECONDARIES], 'readwrite'); 66 67 // 1. insert the actual notification 68 tx.objectStore(NOTIFICATIONS).put(notif); 69 70 // 2. update all secondary counts 71 for (const secondary of SECONDARIES) { 72 const store = tx.objectStore(secondary); 73 const key = secondary === 'all' ? 'all' : notif[secondary]; 74 store.get(key).onsuccess = ev => { 75 let count = ev.target.result ?? { 76 k: key, 77 total: 0, 78 unread: 0, 79 }; 80 count.total += 1; 81 count.unread += 1; 82 store.put(count); 83 }; 84 } 85 86 return new Promise((resolve, reject) => { 87 tx.onerror = () => reject(tx.error); 88 tx.oncomplete = resolve; 89 }); 90} 91 92export async function getNotifications(secondary, secondaryFilter) { 93 const limit = 30; 94 let res = []; 95 const store = (await getDB()) 96 .transaction([NOTIFICATIONS]) 97 .objectStore(NOTIFICATIONS); 98 99 let oc; 100 if (!!secondary && secondary !== 'all' && !!secondaryFilter) { 101 oc = store 102 .index(secondary) 103 .openCursor(IDBKeyRange.only(secondaryFilter), 'prev'); 104 } else { 105 oc = store.openCursor(null, 'prev'); 106 } 107 return new Promise((resolve, reject) => { 108 oc.onerror = () => reject(oc.error); 109 oc.onsuccess = ev => { 110 const cursor = event.target.result; 111 if (cursor) { 112 res.push([cursor.value.id, cursor.value]); 113 if (res.length < limit) cursor.continue(); 114 else resolve(res); 115 } else { 116 resolve(res); 117 } 118 } 119 }); 120} 121 122export async function getSecondary(secondary) { 123 const db = await getDB(); 124 const obj = db 125 .transaction([secondary]) 126 .objectStore(secondary) 127 .getAll(); 128 return new Promise((resolve, reject) => { 129 obj.onerror = () => reject(obj.error); 130 obj.onsuccess = ev => resolve(ev.target.result); 131 }); 132}