- sqlite scheduler tick: a failed COMMIT (or ROLLBACK) left the connection released with a stuck-open transaction, wedging the writer slot forever — the one commit site missing the drop-+-reopen-replenish arm; tick now takes the conn by value with a poisoned disposition (drop + replenish), the commit-fault seam reaches the tick, and failed_tick_commit_replenishes_the_writer_slot pins the arm - pg save_offset_tx stored GREATEST($3,0) where the wave-8 gate's negative-seed fix (raw seed, ADR-023 §2 2026-10-11 annotation) covered only the auto-commit form; mem's merged in-tx read clamped a staged negative seed against a phantom zero; both fixed and the tx-form totality pinned by a new stamped suite leg (in_tx_reads_see_own_writes) green on all three columns vs the live harness server - pg: a failed forwarder register leaked the channel refcount forever (no receiver to drop it) — listen/spawn_bridge now unregister on the register error, mirroring the subscribe-failure arm; register doc corrected to put the failure disposition at the call site - pg forwarder: nine poisoned-mutex expect() panics converted to the mem crate's into_inner recovery idiom (no-expect-outside-tests rule) - core encode_payload: a deeply-nested Value aborted the process in serde_json's unguarded serializer recursion and the fuzz pin blessed storing bytes no decoder could read back; encode now rejects nesting past 127 containers (the empirically-verified decode limit) with typed Codec — decode-aligned boundary pins added to the fuzz invariant and value_type_tests, applied pre-publish within the open class-1 window - scheduler sleep deadlines: Instant + Duration overflow caps (MAX_SLEEP_S, tick-and-resleep) on all three engines for far-future boundaries; sqlite tick claim stamp now saturating_add - docs read true again: Error::Closed scope (stream try_recv arm), validation whitespace wording, sqlite lib.rs phantom durability knobs, pg lock.rs honest autocommit wording, mem tx/stream merge + wake- granularity wording; wakes.rs stale allow(dead_code) removed - tests: value_type_tests exercised its own map_err instead of the function under test (fixed); unwrap_or_default reference encodes panic with cause - docs/reviews/004-pre-release-general-review.md records nine fixed + sixteen parked findings (sqlite seam panic-safety armor, watcher post-death-subscribe hang + initial-stat self-kill, pg forwarder TOCTOU/hang/diagnosability, pg BIGINT server-side stamp overflow, mem growth/outbox notes, pg identifier-truncation doc note, smaller) and two false positives caught on verification; implementation.md review-rounds entry + core-contract backlog rows updated - gates: build, workspace tests (sqlite 194, core 27, suite legs new), full live pg column 127+25+9, clippy -D warnings, fmt, fuzz replay 6/6, wasm32 check, doc gate -D warnings — all green
alkstore
One reactive store interface — durable notify/subscribe signals, streams
with per-consumer offsets, durable queues with the transactional enqueue
property, named locks, a scheduler, and an outbox helper — over SQLite,
Postgres, and the in-process mem engine. Each engine uses its own native
machinery underneath (SQLite: the forked honker watcher design;
Postgres: pg_notify/LISTEN and the pg-boss schema family; mem: guarded
in-process state, ephemeral by design).
alkstore is the substrate, made once: the answer to "how does a change in the database become visible to other processes/connections?" that downstream stores don't re-derive.
The interface
One unified trait surface (alkstore::Store), seven mechanisms:
- notify / listen — durable publish/subscribe signals with transactional wake discipline (wake on commit, opaque wake → re-read).
- streams — append-only logs with per-consumer offsets, carried
metadata, and consumer-invoked
trim_to. - queues — visibility-timeout claim discipline, backoff, dead-lettering, and an opt-in retention sweep; enqueue is available inside the transaction seam (the outbox pattern).
- named locks — TTL-based lease locks with renewal and expiry reclaim.
- outbox helper — transactional enqueue-on-commit (
outbox_enqueue_tx) with arun_oncedelivery driver. - scheduler —
@every-interval schedules that enqueue onto a queue, with bounded catch-up and leadership discipline (multi-process safe). - transaction seam —
with_txand caller-heldTxHandlemethods; drop of the handle rolls back.
Crate family
Downstream consumers depend on alkstore (core) plus exactly one engine
crate. Engine identity is compile-time: the engine crate your binary
depends on is the deployment statement — there is no runtime
capability surface (ADR-016).
| Crate | Role | Engine posture |
|---|---|---|
alkstore |
the contract artifact: unified trait surface, value types, error model (no driver dependencies) | — |
alkstore-sqlite |
SQLite engine (rusqlite over the in-tree forked honker substrate) | single-host, file-backed — cross-process on one host is verified territory; NFS two-writers is not a supportable posture |
alkstore-postgres |
Postgres engine (tokio-postgres + deadpool-postgres, hand-rolled LISTEN) | multi-host native; v1 TLS is effectively unavailable (NoTls on every connection path) — network confidentiality comes from the deployment topology |
alkstore-mem |
in-process engine, full contract v1 | honest-ephemeral — no durability, single-process, never fleet-valid; loss on process exit is the documented posture; compiles clean on wasm32-unknown-unknown |
The unified trait does not pretend SQLite is multi-host — the honest boundary lives in the compile-time engine identity plus the documented deployment matrix (host semantics, connection budgets, durability knobs, growth postures).
Usage
The mem engine is the zero-config example carrier — one call and no options:
use alkstore::{EnqueueOpts, JobState, QueueOpts, Store};
use alkstore_mem::MemStore;
let rt = tokio::runtime::Builder::new_current_thread().build()?;
let store = MemStore::new();
let queue = rt.block_on(store.queue("demo", QueueOpts::default()))?;
let job_id = rt.block_on(queue.enqueue(
serde_json::json!("hello"),
EnqueueOpts::default(),
))?;
let claimed = rt.block_on(queue.claim_one("worker-1"))?
.expect("the enqueued job is claimable");
assert_eq!(claimed.job().id, job_id);
assert_eq!(claimed.job().state, JobState::Processing);
The surface is the same on every engine; only the constructor differs:
let store = alkstore_sqlite::open("store.db", SqliteOpts::default())?; // single-host
let store = alkstore_postgres::open(&dsn, PgOpts::default()).await?; // multi-host
let store = MemStore::new(); // in-process
Three engines, one pinned contract
The contract is pinned once, not per engine: the internal
alkstore-contract-suite crate parameterizes the contract's property
set over a store factory, and each engine crate runs the same suite
rows against its own factory as part of its test target. Three engines,
one pinned contract — the core contract
is the spec of record (mechanisms, delivery guarantees, error taxonomy,
naming / reserved namespace).
Documentation
- Core contract — the unified trait surface: mechanisms, delivery guarantees, tx seam, naming.
- Deployment matrix — host semantics, connection budgets, durability knobs, growth postures.
- Engine specs — SQLite, Postgres, mem.
- Decision index — the ADRs.
- API docs — full crate documentation.
- CHANGELOG.md
License
Dual-licensed under MIT or
Apache-2.0, matching the workspace
license = "MIT OR Apache-2.0" field.
The SQLite engine carries a forked third-party substrate (honker)
in-tree under its upstream dual license; its bundled license notice
(alkstore-sqlite/src/substrate/LICENSE) is the preservation
obligation's carrier and is not replaced by the root license files
(ADR-018).