Split database up, use lru cache as our table implementation

This commit is contained in:
Jonathan Staab 2023-03-14 10:13:59 -05:00
parent bc1329f9a8
commit de91a06806
40 changed files with 408 additions and 430 deletions

View File

@ -1,5 +1,20 @@
# Current
- [ ] Fix re-connects
- [ ] Fix memory usage
- Re-write database
- Use LRU cache and persist that instead. Use purgeStale/dump/load
- Split state persistence elsewhere
- Keep it focused to abstract interface, split actual tables out elsewhere
- Put all other state in same place
- Re-write to use arrays with an index of id to index
- Fix compatibility, or clear data on first load of new version
- Add table of user events, derive profile from this using `watch`.
- Refine sync, set up some kind of system where we register tables with events coming in
- People.petnames is massive. Split people caches up
- Display/picture/about
- Minimal zapper info
- Drop petnames
- [ ] Show loading/success on zap invoice screen
- [ ] Fix iOS/safari/firefox
- [ ] Update https://nostr.com/clients/coracle

View File

@ -9,9 +9,9 @@
"preview": "vite preview",
"check:es": "eslint src/*/** --quiet",
"check:ts": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"check:fmt": "prettier --check $(git diff --name-only --diff-filter d | grep -E 'js|svelte$' | xargs)",
"check:fmt": "prettier --check $(git diff --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)",
"check": "run-p check:*",
"format": "prettier --write $(git diff --name-only --diff-filter d | grep -E 'js|svelte$' | xargs)",
"format": "prettier --write $(git diff --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)",
"watch": "find src -type f | entr -r"
},
"devDependencies": {

View File

@ -14,7 +14,7 @@
import {timedelta, shuffle, now, sleep} from "src/util/misc"
import {displayPerson, isLike} from "src/util/nostr"
import cmd from "src/agent/cmd"
import database from "src/agent/database"
import {ready, onReady, relays} from "src/agent/state"
import keys from "src/agent/keys"
import network from "src/agent/network"
import pool from "src/agent/pool"
@ -64,14 +64,12 @@
import AddRelay from "src/views/relays/AddRelay.svelte"
import RelayCard from "src/views/relays/RelayCard.svelte"
Object.assign(window, {cmd, database, user, keys, network, pool, sync})
Object.assign(window, {cmd, user, keys, network, pool, sync})
export let url = ""
let scrollY
const {ready} = database
const closeModal = async () => {
modal.clear()
menuIsOpen.set(false)
@ -115,7 +113,7 @@
}
})
database.onReady(() => {
onReady(() => {
initializeRelayList()
if (user.getProfile()) {
@ -132,7 +130,7 @@
// Find relays with old/missing metadata and refresh them. Only pick a
// few so we're not sending too many concurrent http requests
const staleRelays = shuffle(
await database.relays.all({
await relays.all({
"refreshed_at:lt": now() - timedelta(7, "days"),
})
).slice(0, 10)
@ -159,7 +157,7 @@
})
)
database.relays.bulkPatch(createMap("url", freshRelays.filter(identity)))
relays.bulkPatch(freshRelays.filter(identity))
}, 30_000)
return () => {

View File

@ -2,7 +2,7 @@ import {pick, last, prop, uniqBy} from 'ramda'
import {get} from 'svelte/store'
import {roomAttrs, displayPerson, findReplyId, findRootId} from 'src/util/nostr'
import {getPubkeyWriteRelays, getRelayForPersonHint, sampleRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import {getPersonWithFallback} from 'src/agent/state'
import pool from 'src/agent/pool'
import sync from 'src/agent/sync'
import keys from 'src/agent/keys'
@ -43,7 +43,7 @@ const createDirectMessage = (pubkey, content) =>
const createNote = (content, mentions = [], topics = []) => {
mentions = mentions.map(pubkey => {
const name = displayPerson(database.getPersonWithFallback(pubkey))
const name = displayPerson(getPersonWithFallback(pubkey))
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
return ["p", pubkey, url, name]

View File

@ -1,10 +1,4 @@
import type {Writable} from 'svelte/store'
import {throttle} from 'throttle-debounce'
import {omit, prop, partition, is, find, without, pluck, all, identity} from 'ramda'
import {writable, derived} from 'svelte/store'
import {createMap, isObject, ensurePlural} from 'hurdak/lib/hurdak'
import {log, error} from 'src/util/logger'
import {where, now, timedelta} from 'src/util/misc'
import {error} from 'src/util/logger'
// Types
@ -56,7 +50,7 @@ const call = (topic, payload): Promise<Message> => {
})
}
const callLocalforage = async (method, ...args) => {
export const lf = async (method, ...args) => {
const message = await call('localforage.call', {method, args})
if (message.topic !== 'localforage.return') {
@ -65,244 +59,3 @@ const callLocalforage = async (method, ...args) => {
return message.payload
}
// Local copy of data so we can provide a sync observable interface. The worker
// is just for storing data and processing expensive queries
const registry = {} as Record<string, Table>
type TableOpts = {
initialize?: (table: Table) => Promise<object>
}
class Table {
name: string
pk: string
opts: TableOpts
listeners: Array<(data: Record<string, any>) => void>
data: Record<string, any>
ready: Writable<boolean>
constructor(name, pk, opts: TableOpts = {}) {
this.name = name
this.pk = pk
this.opts = {initialize: t => this.dump(), ...opts}
this.listeners = []
this.data = {}
this.ready = writable(false)
registry[name] = this
// Sync from storage initially
;(async () => {
const t = Date.now()
this._setAndNotify(await this.opts.initialize(this) || {})
const {length: recordsCount} = Object.keys(this.data)
const timeElapsed = Date.now() - t
log(`Table ${name} ready in ${timeElapsed}ms (${recordsCount} records)`)
this.ready.set(true)
})()
}
_persist = throttle(4_000, () => {
callLocalforage('setItem', this.name, this.data)
})
_setAndNotify(newData) {
if (!isObject(newData)) {
throw new Error(`Invalid data persisted`)
}
// Update our local copy
this.data = newData
// Notify subscribers
for (const cb of this.listeners) {
cb(this.data)
}
// Save to localstorage
this._persist()
}
subscribe(cb) {
this.listeners.push(cb)
cb(this.data)
return () => {
this.listeners = without([cb], this.listeners)
}
}
async bulkPut(newData: Record<string, object>): Promise<void> {
if (is(Array, newData)) {
throw new Error(`Updates must be an object, not an array`)
}
this._setAndNotify({...this.data, ...newData})
}
async bulkPatch(updates: Record<string, object>): Promise<void> {
if (is(Array, updates)) {
throw new Error(`Updates must be an object, not an array`)
}
const newData = {}
for (const [k, v] of Object.entries(updates)) {
newData[k] = {...this.data[k], ...v}
}
this.bulkPut({...this.data, ...newData})
}
async bulkRemove(keys) {
this._setAndNotify(omit(keys, this.data))
}
put(item) {
return this.bulkPut(createMap(this.pk, [item]))
}
patch(item) {
return this.bulkPatch(createMap(this.pk, [item]))
}
remove(k) {
return this.bulkRemove([k])
}
async drop() {
return callLocalforage('removeItem', this.name)
}
async dump() {
return callLocalforage('getItem', this.name)
}
toArray() {
return Object.values(this.data)
}
all(spec = {}) {
return this.toArray().filter(where(spec))
}
one(spec = {}) {
return find(where(spec), this.toArray())
}
get(k) {
return this.data[k]
}
}
const people = new Table('people', 'pubkey')
const contacts = new Table('contacts', 'pubkey')
const rooms = new Table('rooms', 'id', {
initialize: async table => {
// Remove rooms that our user hasn't joined
const rooms = Object.values(await table.dump() || {})
const [valid, invalid] = partition(prop('joined'), rooms)
if (invalid.length > 0) {
table.bulkRemove(pluck('id', invalid))
}
return createMap('id', valid)
},
})
const alerts = new Table('alerts', 'id', {
initialize: async table => {
// TEMPORARY: we changed our alerts format, clear out the old version
const alerts = Object.values(await table.dump() || {})
const [valid, invalid] = partition(alert => typeof alert.isMention === 'boolean', alerts)
if (invalid.length > 0) {
table.bulkRemove(pluck('id', invalid))
}
return createMap('id', valid)
},
})
const relays = new Table('relays', 'url')
const routes = new Table('routes', 'id', {
initialize: async table => {
const isValid = r => r.last_seen > now() - timedelta(1, 'days')
const [valid, invalid] = partition(isValid, Object.values(await table.dump() || {}))
// Delete stale routes asynchronously
table.bulkRemove(pluck('id', invalid))
return createMap('id', valid)
},
})
// Helper to allow us to listen to changes of any given table
const listener = (() => {
let listeners = []
for (const table of Object.values(registry) as Array<Table>) {
table.subscribe(() => listeners.forEach(f => f(table.name)))
}
return {
subscribe: f => {
listeners.push(f)
return () => {
listeners = without([f], listeners)
}
},
}
})()
// Helper to re-run a query every time a given table changes
const watch = (names, f) => {
names = ensurePlural(names)
const store = writable(null)
const tables = names.map(name => registry[name])
// Initialize synchronously if possible
const initialValue = f(...tables)
if (is(Promise, initialValue)) {
initialValue.then(v => store.set(v))
} else {
store.set(initialValue)
}
// Debounce refresh so we don't get UI lag
const refresh = throttle(300, async () => store.set(await f(...tables)))
// Listen for changes
listener.subscribe(name => {
if (names.includes(name)) {
refresh()
}
})
return store
}
// Other utilities
const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
const dropAll = async () => {
for (const table of Object.values(registry)) {
await table.drop()
log(`Successfully dropped table ${table.name}`)
}
}
const ready = derived(pluck('ready', Object.values(registry)), all(identity))
const onReady = cb => {
const unsub = ready.subscribe($ready => {
if ($ready) {
cb()
setTimeout(() => unsub())
}
})
}
export default {
watch, getPersonWithFallback, dropAll, people, contacts, rooms,
alerts, relays, routes, ready, onReady,
}

View File

@ -3,19 +3,19 @@ import {sortBy, assoc, uniq, uniqBy, prop, propEq, reject, groupBy, pluck} from
import {personKinds, findReplyId} from 'src/util/nostr'
import {log} from 'src/util/logger'
import {chunk} from 'hurdak/lib/hurdak'
import {batch, timedelta, now} from 'src/util/misc'
import {batch, now, timedelta} from 'src/util/misc'
import {
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
getRelaysForEventChildren, sampleRelays,
} from 'src/agent/relays'
import database from 'src/agent/database'
import {people} from 'src/agent/state'
import pool from 'src/agent/pool'
import sync from 'src/agent/sync'
const getStalePubkeys = pubkeys => {
// If we're not reloading, only get pubkeys we don't already know about
return uniq(pubkeys).filter(pubkey => {
const p = database.people.get(pubkey)
const p = people.get(pubkey)
return !p || p.updated_at < now() - timedelta(1, 'days')
})
@ -39,11 +39,10 @@ const listen = ({relays, filter, onChunk = null, shouldProcess = true, delay = 5
const load = ({relays, filter, onChunk = null, shouldProcess = true, timeout = 5000}) => {
return new Promise(resolve => {
const now = Date.now()
const done = new Set()
const allEvents = []
const attemptToComplete = async () => {
const attemptToComplete = async isTimeout => {
const sub = await subPromise
// If we've already unsubscribed we're good
@ -52,7 +51,6 @@ const load = ({relays, filter, onChunk = null, shouldProcess = true, timeout = 5
}
const isDone = done.size === relays.length
const isTimeout = Date.now() - now >= timeout
if (isTimeout) {
const timedOutRelays = reject(r => done.has(r.url), relays)
@ -78,7 +76,7 @@ const load = ({relays, filter, onChunk = null, shouldProcess = true, timeout = 5
}
// If a relay takes too long, give up
setTimeout(attemptToComplete, timeout)
setTimeout(() => attemptToComplete(true), timeout)
const subPromise = pool.subscribe({
relays,
@ -98,11 +96,11 @@ const load = ({relays, filter, onChunk = null, shouldProcess = true, timeout = 5
}),
onEose: url => {
done.add(url)
attemptToComplete()
attemptToComplete(false)
},
onError: url => {
done.add(url)
attemptToComplete()
attemptToComplete(false)
},
})
}) as Promise<MyEvent[]>

View File

@ -2,10 +2,10 @@ import type {Relay} from 'src/util/types'
import LRUCache from 'lru-cache'
import {warn} from 'src/util/logger'
import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} from 'ramda'
import {first, createMap} from 'hurdak/lib/hurdak'
import {first} from 'hurdak/lib/hurdak'
import {Tags, isRelay, findReplyId} from 'src/util/nostr'
import {shuffle, fetchJson} from 'src/util/misc'
import database from 'src/agent/database'
import {relays, routes} from 'src/agent/state'
import pool from 'src/agent/pool'
import user from 'src/agent/user'
@ -25,25 +25,22 @@ import user from 'src/agent/user'
export const initializeRelayList = async () => {
// Throw some hardcoded defaults in there
await database.relays.bulkPatch(
createMap('url', [
{url: 'wss://brb.io'},
{url: 'wss://nostr.zebedee.cloud'},
{url: 'wss://nostr-pub.wellorder.net'},
{url: 'wss://relay.nostr.band'},
{url: 'wss://nostr.pleb.network'},
{url: 'wss://relay.nostrich.de'},
{url: 'wss://relay.damus.io'},
])
)
await relays.bulkPatch([
{url: 'wss://brb.io'},
{url: 'wss://nostr.zebedee.cloud'},
{url: 'wss://nostr-pub.wellorder.net'},
{url: 'wss://relay.nostr.band'},
{url: 'wss://nostr.pleb.network'},
{url: 'wss://relay.nostrich.de'},
{url: 'wss://relay.damus.io'},
])
// Load relays from nostr.watch via dufflepud
try {
const url = import.meta.env.VITE_DUFFLEPUD_URL + '/relay'
const json = await fetchJson(url)
const relays = json.relays.filter(isRelay)
await database.relays.bulkPatch(createMap('url', map(objOf('url'), relays)))
await relays.bulkPatch(map(objOf('url'), json.relays.filter(isRelay)))
} catch (e) {
warn("Failed to fetch relays list", e)
}
@ -53,13 +50,13 @@ export const initializeRelayList = async () => {
const _getPubkeyRelaysCache = new LRUCache({max: 1000})
export const getPubkeyRelays = (pubkey, mode = null, routes = null) => {
export const getPubkeyRelays = (pubkey, mode = null, routesOverride = null) => {
const filter = mode ? {pubkey, mode} : {pubkey}
const key = [mode, pubkey].join(':')
let result = routes || _getPubkeyRelaysCache.get(key)
let result = routesOverride || _getPubkeyRelaysCache.get(key)
if (!result) {
result = database.routes.all(filter)
result = routes.all(filter)
_getPubkeyRelaysCache.set(key, result)
}
@ -75,7 +72,7 @@ export const getPubkeyWriteRelays = pubkey => getPubkeyRelays(pubkey, 'write')
export const getAllPubkeyRelays = (pubkeys, mode = null) => {
// As an optimization, filter the database once and group by pubkey
const filter = mode ? {pubkey: pubkeys, mode} : {pubkey: pubkeys}
const routesByPubkey = groupBy(prop('pubkey'), database.routes.all(filter))
const routesByPubkey = groupBy(prop('pubkey'), routes.all(filter))
return aggregateScores(
pubkeys.map(

View File

@ -1,10 +1,10 @@
import {uniq, without} from 'ramda'
import {Tags} from 'src/util/nostr'
import database from 'src/agent/database'
import {getPersonWithFallback} from 'src/agent/state'
import user from 'src/agent/user'
export const getFollows = pubkey =>
Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all()
Tags.wrap(getPersonWithFallback(pubkey).petnames).type("p").values().all()
export const getNetwork = pubkey => {
const follows = getFollows(pubkey)

25
src/agent/state.ts Normal file
View File

@ -0,0 +1,25 @@
import {pluck, all, identity} from "ramda"
import {derived} from "svelte/store"
import {Table, registry} from "src/agent/table"
export const people = new Table("people", "pubkey")
export const contacts = new Table("contacts", "pubkey")
export const rooms = new Table("rooms", "id")
export const alerts = new Table("alerts", "id")
export const relays = new Table("relays", "url")
export const routes = new Table("routes", "id")
export const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
export const ready = derived(pluck("ready", Object.values(registry)), all(identity))
export const onReady = cb => {
const unsub = ready.subscribe($ready => {
if ($ready) {
cb()
setTimeout(() => unsub())
}
})
}
window.state = {people, contacts, rooms, alerts, relays, routes}

View File

@ -1,10 +1,10 @@
import {uniq, pick, identity, isEmpty} from 'ramda'
import {nip05} from 'nostr-tools'
import {noop, createMap, ensurePlural, chunk, switcherFn} from 'hurdak/lib/hurdak'
import {noop, ensurePlural, chunk, switcherFn} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger'
import {lnurlEncode, tryFunc, lnurlDecode, tryFetch, now, sleep, tryJson, timedelta, shuffle, hash} from 'src/util/misc'
import {Tags, roomAttrs, personKinds, isRelay, isShareableRelay, normalizeRelayUrl} from 'src/util/nostr'
import database from 'src/agent/database'
import {getPersonWithFallback, people, relays, rooms, routes} from 'src/agent/state'
const processEvents = async events => {
await Promise.all([
@ -30,13 +30,12 @@ const batchProcess = async (processChunk, events) => {
const processProfileEvents = async events => {
const profileEvents = events.filter(e => personKinds.includes(e.kind))
const updates = {}
for (const e of profileEvents) {
const person = database.getPersonWithFallback(e.pubkey)
const person = getPersonWithFallback(e.pubkey)
updates[e.pubkey] = {
people.put({
...person,
...updates[e.pubkey],
pubkey: e.pubkey,
...switcherFn(e.kind, {
0: () => tryJson(() => {
const kind0 = JSON.parse(e.content)
@ -53,18 +52,14 @@ const processProfileEvents = async events => {
}
return {
kind0: {
...person?.kind0,
...updates[e.pubkey]?.kind0,
...kind0,
},
kind0: {...person?.kind0, ...kind0},
kind0_updated_at: e.created_at,
}
}
}),
2: () => {
if (e.created_at > (person.relays_updated_at || 0)) {
const {relays = []} = database.getPersonWithFallback(e.pubkey)
const {relays = []} = getPersonWithFallback(e.pubkey)
return {
relays_updated_at: e.created_at,
@ -141,20 +136,15 @@ const processProfileEvents = async events => {
},
}),
updated_at: now(),
}
}
if (!isEmpty(updates)) {
await database.people.bulkPatch(updates)
})
}
}
// Chat rooms
const processRoomEvents = async events => {
const processRoomEvents = events => {
const roomEvents = events.filter(e => [40, 41].includes(e.kind))
const updates = {}
for (const e of roomEvents) {
const content = tryJson(() => pick(roomAttrs, JSON.parse(e.content)))
const roomId = e.kind === 40 ? e.id : Tags.from(e).type("e").values().first()
@ -164,25 +154,20 @@ const processRoomEvents = async events => {
continue
}
const room = database.rooms.get(roomId)
const room = rooms.get(roomId)
// Don't let old edits override new ones
if (room?.updated_at >= e.created_at) {
continue
}
updates[roomId] = {
rooms.put({
...room,
...updates,
...content,
id: roomId,
pubkey: e.pubkey,
updated_at: e.created_at,
}
}
if (!isEmpty(updates)) {
await database.rooms.bulkPatch(updates)
})
}
}
@ -205,7 +190,7 @@ const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
const id = hash([pubkey, url, mode].join('')).toString()
const score = getWeight(type) * (1 - (now() - created_at) / timedelta(30, 'days'))
const defaults = {id, pubkey, url, mode, score: 0, count: 0, types: []}
const route = database.routes.get(id) || defaults
const route = routes.get(id) || defaults
const newTotalScore = route.score * route.count + score
const newCount = route.count + 1
@ -293,8 +278,8 @@ const processRoutes = async events => {
updates = updates.filter(identity)
if (!isEmpty(updates)) {
await database.relays.bulkPatch(createMap('url', updates.map(pick(['url']))))
await database.routes.bulkPut(createMap('id', updates))
await relays.bulkPatch(updates.map(pick(['url'])))
await routes.bulkPut(updates)
}
}
@ -303,22 +288,20 @@ const processRoutes = async events => {
const verifyNip05 = (pubkey, as) =>
nip05.queryProfile(as).then(result => {
if (result?.pubkey === pubkey) {
const person = database.getPersonWithFallback(pubkey)
const person = getPersonWithFallback(pubkey)
database.people.patch({...person, verified_as: as})
people.patch({...person, verified_as: as})
if (result.relays?.length > 0) {
const urls = result.relays.filter(isRelay)
database.relays.bulkPatch(
createMap('url', urls.map(url => ({url: normalizeRelayUrl(url)})))
)
relays.bulkPatch(urls.map(url => ({url: normalizeRelayUrl(url)})))
database.routes.bulkPut(
createMap('id', urls.flatMap(url => [
routes.bulkPut(
urls.flatMap(url => [
calculateRoute(pubkey, url, 'nip05', 'write', now()),
calculateRoute(pubkey, url, 'nip05', 'read', now()),
]).filter(identity))
]).filter(identity)
)
}
}
@ -347,7 +330,7 @@ const verifyZapper = async (pubkey, address) => {
const lnurl = lnurlEncode('lnurl', url)
if (zapper?.allowsNostr && zapper?.nostrPubkey) {
database.people.patch({pubkey, zapper, lnurl})
people.patch({pubkey, zapper, lnurl})
}
}

208
src/agent/table.ts Normal file
View File

@ -0,0 +1,208 @@
import type {Writable} from 'svelte/store'
import LRUCache from 'lru-cache'
import {throttle} from 'throttle-debounce'
import {objOf, is, without} from 'ramda'
import {writable} from 'svelte/store'
import {isObject, mapValues, ensurePlural} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger'
import {where} from 'src/util/misc'
import {lf} from 'src/agent/database'
// Local copy of data so we can provide a sync observable interface. The worker
// is just for storing data and processing expensive queries
type CacheEntry = [string, {value: any}]
type TableOpts = {
maxEntries?: number
initialize?: (table: Table) => Promise<Array<CacheEntry>>
}
export const registry = {} as Record<string, Table>
export class Table {
name: string
pk: string
opts: TableOpts
cache: LRUCache<string, any>
listeners: Array<(Table) => void>
ready: Writable<boolean>
constructor(name, pk, opts: TableOpts = {}) {
this.name = name
this.pk = pk
this.opts = {maxEntries: 1000, initialize: t => this.dump(), ...opts}
this.cache = new LRUCache({max: this.opts.maxEntries})
this.listeners = []
this.ready = writable(false)
registry[name] = this
// Sync from storage initially
;(async () => {
const t = Date.now()
this.cache.load(await this.opts.initialize(this) || [])
this._notify()
log(`Table ${name} ready in ${Date.now() - t}ms (${this.cache.size} records)`)
this.ready.set(true)
})()
}
_persist = throttle(4_000, () => {
lf('setItem', this.name, this.cache.dump())
})
_notify() {
// Notify subscribers
for (const cb of this.listeners) {
cb(this)
}
// Save to localstorage
this._persist()
}
subscribe(cb) {
cb = throttle(100, cb)
this.listeners.push(cb)
cb(this)
return () => {
this.listeners = without([cb], this.listeners)
}
}
bulkPut(items) {
for (const item of items) {
const k = item[this.pk]
if (!k) {
throw new Error(`Missing primary key on ${this.name}`)
}
this.cache.set(k, item)
}
this._persist()
}
put(item) {
this.bulkPut([item])
}
bulkPatch(items) {
for (const item of items) {
const k = item[this.pk]
if (!k) {
throw new Error(`Missing primary key on ${this.name}`)
}
this.cache.set(k, {...this.cache.get(k), ...item})
}
this._persist()
}
patch(item) {
this.bulkPatch([item])
}
bulkRemove(ks) {
for (const k of ks) {
this.cache.delete(k)
}
this._persist()
}
remove(k) {
this.bulkRemove([k])
}
async drop() {
this.cache.clear()
return lf('removeItem', this.name)
}
async dump() {
let data = await lf('getItem', this.name) || []
// Backwards compat - we used to store objects rather than cache dump arrays
if (isObject(data)) {
data = Object.entries(mapValues(objOf('value'), data))
}
return data as Array<CacheEntry>
}
toArray() {
const result = []
for (const item of this.cache.values()) {
result.push(item)
}
return result
}
all(spec = {}) {
return this.toArray().filter(where(spec))
}
find(spec = {}) {
return this.cache.find(where(spec))
}
get(k) {
return this.cache.get(k)
}
}
// Helper to allow us to listen to changes of any given table
const listener = (() => {
let listeners = []
for (const table of Object.values(registry) as Array<Table>) {
table.subscribe(() => listeners.forEach(f => f(table.name)))
}
return {
subscribe: f => {
listeners.push(f)
return () => {
listeners = without([f], listeners)
}
},
}
})()
// Helper to re-run a query every time a given table changes
export const watch = (names, f) => {
names = ensurePlural(names)
const store = writable(null)
const tables = names.map(name => registry[name])
// Initialize synchronously if possible
const initialValue = f(...tables)
if (is(Promise, initialValue)) {
initialValue.then(v => store.set(v))
} else {
store.set(initialValue)
}
// Debounce refresh so we don't get UI lag
const refresh = throttle(300, async () => store.set(await f(...tables)))
// Listen for changes
listener.subscribe(name => {
if (names.includes(name)) {
refresh()
}
})
return store
}
// Methods that work on all tables
export const dropAll = async () => {
for (const table of Object.values(registry)) {
await table.drop()
log(`Successfully dropped table ${table.name}`)
}
}

View File

@ -4,7 +4,7 @@ import {slice, identity, prop, find, pipe, assoc, whereEq, when, concat, reject,
import {findReplyId, findRootId} from 'src/util/nostr'
import {synced} from 'src/util/misc'
import {derived} from 'svelte/store'
import database from 'src/agent/database'
import {people} from 'src/agent/state'
import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd'
@ -34,14 +34,8 @@ const settings = synced("agent/user/settings", {
})
const profile = derived(
[keys.pubkey, database.people as Readable<any>],
([pubkey, $people]) => {
if (!pubkey) {
return null
}
return $people[pubkey] || {pubkey}
}
[keys.pubkey, people as Readable<any>],
([pubkey, t]) => pubkey ? (t.get(pubkey) || {pubkey}) : null
) as Readable<Person>
const profileKeyWithDefault = (key, stores) => derived(

View File

@ -5,7 +5,8 @@ import {createMap} from 'hurdak/lib/hurdak'
import {synced, tryJson, now, timedelta} from 'src/util/misc'
import {Tags, personKinds, isAlert, asDisplayEvent, findReplyId} from 'src/util/nostr'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import {alerts, contacts, rooms} from 'src/agent/state'
import {watch} from 'src/agent/table'
import network from 'src/agent/network'
let listener
@ -24,18 +25,18 @@ const seenAlertIds = synced('app/alerts/seenAlertIds', [])
export const lastChecked = synced('app/alerts/lastChecked', {})
export const newAlerts = derived(
[database.watch('alerts', t => pluck('created_at', t.all()).reduce(max, 0)), lastChecked],
[watch('alerts', t => pluck('created_at', t.all()).reduce(max, 0)), lastChecked],
([$lastAlert, $lastChecked]) => $lastAlert > ($lastChecked.alerts || 0)
)
export const newDirectMessages = derived(
[database.watch('contacts', t => t.all()), lastChecked],
[watch('contacts', t => t.all()), lastChecked],
([contacts, $lastChecked]) =>
Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], contacts))
)
export const newChatMessages = derived(
[database.watch('rooms', t => t.all()), lastChecked],
[watch('rooms', t => t.all()), lastChecked],
([rooms, $lastChecked]) =>
Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], rooms))
)
@ -75,33 +76,33 @@ const processAlerts = async (pubkey, events) => {
zaps.filter(isPubkeyChild).forEach(e => {
const parent = parents[findReplyId(e)]
const note = asAlert(database.alerts.get(parent.id) || parent)
const note = asAlert(alerts.get(parent.id) || parent)
const meta = Tags.from(e).asMeta()
const request = tryJson(() => JSON.parse(meta.description))
if (request) {
database.alerts.put({...note, zappedBy: uniq(note.zappedBy.concat(request.pubkey))})
alerts.put({...note, zappedBy: uniq(note.zappedBy.concat(request.pubkey))})
}
})
likes.filter(isPubkeyChild).forEach(e => {
const parent = parents[findReplyId(e)]
const note = asAlert(database.alerts.get(parent.id) || parent)
const note = asAlert(alerts.get(parent.id) || parent)
database.alerts.put({...note, likedBy: uniq(note.likedBy.concat(e.pubkey))})
alerts.put({...note, likedBy: uniq(note.likedBy.concat(e.pubkey))})
})
replies.forEach(e => {
const parent = parents[findReplyId(e)]
const note = asAlert(database.alerts.get(parent.id) || parent)
const note = asAlert(alerts.get(parent.id) || parent)
database.alerts.put({...note, repliesFrom: uniq(note.repliesFrom.concat(e.pubkey))})
alerts.put({...note, repliesFrom: uniq(note.repliesFrom.concat(e.pubkey))})
})
mentions.forEach(e => {
const note = database.alerts.get(e.id) || asAlert(e)
const note = alerts.get(e.id) || asAlert(e)
database.alerts.put({...note, isMention: true})
alerts.put({...note, isMention: true})
})
}
@ -118,12 +119,12 @@ const processMessages = async (pubkey, events) => {
const recipient = Tags.from(message).type("p").values().first()
$lastChecked[recipient] = Math.max($lastChecked[recipient] || 0, message.created_at)
database.contacts.patch({pubkey: recipient, accepted: true})
contacts.patch({pubkey: recipient, accepted: true})
} else {
const contact = database.contacts.get(message.pubkey)
const contact = contacts.get(message.pubkey)
const lastMessage = Math.max(contact?.lastMessage || 0, message.created_at)
database.contacts.patch({pubkey: message.pubkey, lastMessage})
contacts.patch({pubkey: message.pubkey, lastMessage})
}
}
@ -145,10 +146,10 @@ const processChats = async (pubkey, events) => {
if (message.pubkey === pubkey) {
$lastChecked[id] = Math.max($lastChecked[id] || 0, message.created_at)
} else {
const room = database.rooms.get(id)
const room = rooms.get(id)
const lastMessage = Math.max(room?.lastMessage || 0, message.created_at)
database.rooms.patch({id, lastMessage})
rooms.patch({id, lastMessage})
}
}
@ -159,7 +160,7 @@ const processChats = async (pubkey, events) => {
const listen = async pubkey => {
// Include an offset so we don't miss alerts on one relay but not another
const since = now() - timedelta(7, 'days')
const roomIds = pluck('id', database.rooms.all({joined: true}))
const roomIds = pluck('id', rooms.all({joined: true}))
if (listener) {
listener.unsub()

View File

@ -5,7 +5,7 @@ import {renderContent} from 'src/util/html'
import {displayPerson, findReplyId} from 'src/util/nostr'
import {getUserFollows} from 'src/agent/social'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import {getPersonWithFallback} from 'src/agent/state'
import network from 'src/agent/network'
import keys from 'src/agent/keys'
import alerts from 'src/app/alerts'
@ -46,7 +46,7 @@ export const renderNote = (note, {showEntire = false}) => {
}
const pubkey = note.tags[parseInt(i)][1]
const person = database.getPersonWithFallback(pubkey)
const person = getPersonWithFallback(pubkey)
const name = displayPerson(person)
const path = routes.person(pubkey)

View File

@ -6,7 +6,7 @@
import {sleep, createScroller, Cursor} from "src/util/misc"
import Spinner from "src/partials/Spinner.svelte"
import user from "src/agent/user"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import network from "src/agent/network"
export let loadMessages
@ -26,7 +26,7 @@
// Group messages so we're only showing the person once per chunk
annotatedMessages = reverse(
sortBy(prop("created_at"), uniqBy(prop("id"), messages)).reduce((mx, m) => {
const person = database.getPersonWithFallback(m.pubkey)
const person = getPersonWithFallback(m.pubkey)
const showPerson = person.pubkey !== getPath(["person", "pubkey"], last(mx))
return mx.concat({...m, person, showPerson})

View File

@ -7,7 +7,7 @@
import {displayPerson} from "src/util/nostr"
import {fromParentOffset} from "src/util/html"
import Badge from "src/partials/Badge.svelte"
import database from "src/agent/database"
import {people} from "src/agent/state"
export let onSubmit
@ -17,7 +17,7 @@
let input = null
let prevContent = ""
const search = fuzzy(database.people.all({"kind0.name:!nil": null}), {
const search = fuzzy(people.all({"kind0.name:!nil": null}), {
keys: ["kind0.name", "pubkey"],
})

View File

@ -7,7 +7,7 @@
import Content from "src/partials/Content.svelte"
import Alert from "src/views/alerts/Alert.svelte"
import Mention from "src/views/alerts/Mention.svelte"
import database from "src/agent/database"
import {alerts} from "src/agent/state"
import user from "src/agent/user"
import {lastChecked} from "src/app/alerts"
@ -25,7 +25,7 @@
// Filter out mutes, and alerts for which we failed to find the required context. The bug
// is really upstream of this, but it's an easy fix
const events = user
.applyMutes(database.alerts.all())
.applyMutes(alerts.all())
.filter(e => any(k => e[k]?.length > 0, ["replies", "likedBy", "zappedBy"]) || e.isMention)
notes = sortBy(e => -e.created_at, events).slice(0, limit)

View File

@ -9,7 +9,7 @@
import user from "src/agent/user"
import {getRelaysForEventChildren, sampleRelays} from "src/agent/relays"
import network from "src/agent/network"
import database from "src/agent/database"
import {watch} from "src/agent/table"
import cmd from "src/agent/cmd"
import {modal} from "src/app/ui"
import {lastChecked} from "src/app/alerts"
@ -18,7 +18,7 @@
export let entity
const id = toHex(entity)
const room = database.watch("rooms", t => t.get(id) || {id})
const room = watch("rooms", t => t.get(id) || {id})
const getRelays = () => sampleRelays($room ? getRelaysForEventChildren($room) : [])
const listenForMessages = onChunk =>

View File

@ -5,7 +5,7 @@
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import ChatListItem from "src/views/chat/ChatListItem.svelte"
import database from "src/agent/database"
import {watch} from "src/agent/table"
import network from "src/agent/network"
import {getUserReadRelays} from "src/agent/relays"
import {modal} from "src/app/ui"
@ -14,8 +14,8 @@
let search
let results = []
const userRooms = database.watch("rooms", t => t.all({joined: true}))
const otherRooms = database.watch("rooms", t => t.all({"joined:!eq": true}))
const userRooms = watch("rooms", t => t.all({joined: true}))
const otherRooms = watch("rooms", t => t.all({"joined:!eq": true}))
$: search = fuzzy($otherRooms, {keys: ["name", "about"]})
$: results = search(q).slice(0, 50)

View File

@ -2,14 +2,14 @@
import {fly} from "svelte/transition"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import database from "src/agent/database"
import {dropAll} from "src/agent/table"
let confirmed = false
const confirm = async () => {
confirmed = true
await database.dropAll()
await dropAll()
localStorage.clear()

View File

@ -8,7 +8,8 @@
import Channel from "src/partials/Channel.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {watch} from "src/agent/table"
import network from "src/agent/network"
import keys from "src/agent/keys"
import user from "src/agent/user"
@ -16,13 +17,13 @@
import {routes} from "src/app/ui"
import {lastChecked} from "src/app/alerts"
import {renderNote} from "src/app"
import PersonCircle from "src/partials/PersonCircle.svelte";
import PersonCircle from "src/partials/PersonCircle.svelte"
export let entity
let crypt = keys.getCrypt()
let pubkey = toHex(entity)
let person = database.watch("people", () => database.getPersonWithFallback(pubkey))
let person = watch("people", () => getPersonWithFallback(pubkey))
lastChecked.update(assoc(pubkey, now()))

View File

@ -4,7 +4,7 @@
import Tabs from "src/partials/Tabs.svelte"
import Content from "src/partials/Content.svelte"
import MessagesListItem from "src/views/messages/MessagesListItem.svelte"
import database from "src/agent/database"
import {watch} from "src/agent/table"
let activeTab = "messages"
@ -12,8 +12,8 @@
activeTab = tab
}
const accepted = database.watch("contacts", t => t.all({accepted: true}))
const requests = database.watch("contacts", t => t.all({"accepted:!eq": true}))
const accepted = watch("contacts", t => t.all({accepted: true}))
const requests = watch("contacts", t => t.all({"accepted:!eq": true}))
const getContacts = tab => sortBy(c => -c.lastMessage, tab === "messages" ? $accepted : $requests)

View File

@ -18,9 +18,9 @@
import user from "src/agent/user"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import network from "src/agent/network"
import database from "src/agent/database"
import {getPersonWithFallback, people} from "src/agent/state"
import {routes, modal} from "src/app/ui"
import PersonCircle from "src/partials/PersonCircle.svelte";
import PersonCircle from "src/partials/PersonCircle.svelte"
export let npub
export let activeTab
@ -35,7 +35,7 @@
let muted = false
let followers = new Set()
let followersCount = tweened(0, {interpolate, duration: 1000})
let person = database.getPersonWithFallback(pubkey)
let person = getPersonWithFallback(pubkey)
let loading = true
let showActions = false
let actions = []
@ -88,12 +88,12 @@
// Refresh our person
network.loadPeople([pubkey], {force: true}).then(() => {
person = database.getPersonWithFallback(pubkey)
person = getPersonWithFallback(pubkey)
loading = false
})
// Prime our followers count
database.people.all().forEach(p => {
people.all().forEach(p => {
if (Tags.wrap(p.petnames).type("p").values().all().includes(pubkey)) {
followers.add(p.pubkey)
followersCount.set(followers.size)
@ -173,7 +173,7 @@
<Content>
<div class="flex gap-4">
<PersonCircle person={person} size={16} class="sm:h-32 sm:w-32" />
<PersonCircle {person} size={16} class="sm:h-32 sm:w-32" />
<div class="flex flex-grow flex-col gap-4">
<div class="flex items-start justify-between gap-4">
<div class="flex flex-grow flex-col gap-2">

View File

@ -7,22 +7,22 @@
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Feed from "src/views/feed/Feed.svelte"
import database from "src/agent/database"
import {relays} from "src/agent/state"
import pool from "src/agent/pool"
import user from "src/agent/user"
export let url
const relay = database.relays.get(url) || {url}
const relay = relays.get(url) || {url}
let quality = null
let message = null
let showStatus = false
let joined = false
const {relays} = user
const {relays: userRelays} = user
$: joined = find(propEq("url", relay.url), $relays)
$: joined = find(propEq("url", relay.url), $userRelays)
onMount(() => {
return poll(10_000, async () => {
@ -73,7 +73,7 @@
</Anchor>
{/if}
{#if joined}
{#if $relays.length > 1}
{#if $userRelays.length > 1}
<Anchor
type="button"
class="flex items-center gap-2 rounded-full"

View File

@ -6,7 +6,7 @@
import RelaySearch from "src/views/relays/RelaySearch.svelte"
import RelayCard from "src/views/relays/RelayCard.svelte"
import PersonSearch from "src/views/person/PersonSearch.svelte"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import user from "src/agent/user"
export let enforceRelays = true
@ -62,7 +62,7 @@
<h1 class="text-2xl">Your Follows</h1>
{/if}
{#each $petnamePubkeys as pubkey (pubkey)}
<PersonInfo person={database.getPersonWithFallback(pubkey)} />
<PersonInfo person={getPersonWithFallback(pubkey)} />
{:else}
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">

View File

@ -3,7 +3,7 @@
import Badge from "src/partials/Badge.svelte"
import Popover from "src/partials/Popover.svelte"
import {formatTimestamp} from "src/util/misc"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {modal} from "src/app/ui"
export let note
@ -35,7 +35,7 @@
<div slot="tooltip">
<div class="grid grid-cols-2 gap-y-2 gap-x-4">
{#each pubkeys as pubkey}
<Badge person={database.getPersonWithFallback(pubkey)} />
<Badge person={getPersonWithFallback(pubkey)} />
{/each}
</div>
</div>

View File

@ -4,13 +4,13 @@
import {displayPerson} from "src/util/nostr"
import Popover from "src/partials/Popover.svelte"
import PersonSummary from "src/views/person/PersonSummary.svelte"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {modal} from "src/app/ui"
import PersonCircle from "src/partials/PersonCircle.svelte";
import PersonCircle from "src/partials/PersonCircle.svelte"
export let note
const person = database.getPersonWithFallback(note.pubkey)
const person = getPersonWithFallback(note.pubkey)
</script>
<button
@ -19,7 +19,7 @@
on:click={() => modal.set({type: "note/detail", note})}>
<div class="relative flex w-full items-center justify-between gap-2">
<div class="flex items-center gap-2">
<PersonCircle person={person} />
<PersonCircle {person} />
<div on:click|stopPropagation>
<Popover class="inline-block">
<div slot="trigger" class="font-bold">

View File

@ -8,7 +8,7 @@
import Textarea from "src/partials/Textarea.svelte"
import Button from "src/partials/Button.svelte"
import {getUserWriteRelays} from "src/agent/relays"
import database from "src/agent/database"
import {rooms} from "src/agent/state"
import cmd from "src/agent/cmd"
import {toast, modal} from "src/app/ui"
import {publishWithToast} from "src/app"
@ -45,7 +45,7 @@
const [event] = await publishWithToast(relays, cmd.createRoom(room))
// Auto join the room the user just created
database.rooms.patch({id: event.id, joined: true})
rooms.patch({id: event.id, joined: true})
}
modal.set(null)

View File

@ -4,13 +4,13 @@
import {fly} from "svelte/transition"
import {ellipsize} from "hurdak/lib/hurdak"
import Anchor from "src/partials/Anchor.svelte"
import database from "src/agent/database"
import {rooms} from "src/agent/state"
export let room
const enter = () => navigate(`/chat/${nip19.noteEncode(room.id)}`)
const join = () => database.rooms.patch({id: room.id, joined: true})
const leave = () => database.rooms.patch({id: room.id, joined: false})
const join = () => rooms.patch({id: room.id, joined: true})
const leave = () => rooms.patch({id: room.id, joined: false})
</script>
<button

View File

@ -12,7 +12,7 @@
import RelayCardSimple from "src/partials/RelayCardSimple.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Modal from "src/partials/Modal.svelte"
import database from "src/agent/database"
import {watch} from "src/agent/table"
import network from "src/agent/network"
import user from "src/agent/user"
import {loadAppData} from "src/app"
@ -24,7 +24,7 @@
let currentRelays = {} as Record<number, Relay>
let attemptedRelays = new Set()
let customRelays = []
let knownRelays = database.watch("relays", table => shuffle(table.all()))
let knownRelays = watch("relays", table => shuffle(table.all()))
let allRelays = []
$: allRelays = $knownRelays.concat(customRelays)

View File

@ -4,14 +4,14 @@
import {fly} from "svelte/transition"
import {ellipsize} from "hurdak/lib/hurdak"
import {displayPerson} from "src/util/nostr"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {lastChecked} from "src/app/alerts"
import PersonCircle from "src/partials/PersonCircle.svelte";
import PersonCircle from "src/partials/PersonCircle.svelte"
export let contact
const newMessages = contact.lastMessage > $lastChecked[contact.pubkey]
const person = database.getPersonWithFallback(contact.pubkey)
const person = getPersonWithFallback(contact.pubkey)
const enter = () => navigate(`/messages/${nip19.npubEncode(contact.pubkey)}`)
</script>
@ -20,7 +20,7 @@
px-4 py-6 transition-all hover:bg-medium"
on:click={enter}
in:fly={{y: 20}}>
<PersonCircle size={14} person={person} />
<PersonCircle size={14} {person} />
<div class="flex min-w-0 flex-grow flex-col justify-start gap-2">
<div class="flex flex-grow items-start justify-between gap-2">
<div class="flex items-center gap-2 overflow-hidden">

View File

@ -29,11 +29,12 @@
import keys from "src/agent/keys"
import network from "src/agent/network"
import {getEventPublishRelays, getRelaysForEventParent} from "src/agent/relays"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {watch} from "src/agent/table"
import cmd from "src/agent/cmd"
import {routes} from "src/app/ui"
import {publishWithToast} from "src/app"
import PersonCircle from "src/partials/PersonCircle.svelte";
import PersonCircle from "src/partials/PersonCircle.svelte"
export let note
export let depth = 0
@ -59,7 +60,7 @@
const links = extractUrls(note.content)
const showEntire = anchorId === note.id
const interactive = !anchorId || !showEntire
const person = database.watch("people", () => database.getPersonWithFallback(note.pubkey))
const person = watch("people", () => getPersonWithFallback(note.pubkey))
let likes, flags, zaps, like, flag, border, childrenContainer, noteContainer, canZap
let muted = false
@ -507,7 +508,7 @@
<button
class="fa fa-times cursor-pointer"
on:click|stopPropagation={() => removeMention(p)} />
{displayPerson(database.getPersonWithFallback(p))}
{displayPerson(getPersonWithFallback(p))}
</div>
{:else}
<div class="text-light inline-block">No mentions</div>

View File

@ -16,7 +16,8 @@
import Modal from "src/partials/Modal.svelte"
import Heading from "src/partials/Heading.svelte"
import {getUserWriteRelays} from "src/agent/relays"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {watch} from "src/agent/table"
import cmd from "src/agent/cmd"
import {toast, modal} from "src/app/ui"
import {publishWithToast} from "src/app"
@ -30,7 +31,7 @@
let q = ""
let search
const knownRelays = database.watch("relays", t => t.all())
const knownRelays = watch("relays", t => t.all())
$: {
const joined = new Set(pluck("url", relays))
@ -91,7 +92,7 @@
onMount(() => {
if (pubkey) {
const person = database.getPersonWithFallback(pubkey)
const person = getPersonWithFallback(pubkey)
input.type("@" + displayPerson(person))
input.trigger({key: "Enter"})

View File

@ -13,7 +13,7 @@
import OnboardingComplete from "src/views/onboarding/OnboardingComplete.svelte"
import {getFollows} from "src/agent/social"
import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import network from "src/agent/network"
import user from "src/agent/user"
import keys from "src/agent/keys"
@ -44,7 +44,7 @@
await user.updatePetnames(() =>
follows.map(pubkey => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
const name = displayPerson(database.getPersonWithFallback(pubkey))
const name = displayPerson(getPersonWithFallback(pubkey))
return ["p", pubkey, url, name]
})

View File

@ -6,7 +6,8 @@
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import PersonInfo from "src/partials/PersonInfo.svelte"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {watch} from "src/agent/table"
import {modal} from "src/app/ui"
export let follows
@ -14,7 +15,7 @@
let q = ""
let search
const knownPeople = database.watch("people", t => t.all({"kind0.name:!nil": null}))
const knownPeople = watch("people", t => t.all({"kind0.name:!nil": null}))
$: search = fuzzy(
$knownPeople.filter(p => !follows.includes(p.pubkey)),
@ -54,7 +55,7 @@
</div>
{:else}
{#each follows as pubkey}
<PersonInfo person={database.getPersonWithFallback(pubkey)} {removePetname} />
<PersonInfo person={getPersonWithFallback(pubkey)} {removePetname} />
{/each}
{/if}
<div class="flex items-center gap-2">

View File

@ -6,7 +6,7 @@
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import database from "src/agent/database"
import {watch} from "src/agent/table"
import {modal} from "src/app/ui"
export let relays
@ -14,7 +14,7 @@
let q = ""
let search
const knownRelays = database.watch("relays", t => t.all())
const knownRelays = watch("relays", t => t.all())
$: {
const joined = new Set(pluck("url", relays))

View File

@ -1,12 +1,13 @@
<script type="ts">
import Content from "src/partials/Content.svelte"
import PersonInfo from "src/views/person/PersonInfo.svelte"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {watch} from "src/agent/table"
import network from "src/agent/network"
export let pubkeys
const people = database.watch("people", t => pubkeys.map(database.getPersonWithFallback))
const people = watch("people", t => pubkeys.map(getPersonWithFallback))
network.loadPeople(pubkeys)
</script>

View File

@ -5,7 +5,7 @@
import Spinner from "src/partials/Spinner.svelte"
import PersonInfo from "src/views/person/PersonInfo.svelte"
import {getUserReadRelays} from "src/agent/relays"
import database from "src/agent/database"
import {watch} from "src/agent/table"
import network from "src/agent/network"
import user from "src/agent/user"
@ -15,7 +15,7 @@
let results = []
const {petnamePubkeys} = user
const search = database.watch("people", t =>
const search = watch("people", t =>
fuzzy(t.all({"kind0.name:!nil": null}), {
keys: ["kind0.name", "kind0.about", "pubkey"],
})

View File

@ -6,15 +6,16 @@
import Anchor from "src/partials/Anchor.svelte"
import user from "src/agent/user"
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
import database from "src/agent/database"
import {getPersonWithFallback} from "src/agent/state"
import {watch} from "src/agent/table"
import {routes} from "src/app/ui"
import PersonCircle from "src/partials/PersonCircle.svelte";
import PersonCircle from "src/partials/PersonCircle.svelte"
export let pubkey
const {petnamePubkeys, canPublish} = user
const getRelays = () => sampleRelays(getPubkeyWriteRelays(pubkey))
const person = database.watch("people", () => database.getPersonWithFallback(pubkey))
const person = watch("people", () => getPersonWithFallback(pubkey))
let following = false

View File

@ -3,12 +3,12 @@
import {fuzzy} from "src/util/misc"
import Input from "src/partials/Input.svelte"
import RelayCard from "src/views/relays/RelayCard.svelte"
import database from "src/agent/database"
import {watch} from "src/agent/table"
import user from "src/agent/user"
let q = ""
let search
let knownRelays = database.watch("relays", t => t.all())
let knownRelays = watch("relays", t => t.all())
const {relays} = user