R5.01.3 · dive 3
The cursor.
The stream entry id is the cursor: it orders the log, it is the resume position a reader saves, and
— because the id carries time — a DateTime becomes a range bound.
A reader of a long log does not re-read from the start each time. It remembers the id of the
last entry it processed, and the next read starts strictly after that id — XRANGE key (<last-id>
+, the ( making the bound exclusive. The saved id is the cursor; advancing it is the whole of
"resume". Advance the cursor through the log; the readout shows what the next read returns.
The entry id is the resume position
the-cursor.md · resumeA reader of a long log does not re-read from the start each time. It remembers the id of the last entry it
processed, and the next read starts strictly after that id: XRANGE key (<last-id> + reads
everything newer than <last-id> — the ( makes the bound exclusive. The saved
id is the cursor; advancing it is the whole of "resume". Because the id is monotonic, "after this id" is
unambiguous, and a restarted reader picks up exactly where it stopped.
# first read — from the start XRANGE orders - + COUNT 100 # … last entry id …099-0 # save the cursor: …099-0 # next read — strictly after the cursor XRANGE orders (…099-0 + COUNT 100 # ( makes the bound exclusive
The cursor is an id, so it is cheap to store anywhere — a key, a row, a variable. A reader is defined by its cursor: where it is in the log is the id it last saw.
The cursor is the id last processed; the next read is XRANGE (cursor +, exclusive, so a
restarted reader resumes exactly where it stopped.
The id carries time, so time is a bound
the-cursor.md · time-travelA stream entry id begins with the millisecond the entry was added. That makes the id a clock: an instant maps to
an id, and an id range expresses a time range. To read "everything from time t onward", form the
smallest id at that millisecond — <t-in-ms>-0 — and range from there.
XRANGE orders 1719312000000-0 + # every entry from that ms onward XRANGE orders 1719312000000-0 1719315600000-0 # a closed time window
This is the time-travel read: state as of a past instant is the fold of the log up to that instant. Folding the
events with id at or below <t> reconstructs exactly what was true at t — the
log read as a function of time.
The id is a clock, so a time becomes the floor id <ms>-0 and a window becomes an id
range; the state at a past instant is the fold of the log up to it.
The archive frontier — a cursor into durable history
the-cursor.md · the watermarkA log is trimmed so it does not grow without bound (the subject of R5.02). What is trimmed is not lost: it folds into a durable store, and a watermark records how far the archive has advanced. That watermark is itself a cursor — the id of the highest entry already archived. Below it is deep history, read from the durable floor; above it is the live tail, read from the stream. A reader chooses which side of the watermark it needs, and the two read paths meet at that one id.
The archive watermark is a cursor too — the id where durable history ends and the live tail begins; a reader picks the side it needs and the two meet at that one id.
In EchoMQ — read_since/5 and the archive cursor
echo_mq/stream.ex · read_since/5 · put_archived/4EchoMQ derives the time bound host-side so the wire only ever sees an <ms>-<seq> id,
never a raw integer. EchoMQ.Stream.read_since(conn, queue, name, %DateTime{} = t0, count) reads
everything minted at or after t0, in real code in echo/apps/echo_mq:
# EchoMQ.Stream.read_since/5 — a DateTime becomes a range bound (real) EchoMQ.Stream.read_since(conn, "orders", "events", ~U[2024-06-25 12:00:00Z]) # from = minid_floor(t0) -> "<ms>-0" (the smallest id at that ms) # to = "+" (the live top) # XRANGE emq:{orders}:stream:events <ms>-0 + # -> {:ok, [{branded, fields_map}, …]} mint order, [t0, ∞)
minid_floor/1 turns t0 into "<ms>-0" — the lowest id at that
millisecond — so the half-open [t0, ∞) edge is exact: an entry minted at t0
is included, one minted a millisecond earlier is not. read_window/6 adds the inclusive upper bound
maxid_ceil/1 ("<ms>-<max-seq>") for a closed [t0, t1] window
— backtest, audit, debug. The branded EVT mint instant of each returned entry falls inside the
window, in mint order.
The archive watermark is a cursor too. EchoMQ.Stream.put_archived/4 caches the branded
EVT id of the highest-folded record under emq:{q}:stream:<name>:archived, and
EchoMQ.Stream.get_archived/3 reads it back — {:ok, w} for a recorded seam, or
:empty when the whole stream is still live tail. A polyglot reader discovers where the archive ends
and the live tail begins from that one id, without a store call. What is below the watermark folds to the durable
floor; the store-side EchoStore.StreamArchive.fold/3 is the engine that puts it there.
Notes on ValkeyAn XRANGE bound prefixed with ( is
exclusive, and a bare <ms> is treated as <ms>-0 — which is exactly how a
millisecond instant becomes the floor id of a time-bounded read —
valkey.io/commands/xrange.
The entry id is the cursor — the resume position, and a time bound because the id carries the time the entry was added.
EchoMQ.Stream.read_since/5 turns a %DateTime{} into the floor id
"<ms>-0" via minid_floor/1 and ranges from it; the archive watermark
emq:{q}:stream:<name>:archived is the cursor where durable history meets the live tail.
EchoMQ turns a DateTime into a stream id and ranges from it, and keeps the archive seam
as a cached id — the cursor is both the resume position and the boundary into durable history.
A door, not a depth — the persistence floor
What is trimmed off the log folds into the durable page tier and, beyond it, to remote object storage behind a
create-only commit fence. That floor — the single-writer engine, the lazy reader, and the durability dial
a system turns from "hold nothing" to "commit-per-record + replicate off-box" — is the subject of
the EchoStore persistence course. EchoStore.StreamArchive.fold/3
folds a trimmed slice into it. This dive teaches the cursor; that course teaches the floor it points into.
The Bus pillar that owns the stream
The consumer-group read that resumes from a saved cursor, the time-travel reads in full, and the archive fold consumer are the subject of the dedicated EchoMQ course. From here, open onto the Bus pillar — the retained, replayable log and its readers.
References
sources · relatedSources
- Valkey — XRANGE — the exclusive
(bound and the<ms>-<seq>id range, the cursor and the time bound. - Valkey — Introduction to Streams — the entry id as a clock and a resume position.
- Valkey — XINFO STREAM — the stream's last id, the top the cursor chases.
- Kreps, J. — The Log — the offset into the log as the reader's position.
Related in this course
- R5.01 · Event sourcing on Streamsthe module hub: the entry id is the cursor
- R5.01.1 · The append-only logthe write side, XADD
- R5.01.2 · Replay and rebuildthe read side, XRANGE and the fold
- R5.02 · Stream consumer patternstrimming the log and the archive
- /echo-persistencethe durable floor a trimmed segment folds into