Today’s goal
Build front-end systems that remain understandable when messages arrive, networks disappear, and local work must synchronize later.
We will connect:
- polling, long polling, SSE, WebSockets, and WebRTC;
- connection state, ordering, duplication, backpressure, and reconnection;
- cookies, browser storage, IndexedDB, and Cache Storage;
- service-worker lifecycle and caching strategies;
- offline reads, offline writes, outboxes, and synchronization;
- identity, conflicts, eventual consistency, and background sync;
- a field-inspection architecture that works without a permanent connection.
By the end of today you can
- choose the simplest communication model that satisfies the requirement;
- prevent overlapping polls and stale live updates;
- design SSE and WebSocket message envelopes;
- model connection state as user-visible state;
- distinguish transport delivery from application semantics;
- choose browser storage by data meaning and lifetime;
- explain service-worker install, activate, and fetch interception;
- design cache-first, network-first, and stale-while-revalidate policies;
- persist drafts and queued operations offline;
- synchronize with idempotency, retry limits, and conflict detection.
The central lesson
Real-time and offline behavior are not switches; they are explicit policies for communication, persistence, synchronization, identity, and recovery.
The network may be delayed, duplicated, reordered, unavailable, or partially available.
The application must make those conditions meaningful rather than pretending they cannot happen.
Avoid overlapping poll requests
let active: AbortController | undefined;
async function poll() {
active?.abort();
active = new AbortController();
return fetch("/api/updates", { signal: active.signal });
}If a request takes longer than the interval, a naive timer can create concurrent requests and out-of-order responses.
Runtime validation still applies
socket.addEventListener("message", event => {
const value: unknown = JSON.parse(event.data);
const message = parseMessage(value);
if (!message.ok) return showProtocolError(message.error);
applyMessage(message.data);
});TypeScript cannot guarantee that a remote peer sent the expected data.
Backpressure protects the client
If messages arrive faster than the UI or storage can process them, define a policy:
- coalesce updates;
- drop obsolete intermediate states;
- pause subscriptions;
- apply batches;
- request a fresh snapshot;
- show degraded status.
Unbounded queues turn a temporary burst into a memory and responsiveness problem.
localStorage stores strings
localStorage.setItem("settings", JSON.stringify(settings));
const raw = localStorage.getItem("settings");
const value: unknown = raw === null ? null : JSON.parse(raw);Validate versions and shape on read. Data written by the application is still an external runtime boundary after reload.
Cache Storage versus IndexedDB
| Cache Storage | IndexedDB |
|---|---|
| request/response pairs | structured application records |
| asset and HTTP-like retrieval | queries, indexes, transactions |
| service-worker friendly | domain/offline data friendly |
| cache strategy | persistence and synchronization model |
Choose based on data semantics, not only size.
Strategy matrix
| Data | Useful strategy |
|---|---|
| versioned assets | cache-first |
| current account data | network-first / query policy |
| readable offline catalogue | stale-while-revalidate |
| mutation command | network-only plus outbox when offline |
| private durable draft | IndexedDB, not public cache |
The right choice depends on authority, freshness, and recovery.
Offline-capable versus offline-first
offline-capable → core paths work without connectivity when needed
offline-first → local state is the primary interaction modelOffline-first is a larger product commitment involving conflict, identity, local storage, and synchronization.
Do not adopt it for a feature that only needs cached reading.
User identity and offline data
When a user signs out or changes account, decide what happens to local data:
- delete it;
- encrypt or isolate it;
- keep only non-sensitive preferences;
- mark it for a specific identity;
- prevent another account from reading it.
Local persistence must respect authorization boundaries.
Practical project: Offline-Capable Field Inspections
Build a field-inspection application that can read assignments, save drafts, queue completed inspections, and recover when connectivity returns.
The practical combines live communication, persistence, service workers, outbox sync, idempotency, and conflict decisions.
Troubleshooting guide (Part 1)
| Symptom | Likely cause |
|---|---|
| Poll responses arrive out of order | Requests overlap without identity or cancellation |
| Reconnected socket misses updates | No snapshot or event-version recovery |
| Same event changes data twice | Handler lacks idempotency identity |
| Offline work disappears on reload | Only in-memory state was used |
| Service worker is registered but offline fails | No request strategy or local data model |
Troubleshooting guide (Part 2)
| Symptom | Likely cause |
|---|---|
| Old assets break with new worker | Cache versioning and activation are unsafe |
| Duplicate inspection is created | No idempotency key or server deduplication |
| Local data leaks across accounts | Persistence is not scoped or cleared on identity change |
| Sync retries forever | Permanent failures lack a terminal state |
Completion checklist
- the transport matches the actual freshness and direction requirements;
- polling and live connections have cancellation and reconnection policy;
- messages have validation and identity where needed;
- connection state is visible to users;
- storage is chosen by semantics and lifetime;
- service-worker caches are versioned and scoped;
- offline writes are durable before being acknowledged locally;
- outbox operations have IDs, retry limits, and terminal failures;
- synchronization detects duplicates, ordering issues, and conflicts;
- the app remains useful without optional background capabilities.
Misconceptions to leave behind (Part 1)
| Misconception | Better mental model |
|---|---|
| Real-time means WebSocket | Choose the simplest transport that fits |
| Polling is outdated | Polling is often clear and sufficient |
| SSE and WebSocket are the same | Direction and protocol responsibilities differ |
| Reopening a socket solves reconnect | Reconnect also needs resync and recovery |
| Messages arrive exactly once | Design for duplicates and reordering |
localStorage is a database | It is synchronous string storage |
Misconceptions to leave behind (Part 2)
| Misconception | Better mental model |
|---|---|
| App-written local data is trusted | Reloaded storage is a runtime boundary |
| Service-worker registration means offline | Strategies, persistence, and recovery are still needed |
navigator.onLine proves connectivity | It is only a hint |
| Retry and synchronization are identical | Sync reconciles local intent with remote state |
| Background Sync is guaranteed | It is an enhancement, not the foundation |
| Every application should be offline-first | Choose an offline scope from user need |