Switch from multiple instances of localforage to dumping everything into one key per table. Fix logout as well.

This commit is contained in:
Jonathan Staab 2023-02-18 13:49:17 -06:00
parent 0d97800713
commit 3ea222b120
5 changed files with 25 additions and 147 deletions

View File

@ -1,10 +1,10 @@
import type {Writable} from 'svelte/store'
import {debounce} from 'throttle-debounce'
import {partition, is, prop, find, without, pluck, all, identity} from 'ramda'
import {omit, partition, is, find, without, pluck, all, identity} from 'ramda'
import {writable, derived} from 'svelte/store'
import {switcherFn, createMap, ensurePlural} from 'hurdak/lib/hurdak'
import {createMap, ensurePlural} from 'hurdak/lib/hurdak'
import {log, error} from 'src/util/logger'
import {defer, where, now, timedelta, asyncIterableToArray} from 'src/util/misc'
import {where, now, timedelta} from 'src/util/misc'
// Types
@ -56,8 +56,8 @@ const call = (topic, payload): Promise<Message> => {
})
}
const callLocalforage = async (storeName, method, ...args) => {
const message = await call('localforage.call', {storeName, method, args})
const callLocalforage = async (method, ...args) => {
const message = await call('localforage.call', {method, args})
if (message.topic !== 'localforage.return') {
throw new Error(`callLocalforage received invalid response: ${message}`)
@ -66,55 +66,6 @@ const callLocalforage = async (storeName, method, ...args) => {
return message.payload
}
// Methods that proxy localforage
const iterate = (storeName, where = {}) => ({
[Symbol.asyncIterator]() {
let done = false
let promise = defer()
const messages = []
const channel = new Channel({
onMessage: m => switcherFn(m.topic, {
'localforage.item': () => {
promise.resolve()
messages.push(m.payload)
},
'localforage.iterationComplete': () => {
done = true
promise.resolve()
channel.close()
},
default: () => {
throw new Error(`Invalid topic ${m.topic}`)
},
}),
})
channel.send('localforage.iterate', {storeName, where})
const next = async () => {
if (done) {
return {done}
}
const [value] = messages.splice(0, 1)
if (value) {
return {done, value}
} else {
promise = defer()
await promise
return next()
}
}
return {next}
}
})
// Local copy of data so we can provide a sync observable interface. The worker
// is just for storing data and processing expensive queries
@ -145,7 +96,7 @@ class Table {
;(async () => {
const t = Date.now()
this._setAndNotify(await this.opts.initialize(this))
this._setAndNotify(await this.opts.initialize(this) || {})
const {length: recordsCount} = Object.keys(this.data)
const timeElapsed = Date.now() - t
@ -155,6 +106,9 @@ class Table {
this.ready.set(true)
})()
}
_persist = debounce(10_000, () => {
callLocalforage('setItem', this.name, this.data)
})
_setAndNotify(newData) {
// Update our local copy
this.data = newData
@ -163,6 +117,9 @@ class Table {
for (const cb of this.listeners) {
cb(this.data)
}
// Save to localstorage
this._persist()
}
subscribe(cb) {
this.listeners.push(cb)
@ -179,8 +136,6 @@ class Table {
}
this._setAndNotify({...this.data, ...newData})
callLocalforage(this.name, 'setItems', newData)
}
async bulkPatch(updates: Record<string, object>): Promise<void> {
if (is(Array, updates)) {
@ -195,7 +150,7 @@ class Table {
this.bulkPut({...this.data, ...newData})
}
async bulkRemove(keys) {
await callLocalforage(this.name, 'removeItems', keys)
this._setAndNotify(omit(keys, this.data))
}
put(item) {
return this.bulkPut(createMap(this.pk, [item]))
@ -206,18 +161,15 @@ class Table {
remove(k) {
return this.bulkRemove([k])
}
drop() {
return callLocalforage(this.name, 'dropInstance')
async drop() {
return callLocalforage('removeItem', this.name)
}
dump() {
return callLocalforage(this.name, 'dump')
async dump() {
return callLocalforage('getItem', this.name)
}
toArray() {
return Object.values(this.data)
}
iter(spec = {}) {
return asyncIterableToArray(iterate(name, spec), prop('v'))
}
all(spec = {}) {
return this.toArray().filter(where(spec))
}
@ -238,7 +190,7 @@ const relays = new Table('relays', 'url')
const routes = new Table('routes', 'id', {
initialize: async table => {
const isValid = r => r.last_seen > now() - timedelta(7, 'days')
const [valid, invalid] = partition(isValid, Object.values(await table.dump()))
const [valid, invalid] = partition(isValid, Object.values(await table.dump() || {}))
// Delete stale routes asynchronously
table.bulkRemove(pluck('id', invalid))

View File

@ -1,83 +1,22 @@
import lf from 'localforage'
import memoryStorageDriver from 'localforage-memoryStorageDriver'
import {switcherFn} from 'hurdak/lib/hurdak'
import {error, warn} from 'src/util/logger'
import {where} from 'src/util/misc'
import {error} from 'src/util/logger'
// Firefox private mode doesn't have access to any storage options
lf.defineDriver(memoryStorageDriver)
lf.setDriver([lf.INDEXEDDB, lf.WEBSQL, lf.LOCALSTORAGE, 'memoryStorageDriver'])
const stores = {}
const getStore = storeName => {
if (!stores[storeName]) {
stores[storeName] = lf.createInstance({name: 'coracle', storeName})
}
if (stores[storeName].dropped) {
warn(`Dropped instance ${storeName} was requested`)
return null
}
return stores[storeName]
}
addEventListener('message', async ({data: {topic, payload, channel}}) => {
const reply = (topic, payload) => postMessage({channel, topic, payload})
switcherFn(topic, {
'localforage.call': async () => {
const {storeName, method, args} = payload
const instance = getStore(storeName)
const {method, args} = payload
if (instance) {
const result = await switcherFn(method, {
dump: () => new Promise(resolve => {
const result = {}
const result = await lf[method](...args)
instance.iterate(
(v, k, i) => { result[k] = v },
() => resolve(result),
)
}),
setItems: async () => {
for (const [k, v] of Object.entries(args[0])) {
await instance.setItem(k, v)
}
},
removeItems: async () => {
for (const k of args[0]) {
await instance.removeItem(k)
}
},
drop: async () => {
instance.dropped = true
await instance.drop()
},
default: () => instance[method](...args),
})
reply('localforage.return', result)
}
},
'localforage.iterate': async () => {
const matchesFilter = where(payload.where)
const instance = getStore(payload.storeName)
if (instance) {
instance.iterate(
(v, k, i) => {
if (matchesFilter(v)) {
reply('localforage.item', {v, k, i})
}
},
() => {
reply('localforage.iterationComplete')
},
)
}
reply('localforage.return', result)
},
default: () => {
throw new Error(`invalid topic: ${topic}`)

View File

@ -5,7 +5,7 @@
import {renderContent, noEvent} from "src/util/html"
import {displayPerson} from "src/util/nostr"
import Anchor from 'src/partials/Anchor.svelte'
import {getPubkeyWriteRelays} from 'src/agent/relays'
import {getPubkeyWriteRelays, sampleRelays} from 'src/agent/relays'
import user from 'src/agent/user'
import {routes} from "src/app/ui"
@ -14,7 +14,7 @@
const {petnamePubkeys} = user
const addPetname = pubkey => {
const [{url}] = getPubkeyWriteRelays(pubkey)
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
user.addPetname(pubkey, url, displayPerson(person))
}

View File

@ -3,17 +3,13 @@
import Anchor from 'src/partials/Anchor.svelte'
import Content from "src/partials/Content.svelte"
import database from 'src/agent/database'
import pool from 'src/agent/pool'
let confirmed = false
const confirm = async () => {
confirmed = true
await Promise.all([
...pool.getConnections().map(c => c.disconnect()),
database.dropAll(),
])
await database.dropAll()
localStorage.clear()

View File

@ -201,15 +201,6 @@ export const defer = () => {
return Object.assign(p, {resolve, reject})
}
export const asyncIterableToArray = async (it, f = identity) => {
const result = []
for await (const x of it) {
result.push(f(x))
}
return result
}
export const avg = xs => sum(xs) / xs.length
export const where = filters =>