Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-gates implements the Gate port for threshold and max_once. Both depend on blockwatcher-types and blockwatcher-ports only, never blockwatcher-core.
  • Catalog fold is blockwatcher_gates::registry::gates::get_all() in crates/blockwatcher-embed/src/catalog.rs. There is no gates feature: 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 with gate 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 fits window_ms; if len >= count, Emit the oldest count indices; else Retain (threshold.rs).
  • max_once: first hit per event-time window. Config { "window_ms" }. Emit([this]) when last_emit_ts is none or this event_ts is outside the window; otherwise Discard([this]) (max_once.rs).
  • Share window_ms bounds (1..=86_400_000) and the block.timestamp schema check (lib.rs). threshold.count is 2..=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

NameKindRole
threshold::ThresholdGatestructGate impl: N hits spanning ≤ window_ms of event time fire once, then the bag resets (threshold.rs)
threshold::RegistrystructModuleRegistry impl exposing NAME = "threshold" (threshold.rs)
max_once::MaxOnceGatestructGate impl: at most one alert per event-time window (max_once.rs)
max_once::RegistrystructModuleRegistry impl exposing NAME = "max_once" (max_once.rs)
registry::gates::get_allfnFamily enumeration folding both modules into a factory lookup table (registry.rs)

Neighbours

blockwatcher-gates depends on, in production:

  • blockwatcher-types: vocabulary crate
  • blockwatcher-ports: the Gate port and GateError
  • serde, serde_json: config (de)serialization

The following crate depends on it directly (per the dependency table):

  • blockwatcher-embed: folds get_all() into build_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

  1. lib.rs: crate contract (engine owns the journal), window bounds, and the shared block.timestamp check.
  2. threshold.rs, max_once.rs: one module each, config validation and on_hit.
  3. registry.rs: family enumeration build_catalog folds.