---
name: mock-state
description: Use when implementing any mock backend behaviour for Phases 1–3 — the BroadcastChannel-backed event bus, in-memory fixture stores, soft-lock timers, AI takeover state. Throwaway by design but must mirror the Socket.IO API so Phase 5 swap is mechanical.
---

# mock-state

Phases 1–3 fake the backend so we can demo every flow without infrastructure. The trick: fake it well enough that switching to Socket.IO in Phase 5 is **one file change, not a refactor**. That means the bus API mirrors Socket.IO event names, payloads come from `packages/shared/src/events/schema.ts`, and round-trips have realistic latency so you discover loading-state bugs early.

## When to use

- Implementing or modifying the BroadcastChannel event bus, seeding fixture data, or simulating server-side timers (soft-lock 30s, agent-disconnect 60s grace) inside `packages/widget` or `packages/agent-app`
- Any throwaway logic that will move to backend in Phase 5+. Skip for `packages/shared` — that is *not* throwaway.

## Do

- Implement the `EventBus` interface from `@indianic/shared` (`packages/shared/src/events/mock-bus.ts`). The widget and agent app each call a `createMockBus()` factory. Backbone is `BroadcastChannel('indianic-mock-bus')`; each emit serializes the typed event and receivers narrow via `event.type`.
- **Simulate latency on every round-trip.** Default jitter window 100–400ms. Use a single helper `withFakeLatency(fn)` so it can be neutered to 0 in tests.
- Generate deterministic fake visitor names from `crypto.randomUUID()` + a paired adjective/noun (`"calm-otter-71"`). Same UUID → same name; debugging across reloads stays sane.
- Keep all mock-state files in a `mock/` folder inside each consuming package, so Phase 5 deletion is `rm -rf packages/*/src/mock`. Persist seeding state in `sessionStorage` (key `indianic.mock.seeded`).
- Wrap every `setTimeout` in `mockTimer.schedule(fn, ms)` (returns a cancel handle); `mockTimer.cancelAll()` is what the demo's "reset mock state" button calls.

## Don't

- Don't put mock code in `packages/shared`. Shared is the part that survives.
- Don't invent event names or payloads not in `schema.ts`. If you need one, add it to the schema first (and document it).
- Don't bypass `withFakeLatency` or fire-and-forget `setTimeout` — instant round-trips hide races; un-cancellable timers break the reset button.
- Don't store mock data in `localStorage` — only `sessionStorage` or in-memory. (The visitor UUID is the one `localStorage` exception, owned by the widget.)
- Don't merge mock-state into stores. Stores hold "what the app knows"; mock-state synthesizes "what a real backend would have told us".

## One short example

```ts
// packages/agent-app/src/mock/bus.ts
import type { BusEvent, BusEventName, BusEventOf, EventBus } from '@indianic/shared';

const CHANNEL = 'indianic-mock-bus';
const LATENCY_MIN = 100;
const LATENCY_MAX = 400;

export function createMockBus(): EventBus {
  const channel = new BroadcastChannel(CHANNEL);
  const listeners = new Map<BusEventName, Set<(e: BusEvent) => void>>();

  channel.addEventListener('message', (ev) => {
    const event = ev.data as BusEvent;
    listeners.get(event.type)?.forEach((fn) => fn(event));
  });

  return {
    emit(event) {
      const delay = LATENCY_MIN + Math.random() * (LATENCY_MAX - LATENCY_MIN);
      setTimeout(() => channel.postMessage(event), delay);
    },
    on(name, listener) {
      const set = listeners.get(name) ?? new Set();
      set.add(listener as (e: BusEvent) => void);
      listeners.set(name, set);
      return () => set.delete(listener as (e: BusEvent) => void);
    },
    once(name, listener) {
      const off = this.on(name, ((e: BusEventOf<typeof name>) => {
        off();
        listener(e);
      }) as never);
      return off;
    },
    close() {
      listeners.clear();
      channel.close();
    },
  };
}
```

## Reference

- Bus interface: `packages/shared/src/events/mock-bus.ts`
- Event schema: `packages/shared/src/events/schema.ts`
- Phase doc: `dev-docs/phase-3-wire-flows.md`
