> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Time & Pacing

> The TimePort — one monotonic clock, one wall clock, and the schedulers pacing is built on.

The `TimePort` is the seam every pacing mechanism in the mobile app is built on: one monotonic clock, one wall clock, and a per-run scheduler for timers and frames.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "TimePort"
        Now[⏱ nowMs<br/>monotonic] --> Pacing[🧺 Coalescer<br/>Publish Gate]
        Epoch[🕰 epochMs<br/>wall clock] --> Session[💾 Session record]
        Sched[🎛 createScheduler] --> Timer[⏲ setTimer / clearTimer]
        Sched --> Frame[🖼 requestFrame]
        Sched --> Every[🔁 every]
    end

    classDef clock fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef bus fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class Now,Epoch clock
    class Sched bus
    class Pacing,Session,Timer,Frame,Every out
```

## Quick Start

<Steps>
  <Step title="Pick the real adapter at boot">
    `createWebTime()` builds a `TimePort` over the browser's clock and frame loop. Pass it as `time:` to `createApp`.

    ```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import { createWebTime } from "praisonai-mobile/adapters/web";

    const booted = await createApp({
      time: createWebTime(),
      // ...the rest of the platform adapters
    });
    ```
  </Step>

  <Step title="Read a monotonic instant">
    Call `time.nowMs()` for delay budgets and elapsed measurements. Call `time.epochMs()` only when a record needs a real timestamp.

    ```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    const startedAt = time.nowMs();      // monotonic: for durations
    const writtenAt = time.epochMs();    // wall clock: for records
    ```

    This is what production now does — `deps.now` is not passed on the real device path, so `time.epochMs()` runs.
  </Step>

  <Step title="Poll or paint through a scheduler">
    Create one scheduler per run, then use `every`, `setTimer` + `clearTimer`, and `requestFrame`. Dispose it at teardown.

    ```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    const scheduler = time.createScheduler();     // one per run
    const stop = time.every(16, () => coalescer.flush());
    scheduler.requestFrame(() => paint());
    // on teardown
    stop();
    scheduler.clearTimer();
    ```
  </Step>
</Steps>

***

## How It Works

The port carries two clocks and a scheduler factory, and each member has one job.

| Member                       | Type                                          | Purpose                                                                                                                                                |
| ---------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `nowMs()`                    | `() => number`                                | **Monotonic** clock. Never goes backwards, never jumps. Use for delay budgets, elapsed-time measurements, the coalescer's `maxDelayMs`.                |
| `epochMs()`                  | `() => number`                                | **Wall clock**. Milliseconds since the Unix epoch. Use when a record needs a real timestamp; `Session.record` writes this. May jump when NTP corrects. |
| `every(ms, cb)`              | `(ms: number, cb: () => void) => Unsubscribe` | Fires the callback **repeatedly** every `ms`. Returns an unsubscribe. Backs readiness polling, the coalescer's flush tick, elapsed-time updates.       |
| `createScheduler()`          | `() => Scheduler`                             | Returns a fresh scheduler bound to this port. **One per run.** Reusing a scheduler across runs carries a closed publish gate into a new answer.        |
| `scheduler.setTimer(cb, ms)` | `(cb: () => void, ms: number) => void`        | Arm a **one-shot** timer. The publish gate uses it as its fallback timeout.                                                                            |
| `scheduler.clearTimer()`     | `() => void`                                  | Cancel the pending timer. A `clearTimer` that does nothing reopens the gate.                                                                           |
| `scheduler.requestFrame(cb)` | `(cb: () => void) => void`                    | Paint on the next animation frame. Backed by `requestAnimationFrame` in the web adapter.                                                               |

***

## Why Two Clocks

One clock measures durations, the other stamps records, and collapsing them reintroduces the bug the port was carved to prevent.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph "Two clocks, one reason"
        A[System clock<br/>NTP-corrected] --> W{Wall clock<br/>epochMs}
        A --> N{Monotonic clock<br/>nowMs}
        W --> WR[💾 Record when the turn ended]
        N --> NR[🧺 How long has this buffer been open?]
    end

    classDef sys fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef wall fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef mono fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef use fill:#10B981,stroke:#7C90A0,color:#fff

    class A sys
    class W wall
    class N mono
    class WR,NR use
```

When the phone's clock is corrected mid-turn — a common event on a wake from sleep — a wall-clock elapsed measurement can jump backwards. The publish gate's `MAX_HELD_CHARS` and the coalescer's `maxDelayMs` are both measured against `nowMs`, so pacing stays sane through a correction. Records that need to be sortable across devices use `epochMs`.

### The app now reads the port, not `Date.now()`

`main.ts` now reads the wall clock through `TimePort.epochMs` and paces the announcer with the monotonic `nowMs` — until this landed the shipping app read `Date.now()` for both, and `TimePort.epochMs` was implemented, conformance-tested, and had **zero callers**.

