Skip to main content

Overview

Posture!Posture!Posture! uses Chrome’s port-based messaging system for real-time communication between extension components. This architecture enables continuous data flow for pose detection updates (~10 messages per second) while maintaining efficient, persistent connections.

Messaging Architecture

Communication Channels

  1. Options ↔ Background: Relay posture detection messages
  2. Popup ↔ Options: Synchronize state and send commands
  3. Background → Content: Forward posture messages to active tab

Port-Based Messaging

What are Ports?

Ports are persistent messaging channels created with chrome.runtime.connect(). Unlike one-time messages (chrome.runtime.sendMessage), ports remain open for continuous bidirectional communication.
Use ports when you need ongoing communication. Use one-time messages for single request/response exchanges.

Port Lifecycle

  1. Connect: chrome.runtime.connect({ name: 'port-name' })
  2. Send: port.postMessage({ data })
  3. Receive: port.onMessage.addListener(callback)
  4. Disconnect: Automatic on page close or manual port.disconnect()

Channel 1: Options → Background → Content

Flow: Posture Detection Messages

Purpose: Send real-time posture updates from Options page to Content script on active tabs.
File: src/pages/Options/Options.tsxThe Options page connects to the Background script on mount:
let portRef = useRef<any>(null);

useEffect(() => {
  // Connect to background script
  portRef.current = chrome.runtime.connect({ name: 'relay-detection' });
}, []);
When posture changes are detected:
function handlePosture(msg: { baseline?: any; posture?: any }) {
  if (msg.baseline) GOOD_POSTURE_POSITION.current = msg.baseline;
  if (msg.posture) {
    // Send posture update via port
    portRef.current.postMessage(msg);
  }
}
Message format:
{ posture: 'good' }  // or 'bad'
Frequency: ~10 messages per second (every 100ms during detection)

Channel 2: Popup ↔ Options

Flow: State Synchronization and Commands

Purpose: Synchronize UI state between Popup and Options page, and send user commands.

Message Actions

ActionDirectionPayloadPurpose
SET_IS_WATCHINGOptions → Popup{ isWatching: boolean }Sync tracking state
SET_IS_PANEL_OPENOptions → Popup{ isPanelOpen: boolean }Sync panel state
TOGGLE_WATCHINGPopup → Options{ isWatching: boolean }Start/stop tracking
RESET_POSTUREPopup → Options(none)Reset baseline
SET_GOOD_POSTURE_DEVIATIONPopup → Options{ GOOD_POSTURE_DEVIATION: number }Update threshold

Message Formats

Posture Messages

// Good posture detected
{ posture: 'good' }

// Bad posture detected
{ posture: 'bad' }

// Set baseline position
{ baseline: 150.5 }  // Y-coordinate in pixels

Command Messages

// Action-based format
{
  action: 'ACTION_NAME',
  payload: { /* action-specific data */ }
}
Examples:
// Toggle tracking
{
  action: 'TOGGLE_WATCHING',
  payload: { isWatching: true }
}

// Reset posture baseline
{
  action: 'RESET_POSTURE'
}

// Update deviation threshold
{
  action: 'SET_GOOD_POSTURE_DEVIATION',
  payload: { GOOD_POSTURE_DEVIATION: 30 }
}

Port vs One-Time Messages

When to use:
  • Continuous communication (pose updates every 100ms)
  • Bidirectional state sync (Popup ↔ Options)
  • Long-lived connections during component lifecycle
Advantages:
  • Persistent connection (no reconnection overhead)
  • Two-way communication
  • Lower latency for frequent messages
Setup:
// Sender
const port = chrome.runtime.connect({ name: 'channel-name' });
port.postMessage({ data: 'value' });

// Receiver
chrome.runtime.onConnect.addListener(function (port) {
  if (port.name === 'channel-name') {
    port.onMessage.addListener(function (msg) {
      // Handle message
    });
  }
});

Background as Message Relay

