jonnifyfsharp

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.

C4.08.3 · part 1

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
The README states the discipline: the in-memory 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."
C4.08.3 · part 2

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

caller Jobs.enqueue Jobs · the surface one contract, two backings in-memory Store · live Map / Set transitions Valkey Connector · coming soon eval raises today

      

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.

C4.08.3 · part 3

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
When the Valkey 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

Related in this course