Every wall-clock read in `main.ts` was `Date.now()`: the chat record's `updated`, the chats-screen "now" argument to `buildChatList`, and — crucially — the `nowMs` passed to the screen-reader announcer.

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
// main.ts — the wall clock resolves through the port on the real device path.
const writtenAt = nowEpochMs();       // platform.time.epochMs() in production
const paced = announce(state, chunk, platform.time.nowMs()); // monotonic
```

The announcer's rate-limit `nowMs - lastStreamAtMs >= ANNOUNCE_INTERVAL_MS` is an **elapsed** comparison — exactly the conflation `core/src/ports/time.ts` says in as many words must not be fed a wall clock: *"Conflating them is how a clock correction mid-stream makes a coalescer wait forever."*

<Warning>
  Feeding `Date.now()` into the announcer's elapsed comparison means a backwards NTP correction mid-answer silences every further streaming announcement until real time catches up with the pre-correction reading — a screen-reader user simply stops being told what the model is saying. The fix: wall clock comes from `nowEpochMs()` (which resolves to `platform.time.epochMs()` in production, or `deps.now` when passed in tests), and the announcer's `nowMs` is `platform.time.nowMs()`.
</Warning>

***

## Executable Specification

The port's first conformance suite runs the same cases against the fake clock and the web adapter, so the two cannot drift.

| Test                                                                           | Asserts                                                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nowMs is monotonic`                                                           | `nowMs()` never returns a smaller value than the previous call. The coalescer's delay budget depends on it.                                                                                                                                                                                       |
| `epochMs is wall time and nowMs is not` (real-clock only)                      | `epochMs() > 1_600_000_000_000`; `nowMs() < 1_600_000_000_000`. An adapter that returns `Date.now()` for both compiles, passes every other case, and reintroduces the NTP-jump bug.                                                                                                               |
| `every() repeats, rather than firing once`                                     | Fires **≥ 2 times** across three periods. Guards the `setInterval → setTimeout` mutation that ships polling loops that fire exactly once.                                                                                                                                                         |
| `the unsubscribe actually stops it`                                            | After `stop()`, no further callbacks. Guards a leaked interval that keeps the event loop alive for the app's lifetime. Now also pinned against hollowing by the `time_unsubscribe_does_nothing` break mode — see [Adapter Conformance](/docs/features/mobile/adapter-conformance#the-ten-break-modes). |
| `a cleared timer does not fire`                                                | After `clearTimer()`, the timer never fires. Guards a `clearTimer` that does nothing, which reopens the publish gate after it was deliberately shut.                                                                                                                                              |
| `an uncleared timer does fire`                                                 | The pair to the previous case; guards a `setTimer` that never fires and disables the gate's only escape from a stalled renderer.                                                                                                                                                                  |
| `requestFrame runs its callback`                                               | The callback runs within a normal wait. Backed by `requestAnimationFrame` in the web adapter.                                                                                                                                                                                                     |
| `schedulers are independent`                                                   | Clearing one scheduler's timer must not clear another's. Enforces the "one per run" rule.                                                                                                                                                                                                         |
| `the WALL CLOCK is read through TimePort.epochMs, not Date.now`                | A `Platform` whose `epochMs` returns a fixed value stamps that value on the chat record. The production path is exercised — no `now` dep is passed.                                                                                                                                               |
| `streaming announcements are paced by the MONOTONIC clock, not the wall clock` | A monotonic clock that advances during a stream that never ends still lets both finished sentences reach the polite live region — a wall-clock `Date.now()` there rate-limits the second announcement out.                                                                                        |

<Note>
  The fake and the real adapter run the **same** cases. `every`'s period was found discarded in the fake in two separate rounds; a test that only ever drives the fake cannot see it a third time.
</Note>

<Note>
  Each of these cases is also driven by the contract fixture — `time_every_fires_once` and `time_clear_does_nothing` — so a case that lost its assertion is caught by the assertion no longer failing when the adapter is deliberately broken. See [Adapter Conformance](/docs/features/mobile/adapter-conformance).
</Note>

<Warning>
  If you write a new time adapter (React Native, a different desktop shell), the contract needs a `requestAnimationFrame` shim in Node — the web adapter targets a browser. Without the shim `requestFrame` throws `ReferenceError` and the adapter cannot be contract-tested at all.
</Warning>

***

## Common Patterns

The coalescer's flush tick is a repeating `every`, stopped at dispose.

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
const stop = time.every(maxDelayMs, () => coalescer.flush());
// on dispose
stop();
```

The publish gate arms a one-shot timer and clears it when a frame arrives.

```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
const scheduler = time.createScheduler();
scheduler.setTimer(forcePaint, UNPAINTED_REOPEN_MS);
// on frame arrival
scheduler.clearTimer();
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Read the monotonic clock for durations, the wall clock for records">
    Collapsing them makes elapsed measurements jump when the OS corrects the clock.
  </Accordion>

  <Accordion title="One scheduler per run, disposed with the run">
    A scheduler that outlives its run carries closed gates into the next answer. With the tick now gated, a scheduler reused across runs really can carry a closed gate into the next answer — the risk is no longer theoretical.
  </Accordion>

  <Accordion title="every() returns an unsubscribe, and you must call it">
    A leaked interval keeps the event loop alive for the app's lifetime.
  </Accordion>

  <Accordion title="Pair every setTimer with an eventual clearTimer on cancel">
    A gate that stays armed after a cancel forces a paint that no longer belongs.
  </Accordion>

  <Accordion title="Test pacing against a fake clock that actually advances">
    A `TimePort.every` fake that never fires the tick makes every pacing test pass for the wrong reason.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Mobile Architecture" icon="sitemap" href="/docs/features/mobile/architecture">
    The coalescer and publish gate the port paces.
  </Card>

  <Card title="Shell & Adapters" icon="mobile-button" href="/docs/features/mobile/shell-and-adapters">
    The sibling port every conformance suite pins.
  </Card>
</CardGroup>
