| name | panel-breaker-stateful |
| description | Unified dashboard panel pattern combining circuit breaker resilience with enhanced UI features like retry logic, state persistence, and detailed error handling. |
Resilient Dashboard Panel Pattern
This skill merges the best of data-service-merged and panel-component-enhanced to create panels that are resilient to API failures while offering enhanced UI features. Each panel manages its own data fetching with built-in circuit breakers and retry logic, and persists its state to localStorage.
Architecture Overview
- Service Module: Each panel has a dedicated service module for data fetching, using circuit breakers for resilience.
- Panel Class: Extends a base
Panel class with built-in loading/error states, retry logic, and state persistence.
- Data Flow: Panels automatically fetch data, handle errors with retries, and persist UI state.
Implementation
1. Circuit Breaker Utility with Enhanced Logging
Create src/utils/circuit-breaker.ts:
interface CircuitState {
failures: number;
cooldownUntil: number;
}
interface CacheEntry<T> {
data: T;
timestamp: number;
}
export interface CircuitBreakerOptions {
name: string;
maxFailures?: number;
cooldownMs?: number;
cacheTtlMs?: number;
}
export class CircuitBreaker<T> {
private state: CircuitState = { failures: 0, cooldownUntil: 0 };
private cache: CacheEntry<T> | null = null;
private name: string;
private maxFailures: number;
private cooldownMs: number;
private cacheTtlMs: number;
constructor(options: CircuitBreakerOptions) {
this.name = options.name;
this.maxFailures = options.maxFailures ?? 2;
this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;
this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000;
}
isOnCooldown(): boolean {
if (Date.now() < this.state.cooldownUntil) return true;
if (this.state.cooldownUntil > 0) {
this.state = { failures: 0, cooldownUntil: 0 };
}
return false;
}
getCached(): T | null {
if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {
return this.cache.data;
}
return null;
}
recordSuccess(data: T): void {
this.state = { failures: 0, cooldownUntil: 0 };
this.cache = { data, timestamp: Date.now() };
console.log(`[${this.name}] Successfully fetched data`);
}
recordFailure(error?: string): void {
this.state.failures++;
if (this.state.failures >= this.maxFailures) {
this.state.cooldownUntil = Date.now() + this.cooldownMs;
console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);
}
console.error(`[${this.name}] Failure ${this.state.failures}/${this.maxFailures}:`, error);
}
async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {
if (this.isOnCooldown()) {
const cached = this.getCached();
return (cached as R) ?? defaultValue;
}
const cached = this.getCached();
if (cached !== null) return cached as R;
try {
const result = await fn();
this.recordSuccess(result);
return result;
} catch (e) {
this.recordFailure(String(e));
return defaultValue;
}
}
}
export function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {
return new CircuitBreaker<T>(options);
}
2. Base Panel Class with Retry Logic and State Persistence
Create src/components/Panel.ts:
export interface PanelOptions {
id: string;
title: string;
showCount?: boolean;
className?: string;
}
export class Panel {
protected element: HTMLElement;
protected content: HTMLElement;
protected header: HTMLElement;
protected countEl: HTMLElement | null = null;
protected panelId: string;
private _fetching = false;
private retryAttempts = 0;
private maxRetries = 3;
private retryDelay = 1000;
constructor(options: PanelOptions) {
this.panelId = options.id;
this.element = document.createElement('div');
this.element. = ;
... = options.;
. = .();
.. = ;
headerLeft = .();
headerLeft. = ;
title = .();
title. = ;
title. = options.;
headerLeft.(title);
..(headerLeft);
(options.) {
. = .();
.. = ;
.. = ;
..(.);
}
. = .();
.. = ;
.. = ;
..(.);
..(.);
.();
.();
}
(): { .; }
(message = ): {
.. = ;
}
(message = , ?: ): {
.. = ;
(onRetry) {
..()?.(, onRetry);
}
}
(: ): {
.. = html;
}
(: ): {
(.) .. = count.();
}
(): { ...(); }
(): { ...(); }
(: ): { . = v; }
(): { .; }
(: ): <> {
(. < .) {
{
response = (url);
(!response.) ();
response.();
} (error) {
.++;
(. >= .) {
();
}
( (resolve, .));
. *= ;
}
}
}
(): {
.(, .({
: !...(),
: ...,
: ...
}));
}
(): {
savedState = .();
(savedState) {
{ isExpanded, width, height } = .(savedState);
(!isExpanded) ...();
(width) ... = width;
(height) ... = height;
}
}
(): {
..();
}
}
3. Example: StockPanel with Integrated Service
import { Panel } from './Panel';
import { createCircuitBreaker } from '../utils/circuit-breaker';
interface StockQuote {
symbol: string;
name: string;
price: number | null;
change: number | null;
sparkline?: number[];
}
const breaker = createCircuitBreaker<StockQuote[]>({
name: 'StockMarket',
cacheTtlMs: 60_000,
});
export class StockPanel extends Panel {
private refreshTimer: ReturnType<typeof setInterval> | null = null;
constructor() {
super({ id: 'stocks', title: 'Stock Market', showCount: true });
this.fetchData();
this.refreshTimer = setInterval( .(), );
}
(): <> {
(.) ;
.();
{
quotes = breaker.( () => {
resp = ();
(!resp.) ();
resp.();
}, []);
.(quotes);
.(quotes.);
.();
} (err) {
.(, {
. = ;
. = ;
.();
});
} {
.();
}
}
(: []): {
rows = quotes.( ).();
.();
}
(): {
(.) (.);
.();
}
}
Key Patterns
- Resilient Data Fetching: Circuit breakers prevent cascading failures while retry logic handles transient errors.
- State Persistence: Panel state (size, expansion) is saved to localStorage.
- Detailed Error Handling: Users see specific error messages and can retry failed operations.
- Type Safety: Strongly typed interfaces throughout the codebase.