How the Favorites System Saves and Organizes WebSocket Messages in DevTools

The favorites system in law-chain-hot/websocket-devtools implements a three-tier architecture where FavoritesService manages localStorage persistence with debounced notifications, GlobalFavorites exposes convenience methods for adding items, and the FavoritesTab React component handles UI rendering with a strict 5-item limit enforcement.

The law-chain-hot/websocket-devtools repository provides browser-based debugging utilities for WebSocket connections. The favorites system enables developers to persist frequently used JSON payloads across browser sessions, eliminating the need to retype complex message structures during repetitive testing workflows. This implementation combines a service-oriented JavaScript backend with a React frontend to deliver real-time CRUD operations with immediate UI feedback.

Core Architecture: Three-Layer Design

The implementation separates concerns across three distinct layers to maintain clean boundaries between storage, business logic, and presentation.

FavoritesService (Low-Level Storage)

Located at /src/utils/favoritesService.js, this class handles all localStorage interactions and state management. It exposes methods for creating, reading, updating, and deleting favorites while managing a Set of listener callbacks for UI updates. The service uses the storage key websocket-favorites to serialize the favorites array as JSON:

// src/utils/favoritesService.js
constructor() {
  this.storageKey = "websocket-favorites";
  this.listeners = new Set();
}

getFavorites() {
  const saved = localStorage.getItem(this.storageKey);
  return saved ? JSON.parse(saved) : [];
}

saveFavorites(favorites) {
  localStorage.setItem(this.storageKey, JSON.stringify(favorites));
  this.notifyListeners(favorites);
}

GlobalFavorites (Convenience Facade)

The /src/utils/globalFavorites.js module provides a simplified API for UI components. It wraps FavoritesService with helper methods like quickAdd(), addFromEditor(), and addSilently() while managing tab-switch callbacks. When switchToFavoritesTab is enabled in the options, it triggers the UI navigation logic:

// src/utils/globalFavorites.js
const result = favoritesService.addFavorite(
  { name, data: messageData },
  { switchToFavoritesTab: true, generateName: true, autoEdit: false }
);

FavoritesTab (React UI Layer)

The /src/components/FavoritesTab.jsx component subscribes to FavoritesService events via the listener pattern. It renders the searchable list, handles inline editing, and invokes parent callbacks for sending or receiving messages. The component respects the maximum item limit and displays warning tooltips when necessary.

localStorage Persistence and Data Structure

All favorite messages persist to the browser's localStorage under the key websocket-favorites. Each favorite object follows a strict schema with generated IDs and ISO timestamps:

// Data structure created in FavoritesService.addFavorite()
const newFavorite = {
  id: Date.now().toString(),           // Unix timestamp as string
  name: name.trim(),
  data: data.trim(),                   // JSON payload
  tags: Array.isArray(tags) ? tags : [],
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),
};

When adding a new item, the system prepends it to the array to maintain reverse-chronological order:

const newFavorites = [newFavorite, ...currentFavorites];
saveFavorites(newFavorites);

The 5-Item Limit and Validation Logic

The system enforces a hard limit of 5 favorites to prevent unbounded localStorage growth. When addFavorite() detects the current count exceeds this threshold, it returns an error object and emits a limit_exceeded event:

// src/utils/favoritesService.js
if (currentFavorites.length >= 5) {
  this.notifyListeners(currentFavorites, { 
    type: "limit_exceeded", 
    count: currentFavorites.length 
  });
  return { 
    error: "LIMIT_EXCEEDED", 
    message: "Maximum 5 favorites allowed" 
  };
}

The FavoritesTab component listens for this event to display temporary tooltips and persistent header warnings using the localized message key t("favorites.limit.exceeded.message").

Automatic Name Generation from Payloads

When the generateName option is true, FavoritesService.generateFavoriteName() inspects the JSON payload to create human-readable labels. It attempts to extract the type field:

generateFavoriteName(data) {
  try {
    const parsed = JSON.parse(data);
    if (parsed.type) return `${parsed.type} Message`;
    // Additional parsing logic...
  } catch (e) {
    // Fallback for invalid JSON
  }
  return `Unnamed Message - ${Date.now().toString().slice(-4)}`;
}

This automatic labeling reduces friction when quickly saving messages from the editor without manually typing descriptions.

Event-Driven Updates with Debouncing

