demos for spacedust
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}