Quick Start
Get tflo running in your project in under 5 minutes. The Rust crate powers server-side streaming pipelines; the TypeScript SDK wraps the same engine (compiled to WebAssembly) for browser event analytics. Pick your language.
Use the Rust crates for server-side streaming pipelines, edge gateways,
embedded services, or batch / replay over historical data. The same crate
compiles to WebAssembly for browser indicators (tflo-wasm)
and browser pattern matching (tflo-cep-wasm).
Prerequisites
- Rust 1.88+ (MSRV). Install with rustup.
- An existing Rust project, or create one with
cargo new my-tflo-app.
Installation
Add the core crate to your Cargo.toml:
[dependencies]
# tflo is pre-1.0 and not yet on crates.io — depend on it via git.
tflo-core = { git = "https://github.com/matt-cochran/tflo" }
tflo-ops = { git = "https://github.com/matt-cochran/tflo" } For async stream support, add the async feature:
[dependencies]
tflo-core = { git = "https://github.com/matt-cochran/tflo", features = ["async"] } Your First Computation
tflo turns any Rust Iterator of domain events into a streaming computation graph.
Here’s a simple moving average that smooths a noisy stream:
//! Full runnable example: tflo-examples/examples/docs-quick-start
use tflo_core::prelude::*;
use tflo_ops::prelude::*;
// A noisy domain event — e.g. a sensor reading or detection confidence.
#[derive(Clone)]
struct Reading {
ts: i64,
value: f64,
}
fn main() {
let readings = vec![
Reading { ts: 1000, value: 100.0 },
Reading { ts: 2000, value: 101.0 },
Reading { ts: 3000, value: 99.0 },
Reading { ts: 4000, value: 102.0 },
Reading { ts: 5000, value: 103.0 },
];
let smoothed: Vec<f64> = readings.into_iter()
.tflo(|t| {
t.timestamp(|x| x.ts);
let value = t.prop(|x| x.value);
value.sma(3_u64.secs())
})
.collect();
println!("{:?}", smoothed);
} What’s happening here:
.tflo(|t| { ... })builds a computation graph over your iterator.t.timestamp(|x| x.ts)tells tflo which field is the event timestamp.t.prop(|x| x.value)extracts the numeric property for computation.value.sma(3_u64.secs())computes a 3-second Simple Moving Average.- The output is a
Vec<f64>— one value per input event.
Multiple Outputs
Want SMA and EMA simultaneously? Just compute both in the same graph:
let results: Vec<(f64, f64)> = readings.into_iter()
.tflo(|t| {
t.timestamp(|x| x.ts);
let value = t.prop(|x| x.value);
let sma = value.sma(3_u64.secs());
let ema = value.ema(3_u64.secs());
(sma, ema)
})
.collect(); Enrichment Pattern
Use .with() instead of .tflo() to keep the original event alongside computed values:
let enriched: Vec<(Reading, f64)> = readings.into_iter()
.with(|t| {
t.timestamp(|x| x.ts);
let value = t.prop(|x| x.value);
value.sma(3_u64.secs())
})
.collect(); Keyed Execution (Per-Source)
When events arrive from many emitters — per sensor, per host, per instrument —
use .tflo_keyed() to give each key its own isolated state:
//! Full runnable example: tflo-examples/examples/docs-quick-start
use tflo_core::prelude::*;
use tflo_ops::prelude::*;
// Events from many emitters — per-sensor, per-host, per-instrument.
#[derive(Clone)]
struct Reading {
source: String,
ts: i64,
value: f64,
}
let results: Vec<Result<_, TFloError>> = readings.into_iter()
.tflo_keyed(
|t| t.source.clone(), // key function — partition by source
OutOfOrderPolicy::Error, // fail on out-of-order timestamps
|t| {
t.timestamp(|x| x.ts);
let value = t.prop(|x| x.value);
value.sma(10_u64.secs())
},
)
.collect();
// Each source gets independent window state.
// See tflo-core/src/keyed.rs for the full API. Next Steps
- Read the Core Concepts to understand the graph model.
- Explore the available Operations & Indicators.
- Learn about Signal Detection.
- For multi-event patterns, see Event Patterns.
- See the full Crate Architecture.
The TypeScript SDK
@tflo/browser-events
wraps tflo-cep-wasm with a capture layer
(addEventListener + IntersectionObserver), a
pluggable Sink interface, and a consent gate. The matching
engine is the same Rust engine::Runtime the Rust crate
uses — just with JS callbacks. Total wire weight: ~25 KB gzip.
Prerequisites
- Node 18+ (for development tooling).
- A bundler that handles WASM imports — Vite, Webpack 5, Rollup, and Astro all work out of the box.
- A browser app, dev server, or static site to load the bundle.
Installation
npm install @tflo/browser-events
The package vendors the WebAssembly bytes and the wasm-bindgen JS glue
produced by wasm-pack --target web. Bundlers load the
.wasm file automatically; no extra config required.
Initialize
Construct a TFloBrowser, register the sinks you want to
route signals to, and call await tflo.init() once on app
startup to load the WASM bundle:
import {
TFloBrowser,
Pattern,
ConsoleSink,
EdgeSink,
type EventRecord,
} from "@tflo/browser-events";
const tflo = new TFloBrowser({
sinks: [
new ConsoleSink(), // dev visibility
new EdgeSink({ name: "edge", endpoint: "/collect" }),
],
consent: () => localStorage.getItem("analytics_consent") === "yes",
});
// Load the ~40 KB WASM bundle.
await tflo.init();
Sinks are name-keyed and isolated — one sink's failure doesn't
block delivery to the others. The consent callback gates
outbound delivery; signals are still captured and matched locally even
when consent is denied.
Capture browser events
Three capture helpers ship in v0.1: capture(...) for
general DOM events, captureViewport(...) for
IntersectionObserver-backed time-on-section tracking, and
ingest(record) for events you collect yourself.
// Capture clicks with a custom payload extractor.
tflo.capture<MouseEvent>({
target: document,
type: "click",
fields: (e) => {
const el = (e.target as HTMLElement | null)
?.closest("[data-analytics-action]");
return el
? { action: el.dataset.analyticsAction, cartId: el.dataset.cartId }
: {};
},
});
// Track time on each <section data-section-id="…"> via IntersectionObserver.
// Emits viewport:enter, viewport:exit, and viewport:dwell (with durationMs).
tflo.captureViewport({
target: document,
selector: "section[data-section-id]",
threshold: 0.5,
});
Each capture call returns an unbind function. The
viewport tracker emits a trailing viewport:exit and
viewport:dwell for any element still visible at unbind
time — so the last section the user was reading still ships its
duration when they navigate away.
Derive signals with patterns
Patterns turn streams of captured events into typed domain signals.
The six-method API mirrors the Rust crate
(timestamp / when / then /
notThen / within / emit):
// "Added to cart but didn't purchase within 5 minutes."
const abandonedCart = new Pattern<EventRecord>("abandoned_cart")
.timestamp((e) => e.ts)
.when((e) => e.fields.action === "add_to_cart")
.notThen((e) => e.fields.action === "purchase")
.within(5 * 60 * 1000)
.emit((m) => ({
name: "abandoned_cart",
payload: { cartId: m.first().fields.cartId },
sinks: ["edge"],
}));
tflo.addPattern(abandonedCart);
// Flush on page hide so end-of-session signals ship via sendBeacon.
window.addEventListener("pagehide", () => void tflo.flush()); Bounded by construction: in-flight partial matches are capped per pattern, and end-of-stream resolves any pending negative-match deadlines so cart-abandonment fires reliably even on tab close.
For a positive multi-event pattern ("three identical clicks within
one second each"), chain explicit .then().within() pairs.
The quantifier sugar .repeated(n..=m, ...) lands in v0.2.
// "Three identical clicks on the same target within 1 second each."
const rageClick = new Pattern<EventRecord>("rage_click")
.timestamp((e) => e.ts)
.when((e) => e.kind === "click" && e.fields.targetId === "buy_button")
.then((e) => e.kind === "click" && e.fields.targetId === "buy_button")
.within(1_000)
.then((e) => e.kind === "click" && e.fields.targetId === "buy_button")
.within(1_000)
.emit((m) => ({
name: "rage_click",
payload: { targetId: m.first().fields.targetId },
})); Route to any sink
The matching engine has zero analytics-vendor knowledge. The
Sink interface is the plug-in seam — bundled
implementations include ConsoleSink, EdgeSink
(HTTP POST with batching + sendBeacon fallback), and
GA4Sink. Adding Segment, Mixpanel, or your own warehouse
is a fifty-line class:
import type { Sink, DerivedSignal } from "@tflo/browser-events";
class SegmentSink implements Sink {
readonly name = "segment";
send(signal: DerivedSignal): void {
window.analytics?.track(signal.name, signal.payload);
}
}
new TFloBrowser({
sinks: [new SegmentSink(), new EdgeSink({ endpoint: "/collect" })],
}); Next Steps
- Read the browser analytics walkthrough for the full end-to-end workflow.
- The event patterns reference has side-by-side Rust + TypeScript examples for every primitive.
- See the blog post on capturing user interaction data with tflo in the browser.
- Companion repo: @tflo/browser-events on GitHub.