What YmerNode.Messages holds — every kept message by thread, and each
handle's read mark, last send and last check on every thread it has taken
part in — as a value, with the functions that decide what a send, a check and
a read do to it.
The process is the clock around this module: every function here is handed the time it runs at, and the page it answers within, so each rule below is a deterministic function a test drives with a count cap of three rather than ten thousand.
Ids and the count cap
Every message takes the next id across all threads, so ids order the whole history and a read's cursor needs no thread beside it. Past the count cap the oldest message is dropped, whichever thread holds it. The thread remembers the newest message it lost, and that is how a check whose read mark sits before it, or a read that reaches back to the oldest message kept, can say that messages were dropped.
Read marks
Each handle's read mark on a thread is kept
here as a message id. check/5 returns what lies past it, oldest first,
leaves out the handle's own
messages, and moves the mark past everything it walked — its own messages
among them, so they never count as unread. A check also joins the thread: a
thread nobody has posted to yet exists from its first check, so a session
waiting on it shows in the thread's header before anything arrives.
Pages
A page stops at its message count or its byte budget, whichever comes first,
and always holds at least one message, so paging always moves. What a
message weighs is the page's size function — YmerNode.Messages measures
each message as it will be sent.
read/4 moves no mark. It merges the named threads' messages newest first,
takes the newest last of them across all those threads together, and cuts
the page from the top. Every message last or the page leaves out is older
than every message kept, so the oldest id returned is the cursor to pass back
as before, and it is given while any named thread keeps an older message.
That is why last is never taken thread by thread: a busy thread's cut
messages would then sit above a quieter thread's oldest returned one, and a
cursor below them would skip them for good.
Summary
Functions
Checks thread as handle at now, within page. Returns the answer and
the state with the handle's read mark moved.
A thread's header: its message count, last activity, and each handle with
its last send and last check, by handle — nil for a thread the process has
never seen.
One row per thread — its message count, last activity and handles — the most recently active first, threads nobody has posted to last.
Whether handle has sent on, or checked, any thread.
Posts body to thread as handle at now. Returns the message and the
state it is kept in — past the count cap, without the oldest message held.
Reads threads — a list of names, or :all for every thread — within
filters and page, moving no mark.
The threads handle has taken part in that hold messages it has not read,
each with the count of them, by thread. Its own messages are never unread.
Types
@type page() :: %{ count: pos_integer(), bytes: pos_integer(), size: (YmerNode.Messages.Message.t() -> non_neg_integer()) }
How many messages a page holds, how many bytes, and what one weighs.
Functions
Checks thread as handle at now, within page. Returns the answer and
the state with the handle's read mark moved.
The answer's messages are oldest first; more counts the unread left past
the page; last_check is the handle's previous check of this thread, nil
on its first; posted? says whether anything was ever posted to the thread;
and dropped_at is the time of the newest message the thread lost to the
count cap, given only when the handle's read mark sits before it.
A thread's header: its message count, last activity, and each handle with
its last send and last check, by handle — nil for a thread the process has
never seen.
One row per thread — its message count, last activity and handles — the most recently active first, threads nobody has posted to last.
Whether handle has sent on, or checked, any thread.
Posts body to thread as handle at now. Returns the message and the
state it is kept in — past the count cap, without the oldest message held.
Reads threads — a list of names, or :all for every thread — within
filters and page, moving no mark.
filters holds last, the newest messages taken across the named threads
together; before, an id every message returned lies below, or nil; and
since, a UTC time every message returned was posted at or after, or nil.
Each thread's answer carries its header — nil for a thread the process has
never seen — its messages newest first; matched, how many of the messages
it keeps fall within before and since, so an answer with none can say
whether the filter or the page left them out; and dropped_at when the read
reached back to the oldest message the thread keeps and the count cap had
dropped messages before it. Under :all only the threads with a message to
return, or a drop to report, answer.
The threads handle has taken part in that hold messages it has not read,
each with the count of them, by thread. Its own messages are never unread.