The Background service worker acts as a message broker because:
  1. Content scripts can’t directly communicate with extension pages (Options/Popup)
  2. Only background scripts can use chrome.tabs.sendMessage
  3. Background scripts persist across page navigations

Relay Implementation

// Background: src/pages/Background/index.js
chrome.runtime.onConnect.addListener(function (port) {
  if (port.name === 'relay-detection') {
    port.onMessage.addListener(function (msg) {
      // Query active tab
      chrome.tabs.query(
        { active: true, currentWindow: true },
        function (tabs) {
          if (!tabs[0]) return;
          // Forward to content script
          chrome.tabs.sendMessage(tabs[0].id, msg);
        }
      );
    });
  }
});
Why not direct Options → Content? Extension pages (like Options) don’t have access to chrome.tabs API - only background scripts do. The relay pattern is necessary for extension architecture.

Connection Error Handling

// Popup.jsx
try {
  port.current = chrome.runtime.connect({ name: 'set-options' });
  port.current.onMessage.addListener(/* ... */);
} catch (error) {
  // Options page not open - show "Open Popup" button
  console.error({ message: `port couldn't connect`, error });
}

Port Disconnection

// Options.tsx
port.onDisconnect.addListener((event) => {
  // Popup closed - no action needed
  // Port will be re-established when Popup reopens
});

Missing Active Tab

// Background/index.js
function handlePostureMessage(msg) {
  chrome.tabs.query(
    { active: true, currentWindow: true },
    function (tabs) {
      if (!tabs[0]) return; // No active tab - silently fail
      chrome.tabs.sendMessage(tabs[0].id, msg);
    }
  );
}
If chrome.tabs.sendMessage is sent to a tab where the content script hasn’t loaded yet (e.g., chrome:// pages where content scripts don’t run), the message is silently dropped. This is expected behavior and doesn’t cause errors.

Badge Updates

The extension badge shows tracking status (“ON” or “OFF”):
// Background/index.js - Initial state
chrome.action.setBadgeText({ text: 'OFF' });

// Options.tsx - When tracking starts
chrome.action.setBadgeText({ text: 'ON' });

// Options.tsx - When tracking stops
chrome.action.setBadgeText({ text: 'OFF' });
Badge updates are triggered by:
  • Options page: handleToggleCamera()
  • Popup commands: TOGGLE_WATCHING action

Performance Considerations

Message Frequency

  • Posture updates: ~10 messages/second (100ms interval)
  • State sync: On-demand (only when Popup opens or state changes)
  • Commands: User-triggered (low frequency)

Port Persistence

// Port created once on mount
useEffect(() => {
  portRef.current = chrome.runtime.connect({ name: 'relay-detection' });
}, []); // Empty dependency array = runs once
Ports remain open until:
  • Component unmounts
  • Page closes
  • Manual port.disconnect()

Memory Management

Ports are automatically cleaned up when pages close. No manual cleanup is required in this extension.

Debugging Messages

Chrome DevTools

  1. Background script: Right-click extension icon → Inspect service worker
  2. Options page: Right-click Options page → Inspect
  3. Popup: Right-click Popup → Inspect
  4. Content script: Regular page DevTools → check Console

Logging Messages

// Log all incoming messages
port.onMessage.addListener(function (msg) {
  console.log('Received:', msg);
  // Handle message...
});

// Log outgoing messages
const message = { posture: 'good' };
console.log('Sending:', message);
port.postMessage(message);

Common Issues

IssueCauseSolution
Port connection failsTarget component not loadedCheck if Options page is open
Messages not receivedWrong port nameVerify port names match (relay-detection, set-options)
Content script silentScript not injectedCheck manifest content_scripts matches pattern
Disconnection errorsPage closed/navigatedAdd onDisconnect listener
  1. Open Background script console
  2. Add breakpoint in handlePostureMessage
  3. Start tracking in Options page
  4. Watch messages flow through Background to Content
  5. Check Content script console for received messages

Build docs developers (and LLMs) love