To prevent UI flooding during rapid successive operations, FavoritesService implements 100ms debouncing on all notifications. The debouncedNotify() method resets a timeout for each state change, ensuring listeners receive only the final update:

debouncedNotify(favorites, eventData) {
  if (this.notificationTimeoutId) clearTimeout(this.notificationTimeoutId);
  this.notificationTimeoutId = setTimeout(() => {
    this.notifyListeners(favorites, eventData);
  }, 100);
}

React components subscribe via addListener() and receive the updated array plus optional event metadata (such as limit_exceeded types):

// src/components/FavoritesTab.jsx
useEffect(() => {
  const unsubscribe = favoritesService.addListener((newFavorites, eventData) => {
    setFavorites(newFavorites);
    if (eventData?.type === "limit_exceeded") {
      setShowLimitTooltip(true);
      setTimeout(() => setShowLimitTooltip(false), 3000);
    }
  });
  return () => unsubscribe();
}, []);

Practical Implementation Examples

Adding a Favorite from the Message Editor

To capture the current editor content and automatically switch to the Favorites tab:

import globalFavorites from "./utils/globalFavorites";

const messageData = '{"type":"subscribe","channel":"marketData"}';

globalFavorites.addFromEditor(messageData, {
  generateName: true,           // Auto-generate name from JSON type
  autoEdit: true,               // Open edit mode immediately
  switchToFavoritesTab: true,   // Navigate to Favorites tab
});

Programmatically Updating a Favorite

Modify an existing favorite by ID with new payload data:

import favoritesService from "./utils/favoritesService";

favoritesService.updateFavorite("1700000000000", {
  name: "Market Subscribe",
  data: '{"type":"subscribe","channel":"marketData","interval":1000}',
  tags: ["trading", "v1"]
});

Subscribing to Changes in Custom Components

Monitor favorites state changes outside the main tab:

import favoritesService from "./utils/favoritesService";

const unsubscribe = favoritesService.addListener((list, event) => {
  console.log("Favorites updated:", list);
  if (event?.type === "limit_exceeded") {
    console.warn(`Limit reached: ${event.count}/5 favorites`);
  }
});

// Cleanup on unmount
unsubscribe();

Silent Deletion Without UI Feedback

Remove items programmatically without triggering tab switches:

import globalFavorites from "./utils/globalFavorites";

globalFavorites.delete("1700000123456");

Summary

  • The favorites system uses a three-tier architecture separating storage (FavoritesService), API convenience (GlobalFavorites), and UI (FavoritesTab).
  • Data persists to localStorage under the key websocket-favorites with a maximum capacity of 5 items enforced at the service layer.
  • Each favorite receives a unique ID via Date.now().toString() and includes ISO timestamps for createdAt and updatedAt.
  • Debounced notifications (100ms) ensure UI components receive batched updates rather than individual renders for rapid successive changes.
  • Automatic name generation parses JSON payloads to extract type fields for human-readable labels.
  • The system emits typed events such as limit_exceeded that UI components handle to display warnings and tooltips.

Frequently Asked Questions

How does the favorites system persist data across browser sessions?

The system stores all favorites in the browser's localStorage using the key websocket-favorites. The FavoritesService class handles JSON serialization and deserialization, writing the entire favorites array to localStorage on every create, update, or delete operation. This ensures data survives page reloads and browser restarts without requiring a backend server.

What happens when you try to save more than 5 favorites?

When addFavorite() detects the collection already contains 5 items, it returns an error object { error: "LIMIT_EXCEEDED", message: "Maximum 5 favorites allowed" } and emits a limit_exceeded event to all registered listeners. The FavoritesTab component responds by displaying a temporary tooltip for 3 seconds and a persistent warning message in the header indicating the storage cap has been reached.

How does the UI stay synchronized with favorites changes?

The FavoritesService maintains a Set of listener callbacks and invokes them through a debounced notification system that waits 100ms after the last state change. React components like FavoritesTab subscribe via addListener() and update their local state when the service broadcasts changes. This pattern ensures all UI instances display identical data even when favorites are modified from different parts of the application.

Can favorites be added programmatically from outside the main component?

Yes, the GlobalFavorites facade exposes convenience methods such as quickAdd(), addFromEditor(), and addSilently() that any module can import and invoke. These methods accept options flags including switchToFavoritesTab to control UI navigation and generateName to enable automatic labeling from JSON payloads, making it possible to integrate favorites functionality into context menus, keyboard shortcuts, or automated testing scripts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →