Event-time vs processing-time
The assumption you didn't know you were making.
The missing concept
Every RxJS time operator — timer, debounceTime,
bufferTime, windowTime, throttleTime — reads
the wall clock. XState's after transition calls setTimeout.
Neither operator ever reads a ts field off the payload.
This is processing-time: time is measured from when the event arrived at the processor, not from when the event said it occurred.
The key insight: processing-time is the degenerate case
of event-time where ts == arrival. RxJS and XState didn't
choose processing-time; they assumed time == now, and that
assumption is invisible until events are delayed, reordered, batched, or replayed —
then they silently produce wrong answers.
No exception is thrown. No assertion fires. The operators run to completion and return a result that looks correct but isn't.
Proof: the silent wrong answer
The validation suite at tflo-react/validation/scenario-1-abandoned-flow/rxjs-naive.mjs
contains the idiomatic RxJS recipe for detecting an abandoned checkout flow.
Feed it an event log where the events arrive out of order (as happens with offline beaconing,
batched analytics, or multi-device journeys) and it returns [] — no
detected abandonment. No error. No warning. The empty array looks like "no abandonment
occurred," which is wrong.
The bug is not in the RxJS code. The bug is in the assumption: the operator measured "500 ms of wall-clock silence" when the question was "did the user abandon within 5 minutes of event-time?" Those are different questions and RxJS can only answer the first one.
The purity insight
tflo's engine is a pure function. Given the same three inputs it produces the same output, always:
output = f(event_log, rule, clock)
The clock is an injected input, not a runtime call to Date.now() or
std::time::Instant::now(). A run is therefore byte-identical across replay
and across tiers (browser, edge, server). What varies is what you pass in:
| Change this input | You get |
|---|---|
| Vary the rule (spec) | What-if / backtest: "Would rule B have fired on last week's data?" |
| Truncate the log at time T | Roll back: "What did the engine know as of T?" |
| Extend the log with new events | Roll forward: re-derive with corrections applied |
| Swap the clock (wall → synthetic) | Simulation / accelerated replay without touching the rule |
Two different questions
// Processing-time: "Did the user click again 500ms after I started waiting?"
debounceTime(500) // measures ms on the scheduler's wall clock
// Event-time: "Did checkout complete within 5 minutes of starting —
// even if events arrived late, replayed, batched, or offline?"
Pattern::new()
.step("start", |e| e.event == "checkout_begin")
.step("complete", |e| e.event == "checkout_done")
.within(5 * 60_000) // measured in event-time milliseconds off the payload When event-time actually matters
This is the honest wedge — not every application needs event-time semantics. It matters when:
- Offline replay: events are beaconed or batched and arrive after they occurred
- Beaconed analytics: mobile or edge devices flush queued events on reconnect
- Multi-device journeys: a user starts on mobile and finishes on desktop; events from both arrive interleaved
- Backfill: you receive a historical dataset and need to re-derive signals as they would have fired in real time
- Client-side abuse detection: the client controls event timing; relying on arrival order is a vulnerability
If your events are always delivered in order within milliseconds of occurring and you never replay them, processing-time is fine. The problem is that assumption stops being true gradually and silently.
The validation suite
The tflo-react/validation/ directory runs 16 reproductions across three scenarios:
- Scenario 1 — Abandoned flow: detect a checkout that started but did not complete within a time window; events arrive out of order. The React/Signals/RxJS/XState paths each reach the correct answer only by hand-writing event-time logic; each also includes a naive variant that silently returns the wrong answer.
- Scenario 2 — Multi-step sequence: A then B within T, with late arrivals. Tests the reorder buffer and "within" semantics under delayed events.
- Scenario 3 — Absence detection: A without B within T. The hardest case for processing-time tools because "nothing happened" must be derived from event-time evidence, not from wall-clock silence.
Each reproduction self-asserts: it runs its own check and either passes or prints a failure with the expected vs actual output. The suite is runnable without any tflo dependency.
Next: Provable temporal logic — how clock injection makes the late-event case a first-class test, not a blind spot.