Fix get_many_to_many pagination with composite cursor
The previous cursor was a plain subject string, which broke pagination
when multiple records shared the same secondary subject. Depending on
the comparison used in skip_while, this caused either duplicates (items
returned on both pages) or skipped items (items returned on neither
page) - these edge cases are now covered by the added collection of new
test.
The root cause is that a subject-only cursor cannot uniquely identify
a position in the result set when multiple (RecordId, subject) pairs
share the same subject value. Five issues were fixed:
1. Composite sort: items are now sorted by (subject, RecordId) instead
of subject alone, establishing a deterministic total order.
2. Composite cursor: the cursor is now a base64-encoded "did|rkey|subject"
string. Shared encode/decode helpers in storage/mod.rs ensure both
MemStorage and RocksDB produce and consume the same format. DID and
rkey are placed first in the encoding so that splitn(3, '|') correctly
handles subjects that may contain the delimiter.
3. Correct skip direction: skip_while now uses <= (skip items at or
before the cursor), fixing the comparison that previously caused
items to be re-included or over-skipped.
4. Post-filter cursor computation: the next cursor is now derived from
the last item after skip+take, not from the unfiltered list.
5. Fetch N+1: take limit+1 items, only emit a cursor when more than
limit items exist, then truncate. This avoids emitting a false cursor
that leads to an empty final page when items exactly equals limit.
authored by
seoul.systems
and committed by
tangled.org
3d4f4b50
b3146a05