forked from cybrespace/pinafore
		
	implement database cleanup on logout
This commit is contained in:
		
							parent
							
								
									8e81926076
								
							
						
					
					
						commit
						0e229bedff
					
				
					 10 changed files with 127 additions and 93 deletions
				
			
		
							
								
								
									
										2
									
								
								routes/_utils/database/cache.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								routes/_utils/database/cache.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| export const openReqs = {} | ||||
| export const databaseCache = {} | ||||
|  | @ -1,9 +1,5 @@ | |||
| import worker from 'workerize-loader!./databaseInsideWorker' | ||||
| 
 | ||||
| import * as databaseInsideWorker from './databaseInsideWorker' | ||||
| 
 | ||||
| // workerize-loader causes weirdness during development
 | ||||
| let database = process.browser && process.env.NODE_ENV === 'production' ? worker() : databaseInsideWorker | ||||
| import worker from 'workerize-loader!./databaseCore' | ||||
| const database = process.browser && worker() | ||||
| 
 | ||||
| export { | ||||
|   database | ||||
|  |  | |||
							
								
								
									
										62
									
								
								routes/_utils/database/databaseCore.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								routes/_utils/database/databaseCore.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| import { META_STORE, getMetaDatabase } from './meta' | ||||
| import { cleanupOldStatuses } from './cleanupTimelines' | ||||
| import { TIMELINE_STORE, getTimelineDatabase } from './timelines' | ||||
| import { toReversePaddedBigInt, transformStatusForStorage, dbPromise, deleteDbPromise } from './utils' | ||||
| import { getKnownDbsForInstance, deleteInstanceFromKnownDbs } from './knownDbs' | ||||
| 
 | ||||
| export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) { | ||||
|   const db = await getTimelineDatabase(instanceName, timeline) | ||||
|   return await dbPromise(db, TIMELINE_STORE, 'readonly', (store, callback) => { | ||||
|     const index = store.index('pinafore_id_as_negative_big_int') | ||||
|     let sinceAsNegativeBigInt = maxId ? toReversePaddedBigInt(maxId) : null | ||||
|     let query = sinceAsNegativeBigInt ? IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false) : null | ||||
| 
 | ||||
|     index.getAll(query, limit).onsuccess = (e) => { | ||||
|       callback(e.target.result) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export async function insertStatuses(instanceName, timeline, statuses) { | ||||
|   const db = await getTimelineDatabase(instanceName, timeline) | ||||
|   await dbPromise(db, TIMELINE_STORE, 'readwrite', (store) => { | ||||
|     for (let status of statuses) { | ||||
|       store.put(transformStatusForStorage(status)) | ||||
|     } | ||||
|   }) | ||||
|   /* no await */ cleanupOldStatuses() | ||||
| } | ||||
| 
 | ||||
| export async function getInstanceVerifyCredentials(instanceName) { | ||||
|   const db = await getMetaDatabase(instanceName) | ||||
|   return await dbPromise(db, META_STORE, 'readonly', (store, callback) => { | ||||
|     store.get('verifyCredentials').onsuccess = (e) => { | ||||
|       callback(e.target.result && e.target.result.value) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export async function setInstanceVerifyCredentials(instanceName, verifyCredentials) { | ||||
|   const db = await getMetaDatabase(instanceName) | ||||
|   return await dbPromise(db, META_STORE, 'readwrite', (store) => { | ||||
|     store.put({ | ||||
|       key: 'verifyCredentials', | ||||
|       value: verifyCredentials | ||||
|     }) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export async function clearDatabasesForInstance(instanceName) { | ||||
|   console.log('clearDatabasesForInstance', instanceName) | ||||
|   const knownDbsForInstance = await getKnownDbsForInstance(instanceName) | ||||
|   for (let knownDb of knownDbsForInstance) { | ||||
|     let { dbName } = knownDb | ||||
|     try { | ||||
|       await deleteDbPromise(dbName) | ||||
|       console.error(`deleted database ${dbName}`) | ||||
|     } catch (e) { | ||||
|       console.error(`failed to delete database ${dbName}`) | ||||
|     } | ||||
|   } | ||||
|   await deleteInstanceFromKnownDbs(instanceName) | ||||
| } | ||||
|  | @ -1,65 +0,0 @@ | |||
| import { META_STORE, getMetaDatabase } from './meta' | ||||
| import { cleanupOldStatuses } from './cleanupTimelines' | ||||
| import { TIMELINE_STORE, getTimelineDatabase } from './timelines' | ||||
| import { toReversePaddedBigInt, transformStatusForStorage } from './utils' | ||||
| 
 | ||||
| export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) { | ||||
|   const db = await getTimelineDatabase(instanceName, timeline) | ||||
|   return await new Promise((resolve, reject) => { | ||||
|     const tx = db.transaction(TIMELINE_STORE, 'readonly') | ||||
|     const store = tx.objectStore(TIMELINE_STORE) | ||||
|     const index = store.index('pinafore_id_as_negative_big_int') | ||||
|     let sinceAsNegativeBigInt = maxId ? toReversePaddedBigInt(maxId) : null | ||||
|     let query = sinceAsNegativeBigInt ? IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false) : null | ||||
| 
 | ||||
|     let res | ||||
|     index.getAll(query, limit).onsuccess = (e) => { | ||||
|       res = e.target.result | ||||
|     } | ||||
| 
 | ||||
|     tx.oncomplete = () => resolve(res) | ||||
|     tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export async function insertStatuses(instanceName, timeline, statuses) { | ||||
|   const db = await getTimelineDatabase(instanceName, timeline) | ||||
|   await new Promise((resolve, reject) => { | ||||
|     const tx = db.transaction(TIMELINE_STORE, 'readwrite') | ||||
|     const store = tx.objectStore(TIMELINE_STORE) | ||||
|     for (let status of statuses) { | ||||
|       store.put(transformStatusForStorage(status)) | ||||
|     } | ||||
|     tx.oncomplete = () => resolve() | ||||
|     tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message) | ||||
|   }) | ||||
|   /* no await */ cleanupOldStatuses() | ||||
| } | ||||
| 
 | ||||
| export async function setInstanceVerifyCredentials(instanceName, verifyCredentials) { | ||||
|   const db = await getMetaDatabase(instanceName) | ||||
|   return await new Promise((resolve, reject) => { | ||||
|     const tx = db.transaction(META_STORE, 'readwrite') | ||||
|     const store = tx.objectStore(META_STORE) | ||||
|     store.put({ | ||||
|       key: 'verifyCredentials', | ||||
|       value: verifyCredentials | ||||
|     }) | ||||
|     tx.oncomplete = () => resolve() | ||||
|     tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export async function getInstanceVerifyCredentials(instanceName, verifyCredentials) { | ||||
|   const db = await getMetaDatabase(instanceName) | ||||
|   return await new Promise((resolve, reject) => { | ||||
|     const tx = db.transaction(META_STORE, 'readwrite') | ||||
|     const store = tx.objectStore(META_STORE) | ||||
|     let res | ||||
|     store.get('verifyCredentials').onsuccess = (e) => { | ||||
|       res = e.target.result && e.target.result.value | ||||
|     } | ||||
|     tx.oncomplete = () => resolve(res) | ||||
|     tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message) | ||||
|   }) | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| import keyval from "idb-keyval" | ||||
| 
 | ||||
| export async function addKnownDb(instanceName, type, dbName) { | ||||
|   let knownDbs = (await keyval.get('known_dbs')) || {} | ||||
|   let knownDbs = (await getKnownDbs()) | ||||
|   if (!knownDbs[instanceName]) { | ||||
|     knownDbs[instanceName] = [] | ||||
|   } | ||||
|  | @ -18,4 +18,10 @@ export async function getKnownDbs() { | |||
| export async function getKnownDbsForInstance(instanceName) { | ||||
|   let knownDbs = await getKnownDbs() | ||||
|   return knownDbs[instanceName] || [] | ||||
| } | ||||
| 
 | ||||
| export async function deleteInstanceFromKnownDbs(instanceName) { | ||||
|   let knownDbs = await getKnownDbs() | ||||
|   delete knownDbs[instanceName] | ||||
|   await keyval.set('known_dbs', knownDbs) | ||||
| } | ||||
|  | @ -1,20 +1,19 @@ | |||
| import { addKnownDb } from './knownDbs' | ||||
| import { openReqs, databaseCache } from './cache' | ||||
| 
 | ||||
| const databaseCache = {} | ||||
| export const META_STORE = 'meta' | ||||
| 
 | ||||
| export function getMetaDatabase(instanceName) { | ||||
|   const key = `${instanceName}_${META_STORE}` | ||||
|   if (databaseCache[key]) { | ||||
|     return Promise.resolve(databaseCache[key]) | ||||
|   const dbName = `${instanceName}_${META_STORE}` | ||||
|   if (databaseCache[dbName]) { | ||||
|     return Promise.resolve(databaseCache[dbName]) | ||||
|   } | ||||
| 
 | ||||
|   let dbName = key | ||||
| 
 | ||||
|   addKnownDb(instanceName, 'meta', dbName) | ||||
| 
 | ||||
|   databaseCache[key] = new Promise((resolve, reject) => { | ||||
|   databaseCache[dbName] = new Promise((resolve, reject) => { | ||||
|     let req = indexedDB.open(dbName, 1) | ||||
|     openReqs[dbName] = req | ||||
|     req.onerror = reject | ||||
|     req.onblocked = () => { | ||||
|       console.log('idb blocked') | ||||
|  | @ -25,5 +24,5 @@ export function getMetaDatabase(instanceName) { | |||
|     } | ||||
|     req.onsuccess = () => resolve(req.result) | ||||
|   }) | ||||
|   return databaseCache[key] | ||||
|   return databaseCache[dbName] | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| import { addKnownDb } from './knownDbs' | ||||
| import { openReqs, databaseCache } from './cache' | ||||
| 
 | ||||
| const databaseCache = {} | ||||
| export const TIMELINE_STORE = 'statuses' | ||||
| 
 | ||||
| export function createTimelineDbName(instanceName, timeline) { | ||||
|  | @ -8,17 +8,17 @@ export function createTimelineDbName(instanceName, timeline) { | |||
| } | ||||
| 
 | ||||
| export function getTimelineDatabase(instanceName, timeline) { | ||||
|   const key = `${instanceName}_${timeline}` | ||||
|   if (databaseCache[key]) { | ||||
|     return Promise.resolve(databaseCache[key]) | ||||
|   } | ||||
| 
 | ||||
|   let dbName = createTimelineDbName(instanceName, timeline) | ||||
| 
 | ||||
|   if (databaseCache[dbName]) { | ||||
|     return Promise.resolve(databaseCache[dbName]) | ||||
|   } | ||||
| 
 | ||||
|   addKnownDb(instanceName, 'timeline', dbName) | ||||
| 
 | ||||
|   databaseCache[key] = new Promise((resolve, reject) => { | ||||
|   databaseCache[dbName] = new Promise((resolve, reject) => { | ||||
|     let req = indexedDB.open(dbName, 1) | ||||
|     openReqs[dbName] = req | ||||
|     req.onerror = reject | ||||
|     req.onblocked = () => { | ||||
|       console.log('idb blocked') | ||||
|  | @ -32,5 +32,5 @@ export function getTimelineDatabase(instanceName, timeline) { | |||
|     } | ||||
|     req.onsuccess = () => resolve(req.result) | ||||
|   }) | ||||
|   return databaseCache[key] | ||||
|   return databaseCache[dbName] | ||||
| } | ||||
|  | @ -1,5 +1,6 @@ | |||
| import cloneDeep from 'lodash/cloneDeep' | ||||
| import padStart from 'lodash/padStart' | ||||
| import { databaseCache, openReqs } from './cache' | ||||
| 
 | ||||
| export function toPaddedBigInt (id) { | ||||
|   return padStart(id, 30, '0') | ||||
|  | @ -19,4 +20,33 @@ export function transformStatusForStorage (status) { | |||
|   status.pinafore_id_as_negative_big_int = toReversePaddedBigInt(status.id) | ||||
|   status.pinafore_stale = true | ||||
|   return status | ||||
| } | ||||
| 
 | ||||
| export async function dbPromise(db, storeName, readOnlyOrReadWrite, cb) { | ||||
|   return await new Promise((resolve, reject) => { | ||||
|     const tx = db.transaction(storeName, readOnlyOrReadWrite) | ||||
|     const store = tx.objectStore(storeName) | ||||
|     let res | ||||
|     cb(store, (result) => { | ||||
|       res = result | ||||
|     }) | ||||
| 
 | ||||
|     tx.oncomplete = () => resolve(res) | ||||
|     tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export function deleteDbPromise(dbName) { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     // close any open requests
 | ||||
|     let openReq = openReqs[dbName]; | ||||
|     if (openReq && openReq.result) { | ||||
|       openReq.result.close() | ||||
|     } | ||||
|     delete openReqs[dbName] | ||||
|     delete databaseCache[dbName] | ||||
|     let req = indexedDB.deleteDatabase(dbName) | ||||
|     req.onsuccess = () => resolve() | ||||
|     req.onerror = () => reject(req.error.name + ' ' + req.error.message) | ||||
|   }) | ||||
| } | ||||
|  | @ -30,11 +30,11 @@ | |||
|       <form class="instance-actions" aria-label="Switch to or log out of this instance"> | ||||
|         {{#if $loggedInInstancesInOrder.length > 1}} | ||||
|           <button class="primary" disabled="{{$currentInstance === params.instanceName}}" | ||||
|             on:click="onSwitchToThisInstance()"> | ||||
|             on:click="onSwitchToThisInstance(event)"> | ||||
|             {{$currentInstance === params.instanceName ? 'This is your current instance' : 'Switch to this instance'}} | ||||
|           </button> | ||||
|         {{/if}} | ||||
|         <button on:click="onLogOut()">Log out</button> | ||||
|         <button on:click="onLogOut(event)">Log out</button> | ||||
|       </form> | ||||
|     {{/if}} | ||||
|   </SettingsLayout> | ||||
|  | @ -145,7 +145,8 @@ | |||
|           switchToTheme(newTheme) | ||||
|         } | ||||
|       }, | ||||
|       onSwitchToThisInstance() { | ||||
|       onSwitchToThisInstance(e) { | ||||
|         e.preventDefault() | ||||
|         let instanceName = this.get('params').instanceName | ||||
|         let instanceThemes = this.store.get('instanceThemes') | ||||
|         this.store.set({ | ||||
|  | @ -154,7 +155,8 @@ | |||
|         this.store.save() | ||||
|         switchToTheme(instanceThemes[instanceName]) | ||||
|       }, | ||||
|       onLogOut() { | ||||
|       onLogOut(e) { | ||||
|         e.preventDefault() | ||||
|         let loggedInInstances = this.store.get('loggedInInstances') | ||||
|         let instanceThemes = this.store.get('instanceThemes') | ||||
|         let loggedInInstancesInOrder = this.store.get('loggedInInstancesInOrder') | ||||
|  | @ -173,6 +175,7 @@ | |||
|           currentInstance: newInstance | ||||
|         }) | ||||
|         this.store.save() | ||||
|         database.clearDatabasesForInstance(instanceName) | ||||
|         switchToTheme(instanceThemes[newInstance] || 'default') | ||||
|         toast.say(`Logged out of ${instanceName}`) | ||||
|         goto('/settings/instances') | ||||
|  |  | |||
|  | @ -47,7 +47,8 @@ body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-o | |||
|       offline:   "#999999" | ||||
| 		} | ||||
| 		if (localStorage.store_currentInstance && localStorage.store_instanceThemes) { | ||||
| 		  let theme = JSON.parse(localStorage.store_instanceThemes)[JSON.parse(localStorage.store_currentInstance)] | ||||
| 		  let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str) | ||||
| 		  let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)] | ||||
| 			if (theme !== 'default') { | ||||
|         document.body.classList.add(`theme-${theme}`) | ||||
| 				let link = document.createElement('link') | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue