NAME
Net::Nostr::RelayStore - Indexed in-memory event storage for Nostr relays
SYNOPSIS
use Net::Nostr::RelayStore;
my $store = Net::Nostr::RelayStore->new(max_events => 10000);
$store->store($event); # returns 1 (new) or 0 (duplicate)
my $event = $store->get_by_id($id); # or undef
my $old = $store->find_replaceable($pk, $k); # or undef
my $addr = $store->find_addressable($pk, $k, $d); # or undef
$store->delete_by_id($id); # returns removed event or undef
$store->delete_matching($pk, \@ids, \@addrs, $ts); # NIP-09
my $results = $store->query(\@filters); # sorted, deduped, limit-aware
my $count = $store->count(\@filters); # deduped count
my $all = $store->all_events; # snapshot copy, sorted
my $n = $store->event_count;
$store->clear;
DESCRIPTION
Provides indexed in-memory event storage for Net::Nostr::Relay. Events are indexed by id, pubkey, kind, pubkey+kind (replaceable), pubkey+kind+d_tag (addressable), and tag values. Queries use the narrowest applicable index as the candidate set, then post-filter with "matches" in Net::Nostr::Filter.
This is the default storage backend. Third-party backends (SQLite, LMDB, etc.) can implement the same method interface and be passed to Net::Nostr::Relay via the store constructor option.
CONSTRUCTOR
new
my $store = Net::Nostr::RelayStore->new;
my $store = Net::Nostr::RelayStore->new(max_events => 5000);
Strict constructor. Creates a new empty store. All arguments are optional. Croaks on unknown arguments or invalid max_events values.
- max_events
-
Maximum number of events to retain. When exceeded, the oldest event (by
created_at) is evicted. Must be a positive integer. Defaults to unlimited.
METHODS
store
my $ok = $store->store($event);
Stores an event. Returns 1 on success, 0 if the event is a duplicate (same id already stored). Updates all indexes. If max_events is set and the store exceeds capacity after insertion, the oldest event is evicted.
For replaceable events (kind 0, 3, 10000-19999), the find_replaceable index always points to the newest event by created_at, with lowest id as tiebreak. Storing an older replaceable event adds it to the store but does not overwrite the index entry. The same applies to addressable events (kind 30000-39999) via find_addressable.
$store->store($event); # 1
$store->store($event); # 0 (duplicate)
get_by_id
my $event = $store->get_by_id($event_id);
Returns the event with the given id, or undef if not found.
find_replaceable
my $event = $store->find_replaceable($pubkey, $kind);
Returns the newest stored replaceable event for the given pubkey and kind, or undef if none exists. When the store holds multiple versions (e.g. an older event not yet cleaned up by the relay), this always returns the one with the highest created_at (lowest id as tiebreak). Replaceable kinds are 0, 3, and 10000-19999.
find_addressable
my $event = $store->find_addressable($pubkey, $kind, $d_tag);
Returns the newest stored addressable event for the given pubkey, kind, and d tag, or undef if none exists. When the store holds multiple versions, this always returns the one with the highest created_at (lowest id as tiebreak). Addressable kinds are 30000-39999.
delete_by_id
my $removed = $store->delete_by_id($event_id);
Removes the event with the given id from all indexes. Returns the removed event, or undef if not found. If the removed event was the current entry in the replaceable or addressable index, the next best candidate (by created_at DESC, id ASC) is promoted. If no candidates remain, the index entry is cleared.
delete_matching
my $count = $store->delete_matching($pubkey, \@ids, \@addresses, $before_ts);
NIP-09 bulk deletion. Deletes events owned by $pubkey by event id or by coordinate. Addresses may be addressable ("kind:pubkey:d_tag") or replaceable ("kind:pubkey:" with empty d_tag). Both id-based and address-based deletions skip events belonging to a different pubkey. Address-based deletion additionally only removes events with created_at <= $before_ts. Kind 5 (deletion) events are never deleted. Returns the number of events deleted.
$store->delete_matching($pk, [$event_id], [], 9999);
$store->delete_matching($pk, [], ["30023:$pk:slug"], $deletion_ts);
$store->delete_matching($pk, [], ["10000:$pk:"], $deletion_ts); # replaceable
query
my $events = $store->query(\@filters);
Returns an arrayref of events matching any of the given filters (OR across filters, AND within each filter). Results are sorted by created_at DESC then id ASC. Expired events (NIP-40) are excluded. Events matching multiple filters are deduplicated. Each filter's limit is respected independently. A limit of 0 returns no events for that filter.
my $results = $store->query([
Net::Nostr::Filter->new(kinds => [1], limit => 10),
Net::Nostr::Filter->new(authors => [$pk]),
]);
count
my $n = $store->count(\@filters);
Like "query" but returns only the count. Deduplicates across filters and skips expired events.
all_events
my $events = $store->all_events;
Returns a snapshot (array copy) of all stored events, sorted by created_at DESC then id ASC. Mutating the returned arrayref does not affect the store. Unlike "query", this includes expired events.
event_count
my $n = $store->event_count;
Returns the number of events currently stored.
max_events
my $max = $store->max_events; # positive integer or undef
Returns the configured maximum event capacity, or undef if unlimited.
clear
$store->clear;
Removes all events and resets all indexes.
PLUGGABLE BACKENDS
Third-party storage backends should implement the same public method interface: store, get_by_id, find_replaceable, find_addressable, delete_by_id, delete_matching, query, count, all_events, event_count, and clear. No base class or role is required -- the interface is duck-typed.
SEE ALSO
Net::Nostr::Relay, Net::Nostr::Filter, Net::Nostr::Event, NIP-01