blockwatcher-gates
blockwatcher-gates implements the Gate port
once per module it ships: threshold and max_once
(crates/blockwatcher-gates/src/registry.rs). Each module compiles a
monitor’s gate.config against the same schemas the predicate uses, then
answers on_hit over an engine-owned journal. The engine owns
persistence, prune, and delivery: a module that opens sqlite, or that
drains the journal in on_invalidate, is a bug (lib.rs). It is a
module crate: adding, removing,
or changing one gate module never touches blockwatcher-core.
Gates already documents the operator-facing contract: event time, one gate per monitor, outstanding 0 on quiet hits, and prune-by-cursor on invalidate. This page covers what is specific to the crate: registration, the two shipped modules, and the compile refusals they share.
Key takeaways
blockwatcher-gatesimplements theGateport forthresholdandmax_once. Both depend onblockwatcher-typesandblockwatcher-portsonly, neverblockwatcher-core.- Catalog fold is
blockwatcher_gates::registry::gates::get_all()incrates/blockwatcher-embed/src/catalog.rs. There is nogatesfeature: both modules are always registered, the same way storage is. - Time-window gates require
block.timestamp(unsigned or non-negative int) on the compiled schema. The write refuses withgate requires 'block.timestamp'; this monitor's selectors do not expose it.
Responsibilities
- Register exactly the gate modules it ships (
"threshold","max_once") under the same family-enumeration convention every module crate follows (registry.rs). threshold: session digest. Config{ "count", "window_ms" }. Drop a prefix until the remaining span fitswindow_ms; iflen >= count,Emitthe oldestcountindices; elseRetain(threshold.rs).max_once: first hit per event-time window. Config{ "window_ms" }.Emit([this])whenlast_emit_tsis none or thisevent_tsis outside the window; otherwiseDiscard([this])(max_once.rs).- Share
window_msbounds (1..=86_400_000) and theblock.timestampschema check (lib.rs).threshold.countis2..=10_000.
Not this crate’s job: persisting gate_hits / gate_meta, pruning
on invalidate, minting Match ids, or stalling Progress on persist
failure: blockwatcher-core’s pipeline/gate.rs and
engine/invalidate.rs own those; defining Gate, GateDecision,
GateHit, CompiledGate, GateAux, or GateError: those are
blockwatcher-ports; constructing a passthrough: omit gate on the
monitor, or use the ports fake (PassthroughGate) in tests.
Key types and traits
| Name | Kind | Role |
|---|---|---|
threshold::ThresholdGate | struct | Gate impl: N hits spanning ≤ window_ms of event time fire once, then the bag resets (threshold.rs) |
threshold::Registry | struct | ModuleRegistry impl exposing NAME = "threshold" (threshold.rs) |
max_once::MaxOnceGate | struct | Gate impl: at most one alert per event-time window (max_once.rs) |
max_once::Registry | struct | ModuleRegistry impl exposing NAME = "max_once" (max_once.rs) |
registry::gates::get_all | fn | Family enumeration folding both modules into a factory lookup table (registry.rs) |
Neighbours
blockwatcher-gates depends on, in production:
blockwatcher-types: vocabulary crateblockwatcher-ports: theGateport andGateErrorserde,serde_json: config (de)serialization
The following crate depends on it directly (per the dependency table):
blockwatcher-embed: foldsget_all()intobuild_catalog
Using the crate without the engine
blockwatcher-gates depends on blockwatcher-types and blockwatcher-ports
only, never blockwatcher-core. A host can compile and decide without the
binary. Persist, prune, and Match mint stay the host’s (or core’s) job.
#![allow(unused)]
fn main() {
use blockwatcher_gates::threshold::ThresholdGate;
use blockwatcher_ports::{Gate, GateCtx, GateDecision, GateHit};
let gate = ThresholdGate;
let compiled = gate.compile(&config, &schemas)?;
let mut journal: Vec<GateHit> = Vec::new(); // host-owned
journal.push(hit);
match gate.on_hit(&compiled, &journal, &GateCtx { last_emit_ts: None }) {
GateDecision::Emit { indices } => {
// host mints Match / Digest from journal[indices]
if let Some(max) = indices.iter().copied().max() {
journal.drain(0..=max);
}
}
GateDecision::Retain => {}
GateDecision::Discard { .. } => {}
}
}
Reading the source
lib.rs: crate contract (engine owns the journal), window bounds, and the sharedblock.timestampcheck.threshold.rs,max_once.rs: one module each, config validation andon_hit.registry.rs: family enumerationbuild_catalogfolds.