Changelog
View SourceAll notable changes to mem-evoq will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.1.1] - 2026-05-15
Fixed
- Adapter contract:
mem_evoq_adapterwas returning raw reckon_gater#event{}records to evoq, but theevoq_event_storecontract expects#evoq_event{}records (or maps). The mismatch made the adapter unusable when actually wired into evoq —evoq_event_store:read/5would crash withfunction_clauseinside itsevent_to_map/1helper. 0.1.0 worked when called directly in mem-evoq's own tests but failed at the evoq seam. - Read paths (
read/5,6,read_all/3,4,read_all_global/3,read_by_event_types/3,read_by_tags/3,4) now translate#event{} → #evoq_event{}at the adapter boundary. - Subscription delivery now goes through a per-subscription bridge process that translates
{events, [#event{}]}→{events, [#evoq_event{}]}before forwarding.subscription_errormessages (e.g. integrity violations during catch-up) pass through unchanged. Matches the pattern reckon-evoq uses.
mac and signature are storage-only concerns and are intentionally NOT propagated through the adapter — same as reckon-evoq. Tests that assert on them now go through sys:get_state on the store gen_server.
[0.1.0] - 2026-05-15
Initial release. In-memory event-store adapter for evoq — a reference implementation of the evoq_event_store callback contract, intended for tests, demos, and integration scaffolding without spinning up Khepri/Ra.
Adapter surface
mem_evoq_adapterimplements the fullevoq_event_storecallback set: append, read (forward/backward, with optionalverifymode), read_all / read_all_global, version, exists, has_events, list_streams, delete, save_snapshot / load_snapshot / list_snapshots / delete_snapshot, subscribe / subscribe_all / unsubscribe.mem_evoqfacade:start_store/1,2,stop_store/1,list_stores/0.mem_evoq_registry— ETS-backed StoreId → Pid lookup with monitor-driven cleanup.mem_evoq_store— per-store gen_server holding streams, snapshots, subscribers, and (when enabled) the integrity context.
Write path
append/4honours?NO_STREAM,?ANY_VERSION,?STREAM_EXISTS, and exact-version expected-version semantics matching reckon-db.- Cross-stream global ordering via
epoch_usmicrosecond timestamps.
Read path
- Forward and backward reads slice exact
[FromVersion, FromVersion+Count-1](forward) /[max(0, FromVersion-Count+1), FromVersion](backward) windows. - Out-of-range requests return partial results (no padding, no error). Stream-not-found returns
{stream_not_found, StreamId}. Optional
read/6accepting#{verify => skip_legacy | strict | skip_all}for integrity-enabled stores. Backward verification uses the reverse → verify forward → reverse-back trick from reckon-db 2.1.1.
Subscriptions
- Per-store subscribe / subscribe_all / unsubscribe.
- Filter taxonomy:
by_stream,by_event_type,by_event_pattern,by_tags(withany/allmatch). from => 0triggers synchronous catch-up replay before the call returns.- Dead-subscriber cleanup via
processmonitor — subscriptions self-evict on'DOWN'.
Snapshots
- Save / load / load-at-version / list / delete.
- Stream deletion cascades to snapshots.
Tamper resistance (opt-in per store)
start_store/2accepts#{integrity => #{enabled => true, key => Key}}— Key must be exactly 32 bytes. Invalid sizes / malformed maps surface as{error, ...}at start time.- Every appended event under an integrity-enabled store carries
prev_event_hash(genesis for v0, chain hash of predecessor thereafter) andmac(HMAC-SHA256 viareckon_gater_integrity). - Per-stream chain-start watermark recorded on the first append; lazy enablement supports stores switched on mid-life.
- Read verification:
strictfails on legacy events;skip_legacy(default) verifies integrity-bearing events only;skip_alldisables verification entirely. - Snapshots carry
anchor_hash(chain hash of the event at the snapshot's version) +mac. Load recomputes the anchor — a tampered underlying stream surfaces assnapshot_anchor_mismatcheven if the tampered event itself has been re-signed with the legitimate key. - Subscription catch-up MAC-verifies every event before delivery; violations halt the catch-up with
{subscription_error, {integrity_violation, _}}.
Tests
- 93 EUnit tests across append / read / metadata / read_all_global / subscription / snapshot / integrity / snapshot-integrity surfaces.
- 6 Common Test cases (
test/integration/mem_evoq_integrity_writes_SUITE) mirroringreckon_db_integrity_writes_SUITE. - 6 PropEr properties × 100 random runs each (
test/prop/prop_mem_evoq). - Total: 699 distinct test invocations on
rebar3 eunit && rebar3 ct && rebar3 proper.
Out of scope (see README limitations table)
Persistence, clustering / replication, key rotation, per-region keys, vault integration, capability-token-bound writes, scavenging / archive, and reckon-db's full filter taxonomy. For any of these, pair evoq with reckon-evoq + reckon-db.