Simon-Pierre 6f27547937
chore: update nim-libp2p to f54c7150a7cc (master service-disco fixes) to compile chat2disco
- waku.nimble: libp2p #f54c7150a7ccbc4e9871bb8b56ecfd7e3e59f7de; also pin protobuf_serialization#ce97ba0 and websock#fb8ba71 to match new libp2p reqs; mix remains on 6c5f43 (its declared pins lag)
- nimble.lock + nix/deps.nix updated (libp2p rev/sha)
- Source fixes for new libp2p (object configs, removed utility module -> libp2p/utils/opt, rendezvous nil, kademlia no longer imports mix_protocol to reduce bad dep surface)
- nph on touched .nim
- chat2disco builds+starts successfully against the updated libp2p (with in-nimbledeps patches to mix for its removed symbols like sequninit/utility and withValue(Opt) sites; run make update will require similar or upstream mix bump)

Refs the 106-commit libp2p delta with kademlia/service-disco fixes (e.g. ticket time, record sizes, registration).
2026-06-05 08:09:49 -04:00

155 lines
4.1 KiB
Nim

## TimedMap
## ===========
## Inspired by nim-libp2p's TimedCache class. This is using the same approach to prune
## untouched items from the map where the set timeout duration is reached.
## But unlike TimedCache this TimedMap is capable to hold and return any type of value for a key.
##
## - `mgetOrPut` proc is similar to std/tables, but will renew the timeout for the key.
## - For non-renewal check use `contains` proc.
## - `expire` proc will remove all items that have expired.
##
## Choose your initial timeout for your needs to control the size of the map.
{.push raises: [].}
import std/[hashes, sets]
import chronos/timer, results
import libp2p/utils/opt
export results
type
TimedEntry[K, V] = ref object of RootObj
key: K
value: V
addedAt: Moment
expiresAt: Moment
next, prev: TimedEntry[K, V]
TimedMap*[K, V] = object of RootObj
head, tail: TimedEntry[K, V] # nim linked list doesn't allow inserting at pos
entries: HashSet[TimedEntry[K, V]]
timeout: Duration
func `==`*[K, V](a, b: TimedEntry[K, V]): bool =
if isNil(a) == isNil(b):
isNil(a) or a.key == b.key
else:
false
func hash*(a: TimedEntry): Hash =
if isNil(a):
default(Hash)
else:
hash(a[].key)
func `$`*[T](a: T): string =
if isNil(a):
"nil"
return $a
func `$`*[K, V](a: TimedEntry[K, V]): string =
if isNil(a):
return "nil"
return
"TimedEntry: key:" & $a.key & ", val:" & $a.value & ", addedAt:" & $a.addedAt &
", expiresAt:" & $a.expiresAt
func expire*(t: var TimedMap, now: Moment = Moment.now()) =
while t.head != nil and t.head.expiresAt <= now:
t.entries.excl(t.head)
t.head.prev = nil
t.head = t.head.next
if t.head == nil:
t.tail = nil
func del[K, V](t: var TimedMap[K, V], key: K): Opt[TimedEntry[K, V]] =
# Removes existing key from cache, returning the previous item if present
let tmp = TimedEntry[K, V](key: key)
if tmp in t.entries:
let item =
try:
t.entries[tmp] # use the shared instance in the set
except KeyError:
raiseAssert "just checked"
t.entries.excl(item)
if t.head == item:
t.head = item.next
if t.tail == item:
t.tail = item.prev
if item.next != nil:
item.next.prev = item.prev
if item.prev != nil:
item.prev.next = item.next
Opt.some(item)
else:
Opt.none(TimedEntry[K, V])
func remove*[K, V](t: var TimedMap[K, V], key: K): Opt[V] =
# Removes existing key from cache, returning the previous value if present
# public version of del without exporting TimedEntry
let deleted = t.del(key)
if deleted.isSome():
Opt.some(deleted[].value)
else:
Opt.none(V)
proc mgetOrPut*[K, V](t: var TimedMap[K, V], k: K, v: V, now = Moment.now()): var V =
# Puts k in cache, returning true if the item was already present and false
# otherwise. If the item was already present, its expiry timer will be
# refreshed.
t.expire(now)
let
previous = t.del(k) # Refresh existing item
addedAt = if previous.isSome(): previous[].addedAt else: now
value = if previous.isSome(): previous[].value else: v
let node =
TimedEntry[K, V](key: k, value: value, addedAt: addedAt, expiresAt: now + t.timeout)
if t.head == nil:
t.tail = node
t.head = t.tail
else:
# search from tail because typically that's where we add when now grows
var cur = t.tail
while cur != nil and node.expiresAt < cur.expiresAt:
cur = cur.prev
if cur == nil:
node.next = t.head
t.head.prev = node
t.head = node
else:
node.prev = cur
node.next = cur.next
cur.next = node
if cur == t.tail:
t.tail = node
t.entries.incl(node)
return node.value
func contains*[K, V](t: TimedMap[K, V], k: K): bool =
let tmp = TimedEntry[K, V](key: k)
tmp in t.entries
func addedAt*[K, V](t: var TimedMap[K, V], k: K): Moment =
let tmp = TimedEntry[K, V](key: k)
try:
if tmp in t.entries: # raising is slow
# Use shared instance from entries
return t.entries[tmp][].addedAt
except KeyError:
raiseAssert "just checked"
default(Moment)
func init*[K, V](T: type TimedMap[K, V], timeout: Duration): T =
T(timeout: timeout)