Filters and Streams
Query filters for Woven event streams — match by space, kind, author, refs, and time windows.
Purpose
This page covers EventFilter, the predicate used to narrow event reads from EventStream::query, NexusEventFeed::query, and WovenTransportService::query. Read this when building a timeline, a topic feed, a reply tree, or any other view that needs to slice a Strand of signed events.
The filter contract
EventFilter is a builder over five sets and two optional timestamp bounds:
| Field | Matches when |
|---|---|
spaces | event's SpaceId is in the set (empty = wildcard) |
kinds | event's kind is in the set (empty = wildcard) |
authors | event's author string is in the set (empty = wildcard) |
references | every ref in the set appears in event.refs() (AND semantics) |
since | event.created_at() >= since |
until | event.created_at() <= until |
EventFilter::matches(event) returns true only when every populated field passes. Empty fields are wildcards, so EventFilter::new() matches every signed event.
Building a typed timeline
use woven::{EventFilter, EventStream, SpaceId};
let filter = EventFilter::new()
.space(SpaceId::new("woven://dsocial/global")?)
.kind("social.post")
.author("did:l1fe:agent:alice")
.since(1_777_000_000);
let strand = strand.lock().await;
let posts = EventStream::query(&strand, &filter).await?;This returns every social post Alice authored in the global dSocial space since the supplied Unix timestamp.
Walking a reply tree
To follow replies and reactions, include the parent event's EventRef in the filter — Woven returns only events whose refs() contain it:
use woven::{EventFilter, EventRef};
let parent = EventRef::new("woven:event:abc123...")?;
let filter = EventFilter::new()
.references(parent)
.kind("social.reply");
let replies = EventStream::query(&strand, &filter).await?;Subscription semantics
Filters are pure predicates — they do not subscribe to or block on new events. To consume events as they arrive over the network, wire the same filter into a WovenTransportService::handle_swarm_event loop and re-query the local Strand on each accepted payload. The combination gives you a "filter once at load, then incrementally append" pattern without polling.
loop {
let swarm_event = inbound.recv().await?;
if let Some(record) = service.handle_swarm_event(swarm_event).await? {
if filter.matches(&record.event) {
tx.send(record).await?;
}
}
}Determinism
Two peers running the same filter over the same set of input Strands produce identical match sets: filter evaluation is purely structural, references use BTreeSet ordering, and timestamps are second-resolution integers. There is no language- or platform-dependent ordering.
Primary types to know
EventFilter— the builder and predicateEventStream::query— apply a filter to a single StrandNexusEventFeed::query— apply a filter to a merged multi-writer viewWovenTransportService::query— apply a filter to the local transport Strand