mirror of
https://github.com/coracle-social/coracle.git
synced 2024-10-06 11:43:30 +00:00
Split database up, use lru cache as our table implementation
This commit is contained in:
parent
bc1329f9a8
commit
de91a06806
15
ROADMAP.md
15
ROADMAP.md
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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 () => {
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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[]>
|
||||
|
@ -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,8 +25,7 @@ import user from 'src/agent/user'
|
||||
|
||||
export const initializeRelayList = async () => {
|
||||
// Throw some hardcoded defaults in there
|
||||
await database.relays.bulkPatch(
|
||||
createMap('url', [
|
||||
await relays.bulkPatch([
|
||||
{url: 'wss://brb.io'},
|
||||
{url: 'wss://nostr.zebedee.cloud'},
|
||||
{url: 'wss://nostr-pub.wellorder.net'},
|
||||
@ -35,15 +34,13 @@ export const initializeRelayList = async () => {
|
||||
{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(
|
||||
|
@ -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
25
src/agent/state.ts
Normal 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}
|
@ -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
208
src/agent/table.ts
Normal 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}`)
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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})
|
||||
|
@ -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"],
|
||||
})
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 =>
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()))
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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"})
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
|
@ -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"],
|
||||
})
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user