Skip to main content
The transport layer is a unified interface (SyncTransport) that abstracts the communication mechanism between the POS frontend and the sync backend. Two implementations exist:
  • directTransport — HTTP polling + localStorage outbox (existing behavior, no hub needed)
  • hubTransport — WebSocket connection to the hub server
The active transport is chosen at runtime by a factory function based on bootstrap config.

SyncTransport interface

Defined in nu_pos_react/src/realtime/transport.ts:
All on* methods return an unsubscribe function.

Transport states

Factory: createTransport()

nu_pos_react/src/realtime/createTransport.ts:
The factory reads three fields from the bootstrap config (set in Odoo):
  • nu_hub_enabled (boolean) — Master switch
  • nu_hub_url (string) — WebSocket URL (e.g., ws://10.0.0.5:8766)
  • nu_hub_fallback_ip (string) — IP-only fallback, auto-prefixed with ws:// and port 8766

Direct transport

nu_pos_react/src/realtime/directTransport.ts Wraps the existing sync modules with no behavior change:

Hub transport

nu_pos_react/src/realtime/hubTransport.ts

Lease tracking

  • heldLeases: Set<string> — Order references currently leased by this terminal
  • pendingLeases: Map<string, { resolve, timer }> — In-flight acquire requests
  • Heartbeat timer: sends LEASE_HEARTBEAT for all held leases every 20s

Table lock tracking

  • heldTableLocks: Set<number> — Table IDs currently locked by this terminal
  • pendingTableLocks: Map<number, { resolve, timer }> — In-flight lock requests
  • Separate heartbeat timer: sends TABLE_LOCK_HEARTBEAT for all held locks every 15s

Offline fallback

When connection.send() returns false (WS not open), sendOrderSnapshot() queues the snapshot to the localStorage outbox:

Reconnection events

The hub transport fires onReconnecting and onReconnected callbacks after the initial connection has succeeded at least once (hasConnectedBefore flag). These are wired through to the SyncUIProvider to show a full-screen reconnection overlay. The direct transport provides no-op stubs since it has no WebSocket lifecycle.

Token refresh on AUTH_FAIL

HubTransportConfig uses a dynamic token getter instead of a static token:
When the hub responds with AUTH_FAIL, the connection layer calls onTokenExpired() to attempt a JWT refresh. If the refresh succeeds, it reconnects with the new token (via getToken()). If the refresh token is also expired, it fires onAuthFailed for a permanent logout redirect. A tokenRefreshAttempted flag prevents infinite refresh loops within a single connection cycle.

Visibility change detection

hubConnection.ts listens to document.visibilitychange events. When a tab regains focus after laptop sleep, the browser often leaves the WebSocket in a stale state (TCP connection dead but readyState not updated). The visibility handler force-closes dead sockets to trigger an immediate reconnect instead of waiting 30-90s for the TCP timeout.

Connection state mapping

Hub discovery

nu_pos_react/src/realtime/hub/hubDiscovery.ts Resolution priority:
  1. nu_hub_url from bootstrap — used as-is if starts with ws:// or wss://, otherwise prefixed
  2. nu_hub_fallback_ip from bootstrap — prefixed with ws:// and :8766
  3. null — hub mode disabled, factory returns direct transport

File map