index.js (6112B)
1 import {openDB} from 'idb'; 2 3 const EVENT_STORED_SESSIONS_CHANGED = 'storedsessionschanged'; 4 const EVENT_ACTIVE_SESSIONS_CHANGED = 'activesessionschanged'; 5 6 class SessionRepository { 7 static DB_NAME = 'sessions'; 8 static STORE_NAME = 'sessionData'; 9 static MAX_HISTORY_ENTRIES = 100; 10 static MESSAGE_SESSION_CHANGED = 'sessionchanged'; 11 12 constructor() { 13 this.channel = new BroadcastChannel('sessionrepository'); 14 this.channel.addEventListener('message', (e) => this.onChannelMessage(e)); 15 this.dbPromise = openDB(SessionRepository.DB_NAME, 1, { 16 upgrade(db) { 17 const store = db.createObjectStore(SessionRepository.STORE_NAME, {keyPath: 'sessionId'}); 18 store.createIndex('mtime', 'mtime', {unique: false}); 19 }, 20 }); 21 } 22 23 onChannelMessage(e) { 24 if (e.data.message === SessionRepository.MESSAGE_SESSION_CHANGED) { 25 window.dispatchEvent(new Event(EVENT_STORED_SESSIONS_CHANGED)); 26 } 27 } 28 29 async listSessionStates() { 30 const db = await this.dbPromise; 31 return db.getAll(SessionRepository.STORE_NAME); 32 } 33 34 async getSessionState(sessionId) { 35 const db = await this.dbPromise; 36 const record = await db.get(SessionRepository.STORE_NAME, sessionId); 37 return record?.data; 38 } 39 40 async pruneOldSessions() { 41 const db = await this.dbPromise; 42 const tx = db.transaction(SessionRepository.STORE_NAME, 'readwrite'); 43 const recordsCount = await tx.store.count(); 44 const recordsCountToDelete = recordsCount - SessionRepository.MAX_HISTORY_ENTRIES; 45 if (recordsCountToDelete > 0) { 46 let cursor = await tx.store.index('mtime').openCursor(); 47 for (let i = 0; i < recordsCountToDelete; i++) { 48 if (!cursor) { 49 break; 50 } 51 await cursor.delete(); 52 cursor = await cursor.continue(); 53 } 54 this.broadcastStorageChanged(); 55 } 56 } 57 58 async setSessionState(sessionId, data) { 59 const db = await this.dbPromise; 60 await db.put(SessionRepository.STORE_NAME, { 61 sessionId, 62 mtime: Date.now(), 63 data, 64 }); 65 this.broadcastStorageChanged(); 66 this.pruneOldSessions(); 67 } 68 69 async clearSessionState(sessionId) { 70 await (await this.dbPromise).delete(SessionRepository.STORE_NAME, sessionId); 71 this.broadcastStorageChanged(); 72 } 73 74 broadcastStorageChanged() { 75 this.channel.postMessage({message: SessionRepository.MESSAGE_SESSION_CHANGED}); 76 } 77 } 78 79 const sessionRepository = new SessionRepository(); 80 81 class Session { 82 constructor() { 83 let sessionId = window.history.state?.sessionId; 84 if (!sessionId) { 85 sessionId = this.generateSessionId(); 86 window.history.replaceState({sessionId}, ''); 87 } 88 this.sessionId = sessionId; 89 window.addEventListener('popstate', () => window.history.replaceState({sessionId}, '')); 90 } 91 92 generateSessionId() { 93 return Date.now().toString(36) + '_' + Math.random().toString(36).slice(2); 94 } 95 96 async loadState() { 97 return sessionRepository.getSessionState(this.sessionId); 98 } 99 100 async saveState(data) { 101 sessionRepository.setSessionState(this.sessionId, data); 102 } 103 104 async clearState() { 105 sessionRepository.clearSessionState(this.sessionId); 106 } 107 } 108 109 const session = new Session(); 110 111 class ActiveSessionsMonitor { 112 static MESSAGE_REQUEST_SESSION = 'requestsessions'; 113 static MESSAGE_ACTIVE_SESSION = 'activesession'; 114 static MESSAGE_CLOSE_SESSION = 'closesession'; 115 116 constructor() { 117 this.channel = new BroadcastChannel('sessions'); 118 this.channel.addEventListener('message', (e) => this.onChannelMessage(e)); 119 this._activeSessions = {}; 120 this.broadcastActiveSession(); 121 window.addEventListener('unload', () => this.broadcastSessionEnd()); 122 window.addEventListener('pagehide', () => this.broadcastSessionEnd()); 123 this.monitorRunning = false; 124 } 125 126 onChannelMessage(e) { 127 switch (e.data.message) { 128 case ActiveSessionsMonitor.MESSAGE_REQUEST_SESSION: 129 this.broadcastActiveSession(); 130 break; 131 case ActiveSessionsMonitor.MESSAGE_ACTIVE_SESSION: 132 if (!this.monitorRunning) { 133 break; 134 } 135 this._activeSessions[e.data.sessionId] = true; 136 this.notifyActiveSessionsChanged(); 137 break; 138 case ActiveSessionsMonitor.MESSAGE_CLOSE_SESSION: 139 if (!this.monitorRunning) { 140 break; 141 } 142 delete this._activeSessions[e.data.sessionId]; 143 this.notifyActiveSessionsChanged(); 144 break; 145 default: 146 } 147 } 148 149 startMonitor() { 150 this._activeSessions = {}; 151 this.monitorRunning = true; 152 this.requestActiveSessions(); 153 } 154 155 stopMonitor() { 156 this.monitorRunning = false; 157 this._activeSessions = {}; 158 } 159 160 broadcastActiveSession() { 161 this.channel.postMessage({ 162 message: ActiveSessionsMonitor.MESSAGE_ACTIVE_SESSION, 163 sessionId: session.sessionId, 164 }); 165 } 166 167 broadcastSessionEnd() { 168 this.channel.postMessage({ 169 message: ActiveSessionsMonitor.MESSAGE_CLOSE_SESSION, 170 sessionId: session.sessionId, 171 }); 172 } 173 174 notifyActiveSessionsChanged() { 175 window.dispatchEvent(new Event(EVENT_ACTIVE_SESSIONS_CHANGED)); 176 } 177 178 requestActiveSessions() { 179 this.channel.postMessage({message: ActiveSessionsMonitor.MESSAGE_REQUEST_SESSION}); 180 } 181 182 getActiveSessions() { 183 return Object.keys(this._activeSessions); 184 } 185 } 186 187 const activeSessionsMonitor = new ActiveSessionsMonitor(); 188 189 export { 190 session, 191 sessionRepository, 192 activeSessionsMonitor, 193 EVENT_STORED_SESSIONS_CHANGED, 194 EVENT_ACTIVE_SESSIONS_CHANGED, 195 };