Dive C4.08.3
The store port
One surface, two backings. The caller writes against the verbs; the durable store can change under them.
Persistence ends with a question of structure, not storage: how do you keep the durable backing
separable from everything above it? The answer is a port. In the F# EchoMQ port,
EchoMQ.Jobs is the public verb set — enqueue,
claim, complete, retry,
promote, reap — and each delegates to a pure in-memory
Store transition today. The Valkey-backed form rides
EchoMQ.Connector, whose eval currently raises
failwith "EchoMQ Valkey connector: coming soon". Because both sit behind the same
surface, the durable backing swaps in with no caller change. This dive shows the surface, the two backings, and
the transitions persisting state — all verified in dotnet fsi.
One surface: the verbs
The public surface is a set of named verbs over a queue's state. Each verb takes the state and returns a new state plus a result — the functional shape of a transition. The caller speaks these verbs and nothing else; it never reaches into the rows or the sets directly. That is what makes the backing swappable: the surface is the contract.
// echo_fs/EchoMQ/Jobs.fs — the public verbs, quoted verbatim module Jobs = /// Admit a job (idempotent; refuses a duplicate id). let enqueue (id: string) (payload: string) (st: QueueState) = Store.enqueue id payload st /// Claim the oldest pending job under a lease. let claim (now: int64) (leaseMs: int64) (st: QueueState) = Store.claim now leaseMs st /// Retire a claimed job (token-fenced). let complete (id: string) (token: int) (st: QueueState) = Store.complete id token st
Store "stands in for the
branded CHAMP / Valkey rows behind the same enqueue/claim/complete/retry/promote/reap
surface, so the durable backing swaps in with no caller change."Two backings: in-memory now, Valkey next
The backing today is the in-memory Store: immutable Map and
Set, the working skeleton. The backing on the way is the Valkey transport — the
Connector seam — which is, in the README's words, the genuinely
coming soon part. It is present as a named seam whose one call raises until the wire lands, so the
shape is fixed even though the transport is not.
// echo_fs/EchoMQ/Jobs.fs — the Valkey seam, quoted verbatim module Connector = /// Evaluate a server-side script against Valkey. Coming soon. let eval (_script: string) (_keys: string list) (_argv: string list) : 'a = failwith "EchoMQ Valkey connector: coming soon"
In-memory · live
Immutable Map/Set via Store; the working core, fully tested.
Valkey · coming soon
The Connector seam; eval raises today, the wire lands next.
The same call, either backing
Pick a verb — enqueue, claim, or
complete — then choose the backing. Against the in-memory Store
the call runs and returns its real result over the queue's state. Against the Valkey
Connector the same call routes to a seam that raises today. The caller's line of code
is identical in both — that is the port. The in-memory results below are verified in
dotnet fsi.
The store port · pick a verb and a backing
The verb is the contract. The in-memory backing answers it now; the Valkey backing will answer the same verb later. The caller's code names neither backing.
The in-memory store persists state today
"In memory" does not mean "not persistent" within a run: the Store threads a
QueueState through every transition, so each verb returns the next state for the next
call. Admission is idempotent (a duplicate id is refused), a claim bumps the attempt count that doubles as the
lease's fencing token, and a complete only succeeds for the current token holder. Verified in
dotnet fsi over the real ids:
// verified in dotnet fsi — the in-memory Store, threaded state let st1, _ = Jobs.enqueue "JOB0Dqhu1EacDY" payloadA Store.empty let st2, _ = Jobs.enqueue "JOB0Dqhu1EsDLc" payloadB st1 let _, dup = Jobs.enqueue "JOB0Dqhu1EacDY" payloadA st2 // dup = Admission.Duplicate — idempotent admission let st3, claimed = Jobs.claim 1000L 30000L st2 // claimed = Some { Id = "JOB0Dqhu1EacDY"; Attempts = 1; ... } — oldest, token bumped Jobs.complete "JOB0Dqhu1EacDY" 1 st3 // Ok () — the token holder retires it Jobs.complete "JOB0Dqhu1EacDY" 99 st3 // Error "stale" — a stale token is refused
Connector lands, these same transitions run as
server-side scripts against the durable store, and the rows live as Valkey keys instead of an immutable
Map — but Jobs.enqueue / claim /
complete stay the lines the caller writes.EchoMQ in F#
Status — echo_fs is an in-memory model. The EchoMQ protocol — the Lua transitions executing on a Valkey wire — is not implemented (there is no Lua and no Valkey client in the code); that is the coming-soon transport (EchoMQ.Connector). This block shows how the model uses the structure, not a running bus.
This dive is the bus's persistence story: the port, the two backings, and the transitions. Grounded
in the real echo_fs port.
echo_fs · Jobs (the surface), Store (in-memory), Connector (Valkey, coming soon)
EchoMQ.Store is the swappable persistence port: in memory today, the same
enqueue / claim / complete /
retry / promote / reap surface exposed
through EchoMQ.Jobs. Each verb is a pure transition over a
QueueState — the rows Map plus the four sets — so it returns
the next state, with admission idempotent and completion token-fenced.
The in-memory Store persists state today; the Valkey transport
EchoMQ.Connector lands next — its eval raises
"EchoMQ Valkey connector: coming soon" behind the same surface, so the durable backing
swaps in with no caller change.
Where this leads
The store is settled as a port. The next module keys an in-memory CHAMP on the same branded id, partitioned
by namespace, and puts a single writer in front of it — a MailboxProcessor, F#'s actor
— so concurrent mints and claims serialize without a lock. That is where the rows map becomes a branded CHAMP
behind an agent.
References
Sources
- Microsoft — F# documentation — modules, the verb-as-function surface, and
failwith. - FSharp.Core API reference —
Result,Option,Map, andSet, the transition return types. - Microsoft — .NET documentation — exceptions and the boundary a raising seam sits behind.
- The F# Software Foundation — F# on .NET, the home of the
echo_fsport.
Related in this course
- Elixir F4.08.3 — Redis keys — the parity dive: the namespaced key and the edge gate.
- C4.08 — Branded ids & persistence — the module hub.
- C4.08.1 — Keys & rows — the key the port stores under.
- C4 — Algorithms & Data Structures — the chapter.