13 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-10-05 |
Core contract
The unified, engine-agnostic surface a Store exposes. This document
specifies WHAT the contract is; the per-engine specs map it onto their
machinery; ADRs carry the WHY. The starting artifact is the honker-rs
surface (the Phase 0 §Interface finding) scoped to the
inventory-confirmed features — ADR-002
— and pinned against it. The exact surface is pinned by
ADR-008 (contract v1 partition,
TxHandle representation, wake type, reserved strings, error
taxonomy, config split); the obligations are this document.
Concepts
- Store — what a consumer opens from a connection string or file path (ADR-001): one handle, the engine behind it chosen at open time. Consumer code never branches on engine type (guiding principle 4).
- Mechanism — one of the surface's coordination families: notify,
streams, queues (+outbox), locks, scheduler. Delivery guarantees are
pinned per mechanism in ADR-006's
table (notify/streams/queues), extended with the locks row by
ADR-008; the scheduler row
rides OQ-09's collapse decision.
(Guiding principles are defined in
docs/research/phase-0.md§Vision and cited by number throughout this directory.) - Wake — the opaque "something changed, re-read" signal the wake contract delivers (ADR-006). The contract's central abstraction.
- TxHandle — the caller-held transaction object whose
*_txmethods make side effects commit-atomic with a business write (ADR-007). Contract shape: the*_txmethods live on theTxHandletrait itself — engines implement it for their concrete handle, no downcast (ADR-008 §2).
The seam
Per ADR-008 §6, the trait is constructed by engine crates (durability knobs, pool sizing, watcher cadence are engine options — deployment.md carries the facts); the contract is the trait surface it returns:
Store::begin_tx() -> Box<dyn TxHandle + Send>
trait TxHandle {
enqueue_tx(name, opts, payload) -> job_id
publish_tx(stream, payload) -> event_id
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
commit(self: Box<Self>) -> Result<()> // or rollback
}
Mechanism handles come off the store (or, for transactional variants, off the handle):
store.notify(channel, payload) handle.notify_tx(channel, payload)
store.stream(name) -> Stream handle.publish_tx / save_offset_tx
store.queue(name, opts) -> Queue handle.enqueue_tx
store.try_lock(name, owner, ttl) -> Option<Lock>
store.listen(channel) -> Box<dyn WakeReceiver>
store.outbox(name) -> Outbox
Payloads cross the trait as core value types (non-generic trait
methods — object safety of the boxed handles,
ADR-008 §2); payload_as<T>
decodes on concrete returned values (Job, StreamEvent). The
with_tx closure wrapper ships over the handle shape (the POC-verified
composition direction, ADR-007).
Mechanism handles (Stream, Queue, Outbox, Lock) are core-owned
trait objects like the tx handle — engine types never appear in
consumer signatures; their trait methods pin at implementation,
mirroring the TxHandle pattern (ADR-008
§2's rationale applies identically).
Mechanism contracts
notify / listen
Fire-and-forget signals, commit-atomic when sent in a transaction (ADR-007). No durability, no replay, no per-listener retry; a listener attached after a commit never sees it.
notify(channel, payload)— payload ≤ 8000 bytes on Postgres (client-side checked, typedPayloadTooLargebefore the round trip, verified by POC #2); no limit on SQLite. The error variant is contract-wide (callers match it identically on both engines), the occurrence is the documented engine asymmetry (ADR-008 §5) — see also OQ-08.listen(channel) -> Box<dyn WakeReceiver>— starts from "now"; delivers opaqueWake { channel }signals (ADR-006), never payloads or ids (ADR-008 §3). Wakes are at-least-once, possibly coalesced (SQLite) or per-notify (Postgres), possibly repeated after reconnect. Consumers must be idempotent on wake.- Failure surfaces as channel events, never silence: watcher death
closes the receiver (
recv() -> None, SQLite); the synthetic reconnect-wake (a reserved channel (ADR-004)) covers Postgres connection gaps. - Channel name is the one piece of semantic content a wake carries — the invalidation key for caching subscribers.
streams
Durable pub/sub with per-consumer offsets
(ADR-006). The durable
cousin of notify: publish is commit-atomic; every committed event is
readable by every consumer whose offset hasn't passed it;
replay-on-attach is the default; offsets are explicit and
transaction-aware (save_offset_tx gives exactly-once-within-a-
business-tx shape).
publish/publish_tx/publish_with_key— append to the stream's durable log.read_since/read_from_consumer(offset)— cursor-based reads;get_offset(consumer)— checkpoint inspection.save_offset/save_offset_tx— consumer checkpoint, explicit; the contract's save is always explicit (no auto-checkpoint cadence — honker's per-binding ambiguity is not inherited). The subscription handle exposes nosave_every/ auto-save-on-drop (ADR-008 §8).subscribe(consumer) -> Box<dyn EventReceiver>— durable consumption: attach, read to current tail, resume after restart from the stored offset; explicitsave_offseton the receiver (shape in ADR-008 §8).
queues
Durable at-least-once work (ADR-002). The semantics-depth design is queues.md's (OQ-05); the contract-level obligations here:
enqueue/enqueue_tx— commit-atomic;EnqueueOpts { delay, run_at, priority, max_attempts, expires }(the v1 skeleton, ADR-008 §1; deeperQueueOpts/retry surface rides OQ-05).claim_one/claim_batch— exactly-once handout under concurrency (POC-pinned on both engines);cancel,get_job— job management and inspection.- Job handle:
ack / retry / fail / heartbeat; visibility timeouts make at-least-once safe (work re-appears if a claimant dies). sweep_expired— maintenance entry point (design in queues.md).
named locks
TTL-bounded coordination locks, transactional-friendly:
try_lock(name, owner, ttl)— acquire or fail (Option<Lock>— no-work is a value, not an error);renewandreleaseon the lock handle. Release on explicit unlock or TTL expiry. Re-acquirable after expiry (POC-pinned on Postgres; on SQLite it rests on honker's machinery — the SQLite-side pin is in the verification backlog below).- Guarantee row (ADR-008
§7): mutual exclusion bounded by TTL + renewal — after TTL expiry
exclusion lapses silently (no revocation event); holders must
renewwithin TTL; expiry is a loss of exclusivity, not an error. - Lock names are shared-namespace (reserved-prefix rules below, ADR-008 §4).
outbox
A helper over queues, not a separate mechanism: enqueue inside the business transaction + the delivery/consumption worker entry points. Same evidence base and guarantee as queues (ADR-002).
scheduler
Cron/@every enqueueing into named queues, leader-elected where the
engine has peers (SQLite: single-host, no election needed beyond
documented posture; Postgres: advisory-lock election). Whether this is
a first-class mechanism or queues + schedule() is OQ-09.
Cross-cutting contracts
Delivery-guarantee table
The per-mechanism table in ADR-006 is the contract of record for notify/streams/queues; this spec inherits it and adds the consumer obligations:
- wake idempotence (notify's at-least-once, coalescing behavior);
- explicit offset saves (streams);
- visibility-timeout budgeting (queues);
*_txoperations are only durable after the caller's commit — rollback drops job rows, event rows, notifications, and offset saves together (the no-ghosts property, POC-pinned on both engines).
Errors
thiserror-typed, per the family standard. Pinned by
ADR-008 §5: one top-level
Error, with v1 variants PayloadTooLarge (universal; produced
pg-side, contract-wide matchable), ReservedName, InvalidName,
Closed, Codec, and the opaque Database fallback (engine detail
preserved via the source chain). Pinning rule: a variant exists only
when callers can act differently on it. Queue claim "no work" and
job/lock boolean results are values, not errors.
Capability surface
None in contract v1. Whether the Store exposes engine capabilities
at all — and if so, which (payload limits, host semantics,
wake-cadence knobs) — is OQ-08's decision
(deployment.md).
Naming / reserved namespace
Consumer-visible names (channels, streams, queues, locks) share engine-visible namespaces on Postgres (LISTEN channel names are server-global per database). Pinned by ADR-008 §4:
- Reserved prefix
__alkstore_, engine-independent, applies across all name kinds; the one v1-reserved string is__alkstore_listener_reconnected__(the Postgres reconnect-wake channel, ADR-004). - Reserved-prefix names are rejected at every entry point — the
name-bearing methods and their
*_txcounterparts — with the typedReservedNameerror; empty names withInvalidName(ADR-008 §4). Stream-consumer names are consumer-local identifiers, not a reserved-namespace kind. - Engine-derived names in consumer namespaces carry the reserved
prefix (the outbox's backing queue is derived under the prefix —
honker's
_outbox:{name}scheme is not inherited verbatim). - SQLite: honker's
_honker_*internal table family is storage- internal (not consumer namespace); Postgres: queue/stream tables are schema-scoped (layout rides OQ-05) — the channel namespace is this section's.
Verification backlog
Contract properties POC-pinned on one engine only (or sketched rather
than surface-verified) — the contract test suite must pin both engines
before the engine specs are called stable:
- Lock TTL/expiry re-acquisition on SQLite — pinned on Postgres (pg POC's lock probe); on SQLite it rests on honker's machinery unverified.
- Wake semantics under
WakeReceivershapes — POCs verified wake delivery/coalescing through their own probe types; the pinnedWake { channel }/ recv forms (§3 of ADR-008) need the contract suite's own property tests on both engines. save_offset_txexactly-once-within-a-business-tx shape — both POCs verified it through their sketch implementations (SQLite's offset-save was a plain SQL upsert, not the full honker surface; the pg side likewise through its probe), so the property must be re-pinned against the real engines'save_offset_txin the contract suite.- Concurrent
try_lockloser/error behavior on SQLite (the pg side returns cleanly; honker's busy-path under lock contention is the thing to pin).
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 001 | Crate split | core + per-engine crates; single-driver binaries |
| 002 | Feature scope | inventory-confirmed features only; cut-flags explicit |
| 006 | Wake contract | opaque wake + re-read; notify-vs-streams guarantee split |
| 007 | Tx seam | caller-held handle, *_tx methods, native commit-atomicity |
| 008 | Contract v1 pinning | surface partition, TxHandle trait shape, Wake type, reserved strings, error taxonomy, config split, locks guarantee row |
Open Questions
Open questions are tracked in open-questions.md. Key questions affecting this document: