DrinkMeAlice. Conn
(drink_me_alice v0.1.0)
Copy Markdown
The single supervised connection owner: one Mint/MintWebSocket
connection, one outstanding RPC at a time, bounded admission via
DrinkMeAlice.Credits before any mailbox send.
Outcome vocabulary (transport facts only)
call/3 returns transport-level facts; DrinkMeAlice.Tx.run/4 (or a
caller's own port) maps them to write outcomes:
{:reply, decoded}— a reply for this request id was received and decoded under the bounded profile.{:lost, :timeout | :closed | :transport | :send_failed | :down}— the request was sent (or may have been); no classifying reply arrived before the deadline. For mutations this is{:unknown, request}— never a claim of failure.{:refused, :down | :expired | :exhausted | :too_large | :encode | :clock_uncertain}— certified non-send: the request provably never entered a write path (connection down before send, caller deadline already passed while queued, credit bound reached, payload or encoding bound, or — forkind: :mutationrequests — clock uncertainty latched at dispatch time). For mutations this is{:unavailable, error}.
Serialization
Requests are handled one at a time — the single socket is FIFO and a
serial owner cannot interleave writes. A caller's deadline is carried
in the request; a request whose deadline expired before its send is
refused (:expired) rather than submitted late, so unavailable
always certifies non-submission. A caller that dies while queued is
swept by Credits.sweep/1; a caller that dies after send gets its
outcome discarded — the durable side is unaffected.
Notifications as hints only
An optional live subscription on one configured table delivers
notifications that set a bounded hint_pending flag and fan out to at
most 16 subscriber pids as {hint_tag, table, generation}.
Notifications never mutate durable reads — callers always read afresh. A notification whose live-id is not the current
subscription's is stale (3.0.5's LIVE defect can deliver post-invalidation
on a socket we did not replace); it is counted and discarded. On
session expiry or auth failure the whole socket is closed and a new
connection authenticates/selects/resubscribes — the adapter does not
switch identities on a live-subscription socket.
Clock authority (optional)
When clock_query is configured, establishment reads it — a query
whose single result is %{"now" => string, "floor" => _, "regressed" => boolean} — and every reply is scanned for a regressed: true result
or a clock_regressed decision. Either latches clock_uncertain for
this connection generation; a mutation (kind: :mutation) is refused
at dispatch — serially in this owner, immediately before its frame is
written — while the latch holds. Reads are never gated.
clock_authority/1 reads the latch for introspection only; it is not
part of any write path. Without clock_query no floor is read and
nothing latches.
Configuration
host,port,ns,db,user,pass— required.signin—:database(default: a database-scoped user, the least-privilege runtime identity),:namespaceor:root.live—nil(default) or a table name to subscribe for hints.hint_tag— the atom tagging hint messages (default:drink_me_alice_hint).clock_query—nil(default) or{sql, vars}as above.poll_ms(default 50),connect_timeout(default 2_000).name— optional registered name.
Summary
Functions
Send one RPC under the caller's deadline (default 2 s — the ordinary RPC bound). Reserves a credit slot first; see the module doc for the returned transport facts.
Cached clock authority for introspection: :trusted | :uncertain. Not a write-path gate — mutations are gated at dispatch in this owner.
The credits table id for Credits.snapshot/1 evidence.
Subscribe pid (default caller) to work-item hints. Bounded at 16.
Take the coalesced dirty hint: clears the flag, returns times seen.
Test-only transport fault seam — honest socket-level events, never a bypass of the transaction path
Start the owner. See the module doc for config keys. An invalid
configuration is refused before any process starts:
{:error, {:invalid_config, key}}.
Notification/health counters for evidence — no durable claims.
Connection status: :disconnected | :reconciling | :healthy.
Types
Functions
@spec call(GenServer.server(), request(), keyword()) :: {:reply, map()} | {:lost, atom() | tuple()} | {:refused, atom()}
Send one RPC under the caller's deadline (default 2 s — the ordinary RPC bound). Reserves a credit slot first; see the module doc for the returned transport facts.
The request payload is installed into the reserved slot atomically
(Credits.enqueue/3 — one CAS that publishes payload+state together).
The owner drains :enqueued rows on a fixed poll_ms cadence — no
caller→owner message exists, so the owner mailbox stays bounded under
arbitrarily delayed callers. The owner frees the slot when it consumes
the row (replied or deadline-refused): a caller that times out or dies
never releases, and a dead holder's :reserved-only slot is reclaimed
by Credits.sweep/1. No timing window can orphan a queued payload.
@spec clock_authority(GenServer.server()) :: :trusted | :uncertain
Cached clock authority for introspection: :trusted | :uncertain. Not a write-path gate — mutations are gated at dispatch in this owner.
@spec credits(GenServer.server()) :: {:ok, :ets.tid()} | {:error, :no_conn}
The credits table id for Credits.snapshot/1 evidence.
@spec hint_subscribe(GenServer.server(), pid()) :: :ok | {:error, :full}
Subscribe pid (default caller) to work-item hints. Bounded at 16.
@spec hint_take(GenServer.server()) :: {:hint, non_neg_integer()} | :clean
Take the coalesced dirty hint: clears the flag, returns times seen.
@spec inject(GenServer.server(), :kill_socket | :drop_after_send | :drop_reply) :: :ok
Test-only transport fault seam — honest socket-level events, never a bypass of the transaction path:
:kill_socket— close the TCP transport now (network-failure shape).:drop_after_send— after the next request frame is written, close the transport before the reply can arrive (lost-reply shape).:drop_reply— receive the next request's reply, then close the transport and report{:lost, :reply_dropped}WITHOUT delivering it: a reply arrived but is discarded before classification — it does not prove the write committed — so the caller observes a real lost-reply outcome.
Session-expiry evidence uses REAL server expiry — FOR SESSION
credentials issued through Admin helpers — not a transport seam.
Start the owner. See the module doc for config keys. An invalid
configuration is refused before any process starts:
{:error, {:invalid_config, key}}.
@spec stats(GenServer.server()) :: map()
Notification/health counters for evidence — no durable claims.
@spec status(GenServer.server()) :: status()
Connection status: :disconnected | :reconciling | :healthy.