Skip to content

ASomerN/df_messenger_sample

Repository files navigation

Multi-Tab Chat Coordination

Ensures only one browser tab owns an active chat widget at a time. Other tabs detect the active session and offer to transfer it — preserving full conversation history across the handoff.

The Problem

Embedded chat widgets (like Dialogflow CX Messenger) store state in localStorage, which is shared across tabs. If two tabs load the widget simultaneously, they write to the same keys and corrupt each other's state. There's no built-in mechanism to prevent this.

What This Demonstrates

A single-owner coordination layer that sits between the "Live Chat" button and the chat widget. It uses a two-tier detection strategy:

Tier 1: BroadcastChannel (real-time)

Direct tab-to-tab messaging with liveness proof.

Tab B clicks "Live Chat"
  → broadcasts "who-owns-chat?"
  → Tab A responds "i-own-chat" within 1000ms
  → Tab B shows "Transfer chat here" prompt
  → user clicks transfer
  → Tab A injects "Transferring chat to new tab" system message
  → Tab A destroys widget (localStorage persists)
  → Tab B creates widget, loads full history from localStorage

If no tab responds within 1000ms (owner tab crashed/closed), Tab B claims ownership freely.

Tier 2: localStorage Fallback

When BroadcastChannel is unavailable (older browsers, cross-origin iframes):

  • Detects recent activity via timestamps in localStorage
  • Shows a modal: "Start chat again" (fresh) or "I don't want to chat" (dismiss)
  • Activity older than 5 minutes is treated as stale and ignored

Stale State Self-Healing

No beforeunload/unload cleanup — those events are unreliable. Instead, all state evaluation happens at click time. Stale ownership from crashed tabs resolves automatically through timeouts (Tier 1) or staleness checks (Tier 2).

Architecture

src/
  index.js              Entry point — wires "Live Chat" button with re-entry guard
  chat-coordinator.js   Orchestrator — Tier 1 + Tier 2 logic, transfer flow
  chat-ownership.js     localStorage state — claim, release, staleness checks
  chat-ui.js            DOM layer — widget lifecycle, modals, message passthrough

Coordinator decides what to do. Ownership tracks who owns chat. UI manages the DOM. The coordinator never touches localStorage directly for messages — it goes through ChatUI.addMessage() → widget API → storage.

Chat Message Persistence

Messages are stored in localStorage under the chatMessages key as a JSON array:

[
  { "sender": "user", "text": "Hello", "timestamp": 1740567890000 },
  { "sender": "bot", "text": "Hi there!", "timestamp": 1740567891000 },
  { "sender": "system", "text": "Transferring chat to new tab", "timestamp": 1740567892000 }
]
  • Transfer preserves historyrelease() leaves chatMessages intact
  • "Start chat again" clears historyclearStorage() removes everything
  • System messages (like transfer notifications) use the same storage path as user/bot messages

Running It

npm install
npm run dev          # starts dev server at localhost:3000

Open http://localhost:3000/test-page/ in two tabs. Click "Live Chat" in Tab A, type some messages, then click "Live Chat" in Tab B to see the transfer flow.

Tests

npm test             # 36 unit tests (Vitest + jsdom)
npm run test:e2e     # 13 E2E tests (Playwright + Chromium, multi-tab)
Suite Tests What it covers
chat-ownership.test.js 11 claim/release, staleness, localStorage state
chat-ui.test.js 16 widget lifecycle, modals, addMessage passthrough
chat-coordinator.test.js 9 Tier 1/2 flows, transfer message injection order
tier1-broadcast.spec.js 5 Multi-tab BroadcastChannel coordination, chat history transfer
tier2-fallback.spec.js 5 Fallback modal behavior without BroadcastChannel
stale-state.spec.js 3 Recovery from crashed tabs, stale ownership

Key Design Decisions

Decision Rationale
localStorage over sessionStorage sessionStorage is per-tab — can't detect cross-tab state
No beforeunload cleanup Unreliable across browsers; all checks happen at click time
1000ms probe timeout Accounts for background tab throttling in modern browsers
2s transfer timeout Handles frozen/suspended tabs during handoff
Tier 2 is awareness-only Without real-time messaging, can't guarantee single-owner — shows advisory modal instead
Transfer message via widget API Coordinator never writes to chatMessages directly; uses ChatUI → widget chain

Tech Stack

  • Vanilla JS (ES modules, no framework)
  • Vitest + jsdom for unit tests
  • Playwright + Chromium for multi-tab E2E tests
  • serve for local dev server

About

Sample for df_messenger single tab experience

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors