package Data::RadixTree::Shared;
use strict;
use warnings;
our $VERSION = '0.01';
require XSLoader;
XSLoader::load('Data::RadixTree::Shared', $VERSION);

*get      = \&lookup;
*contains = \&exists;
*remove   = \&delete;
*size     = \&count;

1;
__END__

=encoding utf-8

=head1 NAME

Data::RadixTree::Shared - shared-memory compressed radix tree (prefix tree) for Linux

=head1 SYNOPSIS

    use Data::RadixTree::Shared;

    # anonymous shared mapping: 4096-node pool, 64 KiB label arena
    my $t = Data::RadixTree::Shared->new(undef, 4096, 65536);

    $t->insert("foo", 1);          # 1 (newly inserted)
    $t->insert("foobar", 2);       # 1
    $t->insert("foo", 9);          # 0 (key existed -> value updated)

    $t->lookup("foo");             # 9
    $t->get("foobar");             # 2  ('get' is an alias for lookup)
    $t->exists("foo");             # true
    $t->contains("fo");            # false ('contains' is an alias for exists)

    # longest-prefix match: the longest stored key that is a prefix of the query
    $t->insert("10",       100);
    $t->insert("10.0",     200);
    $t->insert("10.0.0",   300);
    $t->longest_prefix("10.0.0.5");   # 300 (value of "10.0.0")
    $t->longest_prefix("9");          # undef (no stored key is a prefix)

    $t->delete("foo");             # 1 (removed); 'remove' is an alias
    $t->count;                     # number of stored keys ('size' is an alias)
    $t->clear;                     # empty the tree

    # share across processes via a backing file
    my $shared = Data::RadixTree::Shared->new("/tmp/routes.bin", 1 << 16, 1 << 20);

=head1 DESCRIPTION

A compressed radix tree (a radix-256, PATRICIA-style trie) in shared memory. It
maps arbitrary B<byte-string keys> to B<unsigned-integer values>. Edge
compression collapses chains of single-child nodes into one labelled edge, so
insert and lookup are B<O(key length)> regardless of how many keys are stored,
and the structure stays compact.

Beyond exact lookup, a radix tree answers B<longest-prefix> queries
efficiently: given a query string, C<longest_prefix> returns the value of the
B<longest stored key that is a prefix of the query>. This is exactly what
routing tables, dispatch tables and autocomplete back-ends need (e.g. find the
most specific CIDR-like prefix that covers an address).

Keys are B<raw bytes>: a key containing wide characters croaks (encode to bytes
first, e.g. C<utf8::encode> or C<Encode::encode>). Values are unsigned integers
(a Perl C<UV>, stored as 64-bit). Because the node pool and label arena live in
a shared mapping, B<several processes share one tree>: any process that opens
the same backing file, inherits the anonymous mapping across C<fork>, or reopens
a passed memfd, sees the others' keys. A write-preferring futex rwlock with
dead-process recovery guards every mutation, so concurrent inserts and deletes
serialize cleanly.

C<lookup>, C<get>, C<exists>, C<contains> and C<longest_prefix> are pure reads
(no path compression) and take the B<read lock>, so many processes can query in
parallel. C<insert>, C<delete> and C<clear> take the B<write lock>.

B<v1 NOTE -- delete is lazy:> C<delete> unmarks the key (so C<lookup> and
C<exists> no longer find it and C<count> drops) but does B<not> reclaim the
node-pool slots or the label-arena bytes the key occupied. Size the capacities
for your working set, or call C<clear> to reset the whole tree and reclaim all
space at once.

B<Capacity is fixed at creation.> The node pool holds C<node_capacity> nodes and
the label arena holds C<arena_capacity> bytes; C<insert> croaks (after releasing
the lock) when either would overflow. A single insert consumes at most two nodes
and at most C<length(key)> arena bytes, so size both with headroom for your key
set. B<Linux-only.> Requires 64-bit Perl.

=head1 METHODS

=head2 Constructors

    my $t = Data::RadixTree::Shared->new($path, $node_capacity, $arena_capacity);
    my $t = Data::RadixTree::Shared->new(undef, $node_capacity, $arena_capacity); # anonymous
    my $t = Data::RadixTree::Shared->new_memfd($name, $node_capacity, $arena_capacity);
    my $t = Data::RadixTree::Shared->new_from_fd($fd);

C<$path> is the backing file (C<undef> or omitted for an anonymous mapping).
C<$node_capacity> (default 4096) is the number of tree nodes the pool holds; it
must be C<E<gt>= 2> (one slot is the reserved NIL sentinel, one is the root) and
C<E<lt>= 2**24>. C<$arena_capacity> (default 65536) is the number of bytes the
label arena holds; it must be C<E<gt>= 1> and C<E<lt>= 0xF0000000> (~3.75 GiB).
C<new> and C<new_memfd> croak if either capacity is out of range. A freshly created tree is empty
(C<count == 0>).

When reopening an existing file or memfd, the B<stored geometry wins> and the
existing keys are preserved; the capacities you pass to C<new> on a reopen are
only used when the file is brand new. C<new_memfd> creates a Linux memfd
(transferable via its C<memfd> descriptor); C<new_from_fd> reopens one in
another process.

=head2 Insert, lookup, prefix queries

    my $new   = $t->insert($key, $value);   # 1 if newly inserted, 0 if updated
    my $value = $t->lookup($key);           # the stored value, or undef
    my $value = $t->get($key);              # alias for lookup
    my $bool  = $t->exists($key);           # true if $key is stored
    my $bool  = $t->contains($key);         # alias for exists
    my $value = $t->longest_prefix($query); # value of the longest stored prefix, or undef

