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