Add new database built on localforage, apply to people

This commit is contained in:
Jonathan Staab 2023-02-08 17:35:42 -06:00
parent c1569a3903
commit 3ccec4d5b5
24 changed files with 424 additions and 103 deletions

View File

@ -54,6 +54,7 @@ If you like Coracle and want to support its development, you can donate sats via
# Maintenance
- [ ] Normalize relay urls (lowercase, strip trailing slash)
- [ ] Use nip 56 for reporting
- https://github.com/nostr-protocol/nips/pull/205#issuecomment-1419234230
- [ ] Change network tab to list relays the user is connected to
@ -65,7 +66,8 @@ If you like Coracle and want to support its development, you can donate sats via
# Current
- [ ] Make threshold it a setting, or maybe avoid `load` altogether
- [ ] Switch to localforage
- Check that firefox private mode works (it won't work in dev)
- [ ] Add modal for follows/followers
- [ ] Implement gossip model https://bountsr.org/code/2023/02/03/gossip-model.html
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP

BIN
package-lock.json generated

Binary file not shown.

View File

@ -36,6 +36,7 @@
"fuse.js": "^6.6.2",
"hurdak": "github:ConsignCloud/hurdak",
"husky": "^8.0.3",
"localforage": "^1.10.0",
"nostr-tools": "^1.2.1",
"npm-run-all": "^4.1.5",
"ramda": "^0.28.0",

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import "@fortawesome/fontawesome-free/css/fontawesome.css"
import "@fortawesome/fontawesome-free/css/solid.css"
@ -41,6 +41,7 @@
import Chat from "src/routes/Chat.svelte"
import ChatRoom from "src/routes/ChatRoom.svelte"
import Messages from "src/routes/Messages.svelte"
import _db from 'src/agent/database'
export let url = ""
@ -133,7 +134,9 @@
return {...await res.json(), url, refreshed_at: now()}
} catch (e) {
console.warn(e)
if (!e.toString().includes('Failed to fetch')) {
console.warn(e)
}
return {url, refreshed_at: now()}
}

View File