C<insert> stores C<$value> (default 1) under C<$key>, returning B<1> if the key
was B<new> or B<0> if it B<already existed> (the value is updated either way).
It croaks if the node pool or label arena is exhausted -- the croak happens
B<after> the write lock is released, and the tree is left consistent (every
already-inserted key is still present and queryable).

Because the capacity check runs before the key is looked up, in a B<full> tree
C<insert> croaks even for an update of an existing key (which would need no new
space); size the capacities with headroom, or call C<clear>.

C<lookup> (aliased C<get>) returns the value stored under C<$key>, or C<undef>
if the key is not present. C<exists> (aliased C<contains>) returns a boolean.

C<longest_prefix> returns the B<value> of the longest stored key that is a
prefix of C<$query>, or C<undef> if no stored key is a prefix of it. (The empty
string, if stored, is a prefix of every query.) It returns the matched key's
B<value>, not the key itself; recovering the matched key string is deferred to a
future version.

Keys are byte strings; a key (or query) containing wide characters croaks
B<before> any lock is taken. Values are unsigned integers stored as 64-bit.

=head2 Delete and clear

    my $removed = $t->delete($key);   # 1 if removed, 0 if absent
    my $removed = $t->remove($key);   # alias for delete
    $t->clear;                        # remove every key

C<delete> (aliased C<remove>) removes C<$key>, returning B<1> if it was present
or B<0> if it was absent. As noted above, delete is B<lazy> in v1: it unmarks
the key and decrements C<count>, but does not reclaim node or arena space.
Re-inserting the same key reuses its existing path at no extra cost; inserting
many distinct keys after deletes still consumes fresh capacity. C<clear> empties
the tree and reclaims B<all> node and arena space at once.

=head2 Size and stats

    my $n = $t->count;   # number of stored keys
    my $n = $t->size;    # alias for count

C<count> (aliased C<size>) is the current number of distinct stored keys. See
L</STATS> for the full C<stats> hashref.

=head2 Lifecycle

    $t->path; $t->memfd; $t->sync; $t->unlink;   # or Class->unlink($path)

C<sync> flushes the mapping to its backing store (a no-op for anonymous and
memfd trees, which have none); C<unlink> removes the backing file (also callable
as C<< Class->unlink($path) >>); C<path> returns the backing path (C<undef> for
anonymous, memfd, or fd-reopened trees) and C<memfd> the backing descriptor --
the memfd of a C<new_memfd> tree or the dup'd fd of a C<new_from_fd> tree, and
-1 for file-backed or anonymous trees.

=head1 STATS

C<stats()> returns a hashref describing the tree:

=over 4

=item * C<keys> -- the number of distinct stored keys (same as C<count>).

=item * C<nodes_used> -- high-water number of node-pool slots ever allocated
(includes the NIL sentinel and the root). With lazy delete this never
decreases except on C<clear>.

=item * C<nodes_capacity> -- the fixed node-pool capacity.

=item * C<arena_used> -- bytes of label arena consumed (never decreases except
on C<clear>).

=item * C<arena_capacity> -- the fixed label-arena capacity in bytes.

=item * C<ops> -- running count of operations that took the write lock
(C<insert>, C<delete>, C<clear>).

=item * C<mmap_size> -- bytes of the shared mapping.

=back

=head1 SHARING ACROSS PROCESSES

The tree lives in a shared mapping, shared the same three ways as the rest of
the family: a B<backing file> (every process calls C<< new($path, ...) >> on the
same path), an B<anonymous mapping inherited across C<fork>>, or a B<memfd>
whose descriptor is passed to an unrelated process (over a UNIX socket via
C<SCM_RIGHTS>, or via C</proc/$pid/fd/$n>) and reopened with
C<< new_from_fd($fd) >>. Because the mapping is shared, B<every process inserts
into and queries the same tree>. All mutation is serialized by the write lock,
so the final contents are independent of how the processes interleave.

    # parent and children share one radix tree with no coordination
    my $t = Data::RadixTree::Shared->new(undef, 1 << 16, 1 << 20);   # before fork
    unless (fork) { $t->insert("child-$_", $_) for 1 .. 1000; exit }
    wait;
    print $t->count, "\n";   # reflects the child's inserts

=head1 SECURITY

The mmap region is writable by all processes that open it, and a reopened file
is trusted: validation checks the header geometry but cannot vet every internal
node index against a maliciously crafted file. A hostile file could direct a
walk to an out-of-range node index. Do B<not> share backing files with, or
reopen files from, untrusted processes.

=head1 CRASH SAFETY

Mutation is guarded by a futex-based write-preferring rwlock with PID-encoded
ownership; if a holder dies, the next contender detects the dead owner and
recovers. Because an C<insert> performs all of its node/arena allocations under
the lock after a capacity pre-check, a crash leaves the tree consistent up to
the last completed C<insert> or C<delete>. B<Limitation>: PID reuse is not
detected (very unlikely in practice).

=head1 SEE ALSO

L<Data::DisjointSet::Shared>, L<Data::Intern::Shared>,
L<Data::SortedSet::Shared>, L<Data::SpatialHash::Shared>,
L<Data::Histogram::Shared>, L<Data::CountMinSketch::Shared>,
L<Data::HyperLogLog::Shared>, L<Data::BloomFilter::Shared>, and the rest of the
C<Data::*::Shared> family.

=head1 AUTHOR

vividsnow

=head1 LICENSE

This is free software; you can redistribute it and/or modify it under the same
terms as Perl itself.

=cut
