Files
nostrlib/sdk/list.go

156 lines
4.2 KiB
Go

package sdk
import (
"context"
"slices"
"sync"
"sync/atomic"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/sdk/cache"
)
type GenericList[V comparable, I TagItemWithValue[V]] struct {
PubKey nostr.PubKey `json:"-"` // must always be set otherwise things will break
Event *nostr.Event `json:"-"` // may be empty if a contact list event wasn't found
Items []I
}
type TagItemWithValue[V comparable] interface {
Value() V
}
var (
genericListMutexes = [60]sync.Mutex{}
valueWasJustCached = [60]atomic.Bool{}
)
func fetchGenericList[V comparable, I TagItemWithValue[V]](
sys *System,
ctx context.Context,
pubkey nostr.PubKey,
actualKind nostr.Kind,
replaceableIndex replaceableIndex,
parseTag func(nostr.Tag) (I, bool),
cache cache.Cache32[GenericList[V, I]],
) (fl GenericList[V, I], fromInternal bool) {
// we have 60 mutexes, so we can load up to 60 lists at the same time, but if we do the same exact
// call that will do it only once, the subsequent ones will wait for a result to be cached
// and then return it from cache -- 13 is an arbitrary index for the pubkey
n := pubkey[7]
lockIdx := (nostr.Kind(n) + actualKind) % 60
genericListMutexes[lockIdx].Lock()
if valueWasJustCached[lockIdx].CompareAndSwap(true, false) {
// this ensures the cache has had time to commit the values
// so we don't repeat a fetch immediately after the other
time.Sleep(time.Millisecond * 10)
}
genericListMutexes[lockIdx].Unlock()
if v, ok := cache.Get(pubkey); ok {
return v, true
}
v := GenericList[V, I]{PubKey: pubkey}
for evt := range sys.Store.QueryEvents(nostr.Filter{
Kinds: []nostr.Kind{actualKind},
Authors: []nostr.PubKey{pubkey},
}, 1) {
// ok, we found something locally
items := parseItemsFromEventTags(evt, parseTag)
v.Event = &evt
v.Items = items
// but if we haven't tried fetching from the network recently we should do it
lastFetchKey := makeLastFetchKey(actualKind, pubkey)
lastFetchData, _ := sys.KVStore.Get(lastFetchKey)
if lastFetchData == nil || nostr.Now()-decodeTimestamp(lastFetchData) > getLocalStoreRefreshDaysForKind(actualKind)*24*60*60 {
newV := tryFetchListFromNetwork(ctx, sys, pubkey, replaceableIndex, parseTag)
if newV != nil && newV.Event.CreatedAt > v.Event.CreatedAt {
v = *newV
sys.Store.ReplaceEvent(*v.Event)
}
// register this even if we didn't find anything because we tried
// (and we still have the previous event in our local store)
sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now()))
}
// and finally save this to cache
cache.SetWithTTL(pubkey, v, time.Hour*6)
valueWasJustCached[lockIdx].Store(true)
return v, true
}
if newV := tryFetchListFromNetwork(ctx, sys, pubkey, replaceableIndex, parseTag); newV != nil {
v = *newV
// we'll only save this if we got something which means we found at least one event
lastFetchKey := makeLastFetchKey(actualKind, pubkey)
sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now()))
sys.Store.SaveEvent(*v.Event)
}
// save cache even if we didn't get anything
cache.SetWithTTL(pubkey, v, time.Hour*6)
valueWasJustCached[lockIdx].Store(true)
return v, false
}
func tryFetchListFromNetwork[V comparable, I TagItemWithValue[V]](
ctx context.Context,
sys *System,
pubkey nostr.PubKey,
replaceableIndex replaceableIndex,
parseTag func(nostr.Tag) (I, bool),
) *GenericList[V, I] {
evt, err := sys.replaceableLoaders[replaceableIndex].Load(ctx, pubkey)
if err != nil {
return nil
}
v := &GenericList[V, I]{
PubKey: pubkey,
Event: &evt,
Items: parseItemsFromEventTags(evt, parseTag),
}
sys.Publisher.Publish(ctx, evt)
return v
}
func parseItemsFromEventTags[V comparable, I TagItemWithValue[V]](
evt nostr.Event,
parseTag func(nostr.Tag) (I, bool),
) []I {
result := make([]I, 0, len(evt.Tags))
for _, tag := range evt.Tags {
item, ok := parseTag(tag)
if ok {
// check if this already exists before adding
if slices.IndexFunc(result, func(i I) bool { return i.Value() == item.Value() }) == -1 {
result = append(result, item)
}
}
}
return result
}
func getLocalStoreRefreshDaysForKind(kind nostr.Kind) nostr.Timestamp {
switch kind {
case 0:
return 7
case 3:
return 1
default:
return 3
}
}