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:

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:

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.