# Build an External Store

> Build the external store that powers a live dashboard: immutable snapshots, subscribe/getSnapshot, polling, stale-response protection, and pause/resume with cleanup.

- Difficulty: intermediate
- Tags: useSyncExternalStore, external store, subscriptions, polling, timers
- Estimated time: 90 minutes
- Tests: 18
- Requires subscription: yes

## Overview

Build the external metrics store — an in-memory object on the client that holds the current state — that powers the provided dashboard, implementing the `subscribe`/`getSnapshot` contract, polling, pause/resume, and stale-response protection.


## Requirements

- `createMetricsStore(fetcher)` returns an object with `subscribe`, `getSnapshot`, `start`, `refresh`, `pause`, and `resume`.
- The initial snapshot is `{ metrics: [], updatedAt: null, status: "idle", error: null }`.
- `getSnapshot` returns the same object reference until the state changes.
- `subscribe(listener)` registers a listener and returns a function that removes it.
- Subscribers are notified after every state change.
- `start()` fetches immediately, then every five seconds, and returns a cleanup function that stops polling.
- Calling `start()` while polling is already active does not create a second interval.
- While a request started by `refresh()` is in flight and the store is not paused, the status is `loading`.
- When the store is not paused, `refresh()` ends in `live` with the new metrics and `updatedAt`, or in `error` with the message.
- When a request fails, the last successful metrics remain in the snapshot.
- After an error, the next poll retries the request.
- When two requests overlap, only the response from the latest request is applied.
- `pause()` stops polling, sets the status to `paused`, and ignores an in-flight response.
- `resume()` fetches immediately, restarts polling, and does nothing if the store is not paused.
- A manual `refresh()` while paused updates the data but keeps the status `paused`.
- The `dashboardStore` singleton is exported and used by the provided `App.tsx`.


## Notes

- This challenge builds the store consumed in `069-live-dashboard`. The provided `App.tsx` is already wired, so read it first to see the exact API the store must expose.
- The state has to live outside React, but `getSnapshot` is compared by reference. Think about what that means for every update.
- There are two clocks in this exercise: the polling interval and the request latency. Consider what happens when one finishes while the other is still running.
- React StrictMode mounts effects twice in development. Keep that in mind when an effect starts a timer.
- Pausing is not only about stopping the interval; think about what to do with a request that is already in flight.
- A listener can unsubscribe while others remain; the notification mechanism should handle that safely.
- APIs: [`useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore), [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval), [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API).


## Tests

01. exports the dashboardStore singleton
02. renders the provided dashboard with the store
03. starts with an idle snapshot and a stable reference
04. notifies subscribers when the state changes
05. stops notifying after unsubscribing
06. fetches immediately when started
07. shows a loading status while a request is in flight
08. polls every five seconds
09. does not create duplicate intervals when start is called twice
10. stops polling after stop is called
11. keeps the last metrics when a request fails
12. recovers on the next poll after an error
13. discards a stale response that arrives after a newer one
14. pauses polling and marks the dashboard as paused
15. ignores an in-flight response that arrives after pausing
16. resumes with an immediate fetch and restarts polling
17. stays paused when refreshed manually while paused
18. keeps the snapshot reference stable between updates