@ -1,10 +1,10 @@
import Dexie, {liveQuery} from 'dexie'
import {pick, isEmpty} from 'ramda'
import {nip05} from 'nostr-tools'
import {writable} from 'svelte/store'
import {noop, ensurePlural, createMap, switcherFn} from 'hurdak/lib/hurdak'
import {noop, ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {now} from 'src/util/misc'
import {personKinds, Tags, roomAttrs, isRelay} from 'src/util/nostr'
import database from 'src/agent/database'
export const lq = cb => liveQuery(async () => {
try {
@ -20,38 +20,11 @@ db.version(13).stores({
relays: '++url, name',
alerts: '++id, created_at',
messages: '++id, pubkey, recipient',
people: '++pubkey',
rooms: '++id, joined',
})
// A flag for hiding things that rely on people being loaded initially
export const ready = writable(false)
// Some things work better as observables than database tables
export const people = writable([])
// Bootstrap our people observable
db.table('people').toArray().then($p => {
people.set(createMap('pubkey', $p))
ready.set(true)
})
// Sync to a regular object so we have a synchronous interface
let $people = {}
people.subscribe($p => {
$people = $p
})
// Our synchronous interface
export const getPerson = (pubkey, fallback = false) =>
$people[pubkey] || (fallback ? {pubkey} : null)
export const updatePeople = async updates => {
// Sync to our in memory copy
people.update($people => ({...$people, ...updates}))
// Sync to our database
await db.table('people').bulkPut(Object.values(updates))
await database.people.bulkPut(updates)
}
// Hooks
@ -70,7 +43,7 @@ const processProfileEvents = async events => {
const updates = {}
for (const e of profileEvents) {
const person = getPerson(e.pubkey, true)
const person = database.getPersonWithFallback(e.pubkey)
updates[e.pubkey] = {
...person,
@ -92,8 +65,10 @@ const processProfileEvents = async events => {
},
2: () => {
if (e.created_at > (person.relays_updated_at || 0)) {
const {relays = []} = database.getPersonWithFallback(e.pubkey)
return {
relays: ($people[e.pubkey]?.relays || []).concat({url: e.content}),
relays: relays.concat({url: e.content}),
relays_updated_at: e.created_at,
}
}
@ -200,7 +175,7 @@ const processMessages = async events => {
const verifyNip05 = (pubkey, as) =>
nip05.queryProfile(as).then(result => {
if (result?.pubkey === pubkey) {
const person = getPerson(pubkey, true)
const person = database.getPersonWithFallback(pubkey)
updatePeople({[pubkey]: {...person, verified_as: as}})
}

238
src/agent/database.ts Normal file
View File

@ -0,0 +1,238 @@
import {is, prop, without} from 'ramda'
import {writable} from 'svelte/store'
import {switcherFn, ensurePlural, first} from 'hurdak/lib/hurdak'
import {defer, asyncIterableToArray} from 'src/util/misc'
// Types
type Message = {
topic: string
payload: object
}
type Table = {
name: string
subscribe: (subscription: (value: any) => void) => (() => void)
bulkPut: (data: object) => void
all: (where: object) => Promise<any>
get: (key: string) => any
}
// Plumbing
const worker = new Worker('/src/workers/database.js', {type: 'module'})
worker.addEventListener('error', e => console.error(e))
class Channel {
id: string
onMessage: (e: MessageEvent) => void
constructor({onMessage}) {
this.id = Math.random().toString().slice(2)
this.onMessage = e => onMessage(e.data as Message)
worker.addEventListener('message', this.onMessage)
}
close() {
worker.removeEventListener('message', this.onMessage)
}
send(topic, payload) {
worker.postMessage({channel: this.id, topic, payload})
}
}
const call = (topic, payload): Promise<Message> => {
return new Promise(resolve => {
const channel = new Channel({
onMessage: message => {
resolve(message)
channel.close()
},
})
channel.send(topic, payload)
})
}
const callLocalforage = async (storeName, method, ...args) => {
const message = await call('localforage.call', {storeName, method, args})
if (message.topic !== 'localforage.return') {
throw new Error(`callLocalforage received invalid response: ${message}`)
}
return message.payload
}
// Methods that proxy localforage
const getItem = (storeName, ...args) => callLocalforage(storeName, 'getItem', ...args)
const setItem = (storeName, ...args) => callLocalforage(storeName, 'setItem', ...args)
const removeItem = (storeName, ...args) => callLocalforage(storeName, 'removeItem', ...args)
const length = (storeName) => callLocalforage(storeName, 'length')
const clear = (storeName) => callLocalforage(storeName, 'clear')
const keys = (storeName) => callLocalforage(storeName, 'keys')
const iterate = (storeName, where = {}) => ({
[Symbol.asyncIterator]() {
let done = false
let promise = defer()
const messages = []
const channel = new Channel({
onMessage: m => switcherFn(m.topic, {
'localforage.item': () => {
promise.resolve()
messages.push(m.payload)
},
'localforage.iterationComplete': () => {
done = true
promise.resolve()
channel.close()
},
}),
})
channel.send('localforage.iterate', {storeName, where})
const next = async () => {
if (done) {
return {done}
}
const [value] = messages.splice(0, 1)
if (value) {
return {done, value}
} else {
promise = defer()
await promise
return next()
}
}
return {next}
}
})
// Local copy of data so we can provide a sync observable interface. The worker
// is just for storing data and processing expensive queries
const registry = {}
const defineTable = (name: string): Table => {
let p = Promise.resolve()
let listeners = []
let data = {}
const subscribe = f => {
listeners.push(f)
f(data)
return () => {
listeners = without([f], listeners)
}
}
const setAndNotify = newData => {
// Update our local copy
data = newData
// Notify subscribers
for (const f of listeners) {
f(data)
}
}
const bulkPut = newData => {
setAndNotify({...data, ...newData})
// Sync to storage, keeping updates in order
p = p.then(() => {
const updates = []
for (const [k, v] of Object.entries(newData)) {
updates.push(setItem(name, k, v))
}
return Promise.all(updates)
}) as Promise<void>
}
const all = (where = {}) => asyncIterableToArray(iterate(name, where), prop('v'))
const one = (where = {}) => first(all(where))
const get = k => data[k]
// Sync from storage initially
;(async () => {
const initialData = {}
for await (const {k, v} of iterate(name)) {
initialData[k] = v
}
setAndNotify(initialData)
})()
registry[name] = {name, subscribe, bulkPut, all, one, get}
return registry[name]
}
const people = defineTable('people')
// 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)
}
// Listen for changes
listener.subscribe(async name => {
if (names.includes(name)) {
store.set(await f(...tables))
}
})
return store
}
// Other utilities
const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
export default {
getItem, setItem, removeItem, length, clear, keys, iterate,
watch, getPersonWithFallback, people,
}

View File

@ -1,28 +1,31 @@
import type {Person} from 'src/util/types'
import type {Readable} from 'svelte/store'
import {last, uniqBy, prop} from 'ramda'
import {derived, get} from 'svelte/store'
import {Tags} from 'src/util/nostr'
import pool from 'src/agent/pool'
import keys from 'src/agent/keys'
import defaults from 'src/agent/defaults'
import {lq, db, people, ready, getPerson, processEvents} from 'src/agent/data'
import database from 'src/agent/database'
import {lq, db, processEvents} from 'src/agent/data'
Object.assign(window, {pool, db})
Object.assign(window, {pool, db, database})
export {pool, keys, lq, db, ready, people, getPerson}
export {pool, keys, lq, db, database}
export const user = derived(
[keys.pubkey, people],
[keys.pubkey, database.people as Readable<any>],
([pubkey, $people]) => {
if (!pubkey) {
return null
}
return $people[pubkey] || {pubkey}
return ($people[pubkey] || {pubkey})
}
)
) as Readable<Person>
export const getMuffle = () => {
const $user = get(user)
const $user = get(user) as Person
if (!$user?.muffle) {
return []
@ -34,16 +37,16 @@ export const getMuffle = () => {
}
export const getFollows = pubkey => {
const person = getPerson(pubkey)
const person = database.getPersonWithFallback(pubkey)
return Tags.wrap(person?.petnames || defaults.petnames).values().all()
return Tags.wrap(person.petnames || defaults.petnames).values().all()
}
export const getRelays = (pubkey?: string) => {
let relays = getPerson(pubkey)?.relays
let relays = database.getPersonWithFallback(pubkey).relays
if (!relays?.length) {
relays = getPerson(get(keys.pubkey))?.relays
relays = database.getPersonWithFallback(get(keys.pubkey)).relays
}
if (!relays?.length) {

View File

@ -183,7 +183,7 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
}
}
const request = (relays, filters, {threshold = 0.5} = {}): Promise<Record<string, unknown>[]> => {
const request = (relays, filters, {threshold = 0.1} = {}): Promise<Record<string, unknown>[]> => {
return new Promise(async resolve => {
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
threshold = relays.length * threshold

View File

@ -2,7 +2,7 @@ import {prop, pick, join, uniqBy, last} from 'ramda'
import {get} from 'svelte/store'
import {first} from "hurdak/lib/hurdak"
import {Tags, isRelay, roomAttrs, displayPerson} from 'src/util/nostr'
import {keys, publish, getRelays, getPerson} from 'src/agent'
import {keys, publish, getRelays, database} from 'src/agent'
const updateUser = (relays, updates) =>
publishEvent(relays, 0, {content: JSON.stringify(updates)})
@ -32,7 +32,7 @@ const createDirectMessage = (relays, pubkey, content) =>
const createNote = (relays, content, mentions = [], topics = []) => {
mentions = mentions.map(p => {
const {url} = first(getRelays(p))
const name = displayPerson(getPerson(p, true))
const name = displayPerson(database.getPersonWithFallback(p))
return ["p", p, url, name]
})

View File

@ -1,10 +1,11 @@
import type {Person} from 'src/util/types'
import {pluck, whereEq, sortBy, identity, when, assoc, reject} from 'ramda'
import {navigate} from 'svelte-routing'
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {get} from 'svelte/store'
import {renderContent} from 'src/util/html'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {user, people, getPerson, getRelays, keys} from 'src/agent'
import {user, database, getRelays, keys} from 'src/agent'
import defaults from 'src/agent/defaults'
import {toast, routes, modal, settings, logUsage} from 'src/app/ui'
import cmd from 'src/app/cmd'
@ -45,7 +46,7 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
}
export const addRelay = async url => {
const person = get(user)
const person = get(user) as Person
const modify = relays => relays.concat({url, write: '!'})
// Set to defaults to support anonymous usage
@ -63,7 +64,7 @@ export const addRelay = async url => {
}
export const removeRelay = async url => {
const person = get(user)
const person = get(user) as Person
const modify = relays => reject(whereEq({url}), relays)
// Set to defaults to support anonymous usage
@ -75,7 +76,7 @@ export const removeRelay = async url => {
}
export const setRelayWriteCondition = async (url, write) => {
const person = get(user)
const person = get(user) as Person
const modify = relays => relays.map(when(whereEq({url}), assoc('write', write)))
// Set to defaults to support anonymous usage
@ -88,10 +89,9 @@ export const setRelayWriteCondition = async (url, write) => {
export const renderNote = (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire
const $people = get(people)
const peopleByPubkey = createMap(
'pubkey',
Tags.from(note).type("p").values().all().map(k => $people[k]).filter(identity)
Tags.from(note).type("p").values().all().map(k => database.people.get(k)).filter(identity)
)
let content
@ -126,7 +126,7 @@ export const annotate = (note, context) => {
return {
...note, reactions,
person: getPerson(note.pubkey),
person: database.people.get(note.pubkey),
replies: sortBy(e => e.created_at, replies).map(r => annotate(r, context)),
}
}

View File

@ -2,12 +2,12 @@ import {uniqBy, prop, uniq, flatten, pluck, identity} from 'ramda'
import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak'
import {findReply, personKinds, Tags} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc'
import {load, getPerson, getFollows} from 'src/agent'
import {load, database, getFollows} from 'src/agent'
const getStalePubkeys = pubkeys => {
// If we're not reloading, only get pubkeys we don't already know about
return uniq(pubkeys).filter(pubkey => {
const p = getPerson(pubkey)
const p = database.people.get(pubkey)
return !p || p.updated_at < now() - timedelta(1, 'days')
})

View File

@ -8,7 +8,7 @@
import Badge from 'src/partials/Badge.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import {user, getPerson} from 'src/agent'
import {user, database} from 'src/agent'
import {renderNote} from 'src/app'
export let name
@ -32,7 +32,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 = getPerson(m.pubkey, true)
const person = database.getPersonWithFallback(m.pubkey)
const showPerson = person.pubkey !== getPath(['person', 'pubkey'], last(mx))
return mx.concat({...m, person, showPerson})

View File

@ -5,7 +5,7 @@
import {displayPerson} from "src/util/nostr"
import {fromParentOffset} from "src/util/html"
import Badge from "src/partials/Badge.svelte"
import {people} from "src/agent/data"
import {database} from "src/agent"
export let onSubmit
@ -14,10 +14,11 @@
let suggestions = []
let input = null
let prevContent = ''
let search = fuzzy(
Object.values($people).filter(prop('name')),
{keys: ["name", "pubkey"]}
)
let search
database.people.all({'name:!nil': null}).then(people => {
search = fuzzy(people, {keys: ["name", "pubkey"]})
})
const getText = () => {
const selection = document.getSelection()

View File

@ -12,7 +12,7 @@
</script>
{#if size === 'inherit'}
<div {...$$props} class={cx($$props.class, className)}>
<div {...$$props} class={cx($$props.class, className, "w-full")}>
<slot />
</div>
{/if}

View File

@ -5,7 +5,7 @@
import Badge from "src/partials/Badge.svelte"
import {formatTimestamp} from 'src/util/misc'
import {killEvent} from 'src/util/html'
import {getPerson} from 'src/agent'
import {database} from 'src/agent'
import {modal} from 'src/app'
export let note
@ -41,7 +41,7 @@
class="absolute top-0 mt-8 py-2 px-4 rounded border border-solid border-medium
bg-dark grid grid-cols-3 gap-y-2 gap-x-4 z-20">
{#each uniq(note.likedBy) as pubkey}
<Badge person={getPerson(pubkey)} />
<Badge person={database.getPersonWithFallback(pubkey)} />
{/each}
</button>
{/if}

View File

@ -14,7 +14,7 @@
import Badge from "src/partials/Badge.svelte"
import Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte"
import {user, people, getPerson, getRelays, getEventRelays} from 'src/agent'
import {user, database, getRelays, getEventRelays} from 'src/agent'
import cmd from 'src/app/cmd'
export let note
@ -34,10 +34,9 @@
const showEntire = anchorId === note.id
const interactive = !anchorId || !showEntire
const relays = getEventRelays(note)
const person = database.watch('people', p => p.get(note.pubkey) || {pubkey: note.pubkey})
let likes, flags, like, flag, person
$: person = $people[note.pubkey] || {pubkey: note.pubkey}
let likes, flags, like, flag
$: {
likes = note.reactions.filter(n => isLike(n.content))
@ -138,7 +137,7 @@
<Card on:click={onClick} {interactive} {invertColors}>
<div class="flex gap-4 items-center justify-between">
<Badge person={person} />
<Badge person={$person} />
<Anchor
href={"/" + nip19.neventEncode({id: note.id, relays: pluck('url', relays.slice(0, 5))})}
class="text-sm text-light"
@ -205,7 +204,7 @@
{#each replyMentions as p}
<div class="inline-block py-1 px-2 mr-1 mb-2 rounded-full border border-solid border-light">
<button class="fa fa-times cursor-pointer" on:click|stopPropagation={() => removeMention(p)} />
{displayPerson(getPerson(p, true))}
{displayPerson(database.getPersonWithFallback(p))}
</div>
{/each}
<div class="-mt-2" />

View File

@ -4,7 +4,7 @@
import {nip19} from 'nostr-tools'
import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc"
import {getRelays, user, lq, getPerson, listen, db} from 'src/agent'
import {getRelays, user, lq, database, listen, db} from 'src/agent'
import {modal, messages} from 'src/app'
import loaders from 'src/app/loaders'
import Room from "src/partials/Room.svelte"
@ -26,7 +26,7 @@
await loaders.loadPeople(getRelays(), pubkeys)
return sortBy(k => -(mostRecentByPubkey[k] || 0), pubkeys)
.map(k => ({type: 'npub', id: k, ...getPerson(k, true)}))
.map(k => ({type: 'npub', id: k, ...database.getPersonWithFallback(k)}))
.concat(rooms.map(room => ({type: 'note', ...room})))
})

View File

@ -4,7 +4,7 @@
import {personKinds} from 'src/util/nostr'
import {batch, now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte'
import {lq, getRelays, user, db, listen, keys} from 'src/agent'
import {database, getRelays, user, db, listen, keys} from 'src/agent'
import {messages} from 'src/app'
import {routes} from 'src/app/ui'
import cmd from 'src/app/cmd'
@ -13,7 +13,7 @@
let crypt = keys.getCrypt()
let {data: pubkey} = nip19.decode(entity) as {data: string}
let person = lq(() => db.table('people').get(pubkey))
let person = database.watch('people', p => p.get(pubkey))
messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()}))

View File

@ -14,7 +14,7 @@
import Notes from "src/views/person/Notes.svelte"
import Likes from "src/views/person/Likes.svelte"
import Network from "src/views/person/Network.svelte"
import {getPerson, getRelays, listen, user, keys} from "src/agent"
import {database, getRelays, listen, user, keys} from "src/agent"
import {modal} from "src/app"
import loaders from "src/app/loaders"
import {routes} from "src/app/ui"
@ -29,7 +29,7 @@
let following = false
let followers = new Set()
let followersCount = 0
let person = getPerson(pubkey, true)
let person = database.getPersonWithFallback(pubkey)
let loading = true
$: following = find(t => t[1] === pubkey, $user?.petnames || [])
@ -37,7 +37,7 @@
onMount(async () => {
// Refresh our person if needed
loaders.loadPeople(relays || getRelays(pubkey), [pubkey]).then(() => {
person = getPerson(pubkey, true)
person = database.getPersonWithFallback(pubkey)
loading = false
})

View File

@ -9,7 +9,7 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import {lq, pool, db, user, ready} from "src/agent"
import {lq, pool, db, user} from "src/agent"
import {modal, settings} from "src/app"
import defaults from "src/agent/defaults"
@ -41,20 +41,18 @@
}
onMount(() => {
// Attempt to connect so we can show status
relays.forEach(relay => pool.connect(relay.url))
return poll(1000, async () => {
const userRelays = $user?.relays || []
const urls = pluck('url', userRelays)
const relaysByUrl = createMap(
'url',
await db.table('relays').where('url').anyOf(urls).toArray()
)
return poll(300, async () => {
if ($ready) {
const userRelays = $user?.relays || []
const urls = pluck('url', userRelays)
const relaysByUrl = createMap(
'url',
await db.table('relays').where('url').anyOf(urls).toArray()
)
relays = userRelays.map(relay => ({...relaysByUrl[relay.url], ...relay}))
relays = userRelays.map(relay => ({...relaysByUrl[relay.url], ...relay}))
}
// Attempt to connect so we can show status
relays.forEach(relay => pool.connect(relay.url))
status = Object.fromEntries(
pool.getConnections().map(({url, status, stats}) => {

View File

@ -1,5 +1,5 @@
import {debounce} from 'throttle-debounce'
import {pluck, sortBy} from "ramda"
import {pluck, identity, sortBy} from "ramda"
import Fuse from "fuse.js/dist/fuse.min.js"
import {writable} from 'svelte/store'
import {isObject} from 'hurdak/lib/hurdak'
@ -179,3 +179,22 @@ export const batch = (t, f) => {
cb()
}
}
export const defer = () => {
let resolve, reject
const p = new Promise((resolve_, reject_) => {
resolve = resolve_
reject = reject_
})
return Object.assign(p, {resolve, reject})
}
export const asyncIterableToArray = async (it, f = identity) => {
const result = []
for await (const x of it) {
result.push(f(x))
}
return result
}

11
src/util/types.ts Normal file
View File

@ -0,0 +1,11 @@
export type Relay = {
url: string
}
export type Person = {
pubkey: string
picture?: string
relays?: Array<Relay>
muffle?: Array<Array<string>>
petnames?: Array<Array<string>>
}

View File

@ -1,23 +1,21 @@
<script>
import {prop} from 'ramda'
import {ellipsize} from 'hurdak/lib/hurdak'
import {fuzzy} from "src/util/misc"
import {renderContent} from "src/util/html"
import {displayPerson} from "src/util/nostr"
import {user, people} from 'src/agent'
import {user, database} from 'src/agent'
import {routes} from "src/app/ui"
export let q
let search
$: search = fuzzy(
Object.values($people).filter(prop('name')),
{keys: ["name", "about", "pubkey"]}
)
database.people.all({'name:!nil': null}).then(people => {
search = fuzzy(people, {keys: ["name", "about", "pubkey"]})
})
</script>
{#each search(q).slice(0, 30) as p (p.pubkey)}
{#each (search ? search(q) : []).slice(0, 30) as p (p.pubkey)}
{#if p.pubkey !== $user.pubkey}
<a href={routes.person(p.pubkey)} class="flex gap-4">
<div

73
src/workers/database.js Normal file
View File

@ -0,0 +1,73 @@
import lf from 'localforage'
import {complement, equals, isNil, pipe, prop, identity, allPass} from 'ramda'
import {switcherFn} from 'hurdak/lib/hurdak'
const stores = {}
const getStore = storeName => {
if (!stores[storeName]) {
stores[storeName] = lf.createInstance({name: 'coracle', storeName})
}
return stores[storeName]
}
addEventListener('message', async ({data: {topic, payload, channel}}) => {
const reply = (topic, payload) => postMessage({channel, topic, payload})
switcherFn(topic, {
'localforage.call': async () => {
const {storeName, method, args} = payload
const result = await getStore(storeName)[method](...args)
reply('localforage.return', result)
},
'localforage.iterate': async () => {
const {storeName, where} = payload
const matchesFilter = allPass(
Object.entries(where)
.map(([key, value]) => {
let [field, operator = 'eq'] = key.split(':')
let test, modifier = identity
if (operator.startsWith('!')) {
operator = operator.slice(1)
modifier = complement
}
if (operator === 'eq') {
test = equals(value)
} if (operator === 'nil') {
test = isNil
} else {
throw new Error(`Invalid operator ${operator}`)
}
return pipe(prop(field), modifier(test))
})
)
getStore(storeName).iterate(
(v, k, i) => {
if (matchesFilter(v)) {
reply('localforage.item', {v, k, i})
}
},
() => {
reply('localforage.iterationComplete')
},
)
},
default: () => {
throw new Error(`invalid topic: ${topic}`)
},
})
})
addEventListener('error', event => {
console.error(event.error)
})
addEventListener('unhandledrejection', event => {
console.error(event)
})