Introduction to the Weekend Project
Throughout this week, we've explored advanced asynchronous JavaScript concepts including error handling strategies, concurrent operations management, and async generators. Now it's time to apply these skills to build a real-world data-driven application that handles complex asynchronous operations.
We'll use George Polya's famous 4-step problem-solving framework to guide our development process. Polya, a renowned mathematician, outlined this structured approach in his book "How to Solve It" (1945), and it applies remarkably well to software development challenges.
Project Overview: Real-Time Data Dashboard
For this weekend project, you'll build a real-time data dashboard that analyzes and visualizes streaming financial data. The application will fetch data from multiple sources, process it asynchronously, and present it to users in an interactive interface.
Key Features
- Connect to multiple data sources with different protocols (REST APIs, WebSockets, etc.)
- Process streaming data in real-time using async generators
- Implement robust error handling with appropriate recovery strategies
- Manage concurrent operations efficiently
- Visualize data with dynamic, auto-updating charts
- Allow users to customize views and filter data
Step 1: Understand the Problem
The first step in Polya's process is to thoroughly understand the problem. Let's break down the requirements and constraints of our real-time dashboard.
Functional Requirements
- Data Retrieval: Fetch stock prices, financial news, and market indicators from various sources
- Real-Time Updates: Maintain live connections to data streams and reflect changes immediately
- Data Processing: Calculate derived metrics (moving averages, relative performance, etc.)
- Visualization: Display data in charts, tables, and indicators that update in real-time
- User Interaction: Allow users to filter, sort, and customize their view
- Error Handling: Gracefully handle network issues, API limits, and other errors
Technical Constraints
- Browser Compatibility: Support modern browsers (Chrome, Firefox, Safari, Edge)
- Performance: Maintain responsive UI even with frequent updates
- Memory Usage: Efficiently manage data to avoid memory leaks
- Network Efficiency: Minimize unnecessary requests and handle rate limits
- Error Resilience: Application should recover from errors without requiring page refresh
Data Sources
For this project, we'll use a combination of freely available APIs and simulated data sources:
- Stock Price API: REST API for historical and current price data
- Financial News: WebSocket connection for real-time news updates
- Market Indicators: Server-Sent Events stream for index performance
- User Preference Data: Browser localStorage for saving user settings
Understanding the Problem - Key Questions:
- What are we trying to achieve? (A real-time financial dashboard)
- What data do we need to present? (Stocks, news, indicators)
- What are the main challenges? (Async data handling, error resilience, performance)
- What tools and techniques will help us? (Async generators, Promise combinators, etc.)
Step 2: Devise a Plan
With a clear understanding of the problem, it's time to devise a plan. We'll break the problem into smaller, manageable components and design the architecture of our application.
System Architecture
Our data dashboard will follow a modular architecture with these main components:
Key Design Patterns
We'll leverage several design patterns to manage the complexity of our application:
- Observable Pattern: For data streams and event handling
- Strategy Pattern: For different error handling strategies
- Factory Pattern: For creating data sources and visualizations
- Facade Pattern: To simplify the interface for complex operations
- Command Pattern: For implementing user actions and undo functionality
Implementation Plan
Let's break down our implementation plan into smaller tasks:
1. Core Infrastructure
- Set up project structure and dependencies
- Implement core utility functions for async operations
- Create base classes for data sources and processors
2. Data Acquisition
- Implement REST API client with error handling and retries
- Create WebSocket client for real-time updates
- Build SSE client for streaming data
- Develop data source manager to coordinate all sources
3. Data Processing
- Create data transformation pipeline
- Implement financial calculations (moving averages, etc.)
- Build data filtering and aggregation functions
- Develop caching strategies for performance
4. Visualization Layer
- Set up charting library (Chart.js, D3.js, etc.)
- Create reusable chart components
- Implement real-time data binding
- Design responsive layout for the dashboard
5. User Interaction
- Implement user preference storage
- Create controls for filtering and customization
- Add drag-and-drop functionality for dashboard widgets
- Build settings and configuration panels
6. Error Handling & Resilience
- Implement centralized error handling system
- Create recovery strategies for different error types
- Add logging and monitoring capabilities
- Design user-friendly error messages and feedback
7. Testing & Optimization
- Write tests for critical components
- Optimize performance bottlenecks
- Implement load testing for concurrent operations
- Fine-tune memory management
Step 3: Carry Out the Plan
Now it's time to implement our plan. We'll focus on the core components that deal with asynchronous operations, which are central to this week's learning objectives.
Core Infrastructure
Let's start with the essential utility classes for async operations:
Async Utilities
// utils/async.js - Core async utilities
/**
* Retry an async function with exponential backoff
* @param {Function} fn - Function to retry
* @param {Object} options - Retry options
* @returns {Promise} - Result of the function
*/
export async function retry(fn, options = {}) {
const {
maxRetries = 3,
initialDelay = 1000,
maxDelay = 30000,
factor = 2,
shouldRetry = (error) => true
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn(attempt);
} catch (error) {
console.warn(`Attempt ${attempt + 1} failed:`, error);
lastError = error;
if (attempt >= maxRetries || !shouldRetry(error)) {
break;
}
const delay = Math.min(
maxDelay,
initialDelay * Math.pow(factor, attempt) * (0.75 + Math.random() * 0.5)
);
console.log(`Retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
/**
* Execute an async function with a timeout
* @param {Promise} promise - Promise to add timeout to
* @param {number} timeoutMs - Timeout in milliseconds
* @param {string} message - Error message on timeout
* @returns {Promise} - Promise with timeout
*/
export function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(message));
}, timeoutMs);
promise
.then(result => {
clearTimeout(timeoutId);
resolve(result);
})
.catch(error => {
clearTimeout(timeoutId);
reject(error);
});
});
}
/**
* Limit concurrency of async operations
* @param {Function[]} tasks - Array of async functions
* @param {number} concurrency - Maximum number of concurrent tasks
* @returns {Promise} - Results of all tasks
*/
export async function limitConcurrency(tasks, concurrency = 5) {
const results = [];
const executing = new Set();
async function executeTask(task, index) {
executing.add(task);
try {
const result = await task();
results[index] = { status: 'fulfilled', value: result };
} catch (error) {
results[index] = { status: 'rejected', reason: error };
} finally {
executing.delete(task);
}
}
let index = 0;
// Initial batch of tasks
while (index < concurrency && index < tasks.length) {
executeTask(tasks[index], index);
index++;
}
// Execute remaining tasks as others complete
while (index < tasks.length) {
await Promise.race([...executing].map(task =>
task.then ? task : Promise.resolve()
));
if (executing.size < concurrency) {
executeTask(tasks[index], index);
index++;
}
}
// Wait for all tasks to complete
if (executing.size > 0) {
await Promise.all([...executing].map(task =>
task.then ? task : Promise.resolve()
));
}
return results;
}
/**
* Creates a rate limiter
* @param {number} maxRequests - Maximum requests per interval
* @param {number} interval - Interval in milliseconds
* @returns {Function} - Rate limited function wrapper
*/
export function createRateLimiter(maxRequests, interval) {
const queue = [];
let tokens = maxRequests;
let lastRefill = Date.now();
// Refill tokens based on elapsed time
function refillTokens() {
const now = Date.now();
const elapsed = now - lastRefill;
if (elapsed >= interval) {
const refillAmount = Math.floor(elapsed / interval);
tokens = Math.min(maxRequests, tokens + refillAmount * maxRequests);
lastRefill = now;
}
}
// Process the queue when tokens are available
function processQueue() {
refillTokens();
while (tokens > 0 && queue.length > 0) {
const { resolve } = queue.shift();
tokens--;
resolve();
}
if (queue.length > 0) {
const timeToNextRefill = interval - (Date.now() - lastRefill);
setTimeout(processQueue, timeToNextRefill + 10);
}
}
// Acquire a token, returning a promise that resolves when ready
async function acquireToken() {
refillTokens();
if (tokens > 0 && queue.length === 0) {
tokens--;
return Promise.resolve();
}
return new Promise(resolve => {
queue.push({ resolve });
if (queue.length === 1) {
const timeToNextRefill = interval - (Date.now() - lastRefill);
setTimeout(processQueue, timeToNextRefill + 10);
}
});
}
// Wrap a function with rate limiting
return function limitRate(fn) {
return async function(...args) {
await acquireToken();
return fn(...args);
};
};
}
Error Handling System
Next, let's implement our robust error handling system:
Error Handling Classes
// errors/index.js - Custom error classes and handler
// Base application error
export class AppError extends Error {
constructor(message, options = {}) {
super(message);
this.name = this.constructor.name;
this.code = options.code || 'APP_ERROR';
this.status = options.status || 500;
this.retryable = options.retryable ?? false;
this.cause = options.cause; // Original error that caused this one
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Network related errors
export class NetworkError extends AppError {
constructor(message, options = {}) {
super(message, {
code: 'NETWORK_ERROR',
status: 503,
retryable: true, // Network errors are generally retryable
...options
});
}
}
// API related errors
export class ApiError extends AppError {
constructor(message, options = {}) {
super(message, {
code: 'API_ERROR',
...options
});
this.endpoint = options.endpoint;
this.params = options.params;
}
}
// Rate limiting errors
export class RateLimitError extends ApiError {
constructor(message, options = {}) {
super(message, {
code: 'RATE_LIMIT_ERROR',
status: 429,
retryable: true,
...options
});
this.retryAfter = options.retryAfter; // Seconds to wait before retry
}
}
// Data processing errors
export class DataError extends AppError {
constructor(message, options = {}) {
super(message, {
code: 'DATA_ERROR',
...options
});
this.data = options.data;
}
}
// Timeout errors
export class TimeoutError extends AppError {
constructor(message, options = {}) {
super(message, {
code: 'TIMEOUT_ERROR',
status: 408,
retryable: true,
...options
});
this.timeoutMs = options.timeoutMs;
}
}
// Error handler class
export class ErrorHandler {
constructor() {
this.strategies = new Map();
this.defaultStrategy = this.defaultErrorStrategy.bind(this);
// Register common error strategies
this.registerStrategy('NetworkError', this.handleNetworkError.bind(this));
this.registerStrategy('RateLimitError', this.handleRateLimitError.bind(this));
this.registerStrategy('TimeoutError', this.handleTimeoutError.bind(this));
}
// Register a new error handling strategy
registerStrategy(errorType, strategyFn) {
this.strategies.set(errorType, strategyFn);
}
// Handle an error using the appropriate strategy
async handleError(error, context = {}) {
console.error('Handling error:', error);
// Find the appropriate strategy
const errorName = error.name || error.constructor.name;
const strategy = this.strategies.get(errorName) || this.defaultStrategy;
try {
// Apply the strategy
return await strategy(error, context);
} catch (strategyError) {
console.error('Error handling strategy failed:', strategyError);
// Last resort - return a generic error response
return {
success: false,
error: 'A system error occurred. Please try again later.',
originalError: error.message,
code: error.code || 'UNKNOWN_ERROR'
};
}
}
// Default error strategy
async defaultErrorStrategy(error, context) {
return {
success: false,
error: error.message,
code: error.code || 'UNKNOWN_ERROR'
};
}
// Handle network errors
async handleNetworkError(error, context) {
// Log additional info
console.warn('Network error details:', {
retryable: error.retryable,
endpoint: context.endpoint,
attempt: context.attempt
});
// If retryable and we have retry configuration
if (error.retryable && context.retry) {
const { maxRetries, currentRetry } = context.retry;
if (currentRetry < maxRetries) {
return {
success: false,
retry: true,
error: error.message,
code: error.code
};
}
}
return {
success: false,
error: 'Network connection issue. Please check your connection and try again.',
code: error.code,
offline: navigator.onLine === false
};
}
// Handle rate limit errors
async handleRateLimitError(error, context) {
const retryAfter = error.retryAfter || 60; // Default 1 minute
console.warn(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
// If we're in a UI context, show a message
if (context.ui) {
context.ui.showMessage(`API rate limit reached. Retrying in ${retryAfter} seconds...`);
}
return {
success: false,
retry: true,
retryAfter: retryAfter * 1000, // Convert to milliseconds
error: 'Rate limit exceeded. Please wait before trying again.',
code: error.code
};
}
// Handle timeout errors
async handleTimeoutError(error, context) {
console.warn('Operation timed out:', context);
return {
success: false,
error: 'The operation took too long to complete. Please try again.',
code: error.code,
timeout: true
};
}
}
Data Source Management
Let's implement our data sources using async generators:
Data Source Implementations
// data/sources.js - Data source implementations
import { retry, withTimeout, createRateLimiter } from '../utils/async.js';
import {
NetworkError, ApiError, RateLimitError, TimeoutError
} from '../errors/index.js';
// Base data source class
export class DataSource {
constructor(id, options = {}) {
this.id = id;
this.options = options;
this.connected = false;
this.errorHandler = options.errorHandler;
}
// Abstract methods to be implemented by subclasses
async connect() {
throw new Error('connect() must be implemented by subclass');
}
async disconnect() {
throw new Error('disconnect() must be implemented by subclass');
}
async *stream() {
throw new Error('stream() must be implemented by subclass');
}
// Helper method for handling errors
async handleError(error, context = {}) {
if (this.errorHandler) {
return this.errorHandler.handleError(error, {
source: this.id,
...context
});
}
// Default error handling
console.error(`Error in data source ${this.id}:`, error);
throw error;
}
}
// REST API data source
export class RestApiSource extends DataSource {
constructor(id, endpoint, options = {}) {
super(id, options);
this.endpoint = endpoint;
this.interval = options.interval || 60000; // Default polling interval: 1 minute
this.timeout = options.timeout || 10000; // Default timeout: 10 seconds
this.pageSize = options.pageSize || 100;
this.maxRetries = options.maxRetries || 3;
// Create rate-limited fetch function if API has rate limits
if (options.rateLimit) {
const { maxRequests, interval } = options.rateLimit;
const limiter = createRateLimiter(maxRequests, interval);
this.fetchWithRateLimit = limiter(this.fetchData.bind(this));
} else {
this.fetchWithRateLimit = this.fetchData.bind(this);
}
this.abortController = new AbortController();
this.polling = null;
}
async connect() {
if (this.connected) return;
// Reset abort controller
this.abortController = new AbortController();
try {
// Make an initial request to validate connection
await this.fetchWithRateLimit();
this.connected = true;
console.log(`Connected to API: ${this.endpoint}`);
} catch (error) {
await this.handleError(error, { operation: 'connect' });
throw error;
}
}
async disconnect() {
if (!this.connected) return;
// Abort any pending requests
this.abortController.abort();
// Clear polling interval
if (this.polling) {
clearInterval(this.polling);
this.polling = null;
}
this.connected = false;
console.log(`Disconnected from API: ${this.endpoint}`);
}
async fetchData(path = '', params = {}) {
const url = new URL(path, this.endpoint);
// Add query parameters
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, value);
}
});
try {
// Fetch with timeout and abort signal
const response = await withTimeout(
fetch(url.toString(), {
signal: this.abortController.signal,
headers: this.options.headers
}),
this.timeout,
`Request to ${url} timed out after ${this.timeout}ms`
);
// Check for rate limiting
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new RateLimitError(
`Rate limit exceeded for ${url}`,
{
endpoint: url.toString(),
retryAfter: retryAfter ? parseInt(retryAfter, 10) : 60
}
);
}
// Check for other HTTP errors
if (!response.ok) {
throw new ApiError(
`API request failed with status ${response.status}`,
{
status: response.status,
endpoint: url.toString(),
retryable: response.status >= 500
}
);
}
// Parse JSON response
return await response.json();
} catch (error) {
// Convert fetch errors to our custom error types
if (error.name === 'AbortError') {
throw new TimeoutError(
`Request to ${url} was aborted`,
{ endpoint: url.toString() }
);
} else if (error instanceof TypeError && error.message.includes('fetch')) {
throw new NetworkError(
`Network error while fetching ${url}`,
{ endpoint: url.toString() }
);
}
// Re-throw custom errors
throw error;
}
}
// Create an async generator for polling the API
async *stream() {
try {
await this.connect();
let lastFetch = 0;
while (this.connected) {
const now = Date.now();
const timeToWait = Math.max(0, this.interval - (now - lastFetch));
if (timeToWait > 0) {
await new Promise(resolve => setTimeout(resolve, timeToWait));
}
try {
const data = await retry(
() => this.fetchWithRateLimit(),
{ maxRetries: this.maxRetries }
);
lastFetch = Date.now();
yield { timestamp: lastFetch, data };
} catch (error) {
console.error(`Error fetching data from ${this.endpoint}:`, error);
// Handle error but continue streaming
const errorResult = await this.handleError(error, { operation: 'fetch' });
if (errorResult.retry && errorResult.retryAfter) {
console.log(`Waiting ${errorResult.retryAfter}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, errorResult.retryAfter));
} else {
// Non-retryable error, wait the normal interval
await new Promise(resolve => setTimeout(resolve, this.interval));
}
}
}
} finally {
console.log(`Stream ended for ${this.id}`);
this.disconnect();
}
}
// Method for paginated data retrieval
async *streamPaginated(path = '', params = {}) {
let page = 1;
let hasMore = true;
while (hasMore && this.connected) {
try {
const data = await retry(
() => this.fetchWithRateLimit(path, {
...params,
page,
limit: this.pageSize
}),
{ maxRetries: this.maxRetries }
);
// Yield the current page of data
yield {
timestamp: Date.now(),
data: data.items || data.results || data,
page
};
// Check if there are more pages
hasMore = data.hasMore || (data.totalPages && page < data.totalPages);
page++;
// Don't hammer the API, wait a bit between pages
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`Error fetching page ${page} from ${this.endpoint}:`, error);
// Handle error but continue streaming if possible
const errorResult = await this.handleError(error, {
operation: 'fetchPage',
page
});
if (errorResult.retry) {
// Retry after suggested delay or default
const delay = errorResult.retryAfter || 5000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// Non-retryable error, stop pagination
hasMore = false;
}
}
}
}
}
// WebSocket data source
export class WebSocketSource extends DataSource {
constructor(id, url, options = {}) {
super(id, options);
this.url = url;
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectDelay = options.reconnectDelay || 1000;
this.reconnectBackoffFactor = options.reconnectBackoffFactor || 1.5;
this.messageQueue = [];
this.resolveNextMessage = null;
}
async connect() {
if (this.connected) return;
this.reconnectAttempts = 0;
return this.attemptConnect();
}
async attemptConnect() {
try {
// Create WebSocket
this.socket = new WebSocket(this.url);
// Set up event handlers
this.socket.addEventListener('open', this.handleOpen.bind(this));
this.socket.addEventListener('message', this.handleMessage.bind(this));
this.socket.addEventListener('error', this.handleError.bind(this));
this.socket.addEventListener('close', this.handleClose.bind(this));
// Wait for connection
await new Promise((resolve, reject) => {
const onOpen = () => {
this.socket.removeEventListener('open', onOpen);
this.socket.removeEventListener('error', onError);
resolve();
};
const onError = (error) => {
this.socket.removeEventListener('open', onOpen);
this.socket.removeEventListener('error', onError);
reject(new NetworkError(`Failed to connect to WebSocket ${this.url}`, {
retryable: true,
cause: error
}));
};
this.socket.addEventListener('open', onOpen);
this.socket.addEventListener('error', onError);
});
this.connected = true;
this.reconnectAttempts = 0;
console.log(`Connected to WebSocket: ${this.url}`);
} catch (error) {
console.error(`WebSocket connection error (${this.url}):`, error);
if (this.reconnectAttempts < this.maxReconnectAttempts) {
// Calculate backoff delay
const delay = this.reconnectDelay *
Math.pow(this.reconnectBackoffFactor, this.reconnectAttempts);
console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
await new Promise(resolve => setTimeout(resolve, delay));
this.reconnectAttempts++;
// Try to connect again
return this.attemptConnect();
}
// Exceeded max reconnect attempts
await this.handleError(error, { operation: 'connect' });
throw error;
}
}
async disconnect() {
if (!this.connected || !this.socket) return;
try {
this.socket.close();
} catch (error) {
console.error(`Error closing WebSocket (${this.url}):`, error);
}
this.socket = null;
this.connected = false;
this.messageQueue = [];
if (this.resolveNextMessage) {
this.resolveNextMessage(null);
this.resolveNextMessage = null;
}
console.log(`Disconnected from WebSocket: ${this.url}`);
}
handleOpen(event) {
console.log(`WebSocket connection established (${this.url})`);
// Send any authentication or initialization messages
if (this.options.onConnect) {
const message = this.options.onConnect();
if (message) {
this.socket.send(typeof message === 'string' ? message : JSON.stringify(message));
}
}
}
handleMessage(event) {
try {
// Parse message data
const data = JSON.parse(event.data);
// If we have a waiting resolver, resolve with this message
if (this.resolveNextMessage) {
const resolve = this.resolveNextMessage;
this.resolveNextMessage = null;
resolve({ timestamp: Date.now(), data });
} else {
// Otherwise, queue the message
this.messageQueue.push({ timestamp: Date.now(), data });
}
} catch (error) {
console.error(`Error parsing WebSocket message (${this.url}):`, error);
}
}
async handleError(error, context = {}) {
console.error(`WebSocket error (${this.url}):`, error);
// Handle error through error handler if available
if (this.errorHandler) {
return this.errorHandler.handleError(
new NetworkError(`WebSocket error: ${error.message}`, {
retryable: true,
cause: error
}),
{
source: this.id,
operation: context.operation || 'websocket',
...context
}
);
}
}
handleClose(event) {
console.log(`WebSocket connection closed (${this.url}): ${event.code} ${event.reason}`);
this.connected = false;
// Attempt to reconnect if the closure was unexpected
if (event.code !== 1000 && event.code !== 1001) {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = this.reconnectDelay *
Math.pow(this.reconnectBackoffFactor, this.reconnectAttempts);
console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.reconnectAttempts++;
this.attemptConnect().catch(error => {
console.error(`Reconnection attempt failed:`, error);
});
}, delay);
} else {
console.warn(`Max reconnection attempts exceeded (${this.maxReconnectAttempts})`);
// Notify any waiting promises that we're done
if (this.resolveNextMessage) {
this.resolveNextMessage(null);
this.resolveNextMessage = null;
}
}
}
}
// Send a message to the WebSocket
send(message) {
if (!this.connected || !this.socket) {
throw new Error(`Cannot send message: WebSocket is not connected`);
}
const data = typeof message === 'string' ? message : JSON.stringify(message);
this.socket.send(data);
}
// Async generator for WebSocket messages
async *stream() {
try {
// Ensure we're connected
if (!this.connected) {
await this.connect();
}
while (this.connected) {
let message;
// Get message from queue or wait for new one
if (this.messageQueue.length > 0) {
message = this.messageQueue.shift();
} else {
message = await new Promise((resolve) => {
this.resolveNextMessage = resolve;
});
}
// If message is null, the connection was closed
if (message === null) {
break;
}
yield message;
}
} finally {
console.log(`Stream ended for ${this.id}`);
}
}
}
// Server-Sent Events source
export class EventSourceWrapper extends DataSource {
constructor(id, url, options = {}) {
super(id, options);
this.url = url;
this.eventSource = null;
this.messageQueue = [];
this.resolveNextMessage = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
}
async connect() {
if (this.connected) return;
try {
// Create EventSource
this.eventSource = new EventSource(this.url);
// Set up event handlers
this.eventSource.addEventListener('open', this.handleOpen.bind(this));
this.eventSource.addEventListener('error', this.handleError.bind(this));
this.eventSource.addEventListener('message', this.handleMessage.bind(this));
// Add custom event listeners if specified
if (this.options.events) {
for (const [event, handler] of Object.entries(this.options.events)) {
this.eventSource.addEventListener(event, handler);
}
}
// Wait for connection
await new Promise((resolve, reject) => {
const onOpen = () => {
this.eventSource.removeEventListener('open', onOpen);
this.eventSource.removeEventListener('error', onError);
resolve();
};
const onError = (error) => {
this.eventSource.removeEventListener('open', onOpen);
this.eventSource.removeEventListener('error', onError);
reject(new NetworkError(`Failed to connect to EventSource ${this.url}`, {
retryable: true,
cause: error
}));
};
this.eventSource.addEventListener('open', onOpen);
this.eventSource.addEventListener('error', onError);
// Also set a timeout in case neither event fires
setTimeout(() => {
if (this.eventSource.readyState !== 1) { // 1 = OPEN
reject(new TimeoutError(`Connection to EventSource ${this.url} timed out`, {
endpoint: this.url,
timeoutMs: 10000
}));
}
}, 10000);
});
this.connected = true;
this.reconnectAttempts = 0;
console.log(`Connected to EventSource: ${this.url}`);
} catch (error) {
console.error(`EventSource connection error (${this.url}):`, error);
// Clean up failed connection
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
// Try to reconnect
if (this.reconnectAttempts < this.maxReconnectAttempts) {
console.log(`Reconnecting EventSource (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
this.reconnectAttempts++;
// Wait before reconnecting
await new Promise(resolve => setTimeout(resolve, 2000));
return this.connect();
}
// Handle error
await this.handleError(error, { operation: 'connect' });
throw error;
}
}
async disconnect() {
if (!this.connected || !this.eventSource) return;
this.eventSource.close();
this.eventSource = null;
this.connected = false;
this.messageQueue = [];
if (this.resolveNextMessage) {
this.resolveNextMessage(null);
this.resolveNextMessage = null;
}
console.log(`Disconnected from EventSource: ${this.url}`);
}
handleOpen(event) {
console.log(`EventSource connection established (${this.url})`);
}
handleMessage(event) {
try {
// Parse message data
const data = JSON.parse(event.data);
// If we have a waiting resolver, resolve with this message
if (this.resolveNextMessage) {
const resolve = this.resolveNextMessage;
this.resolveNextMessage = null;
resolve({ timestamp: Date.now(), data, event: event.type });
} else {
// Otherwise, queue the message
this.messageQueue.push({
timestamp: Date.now(),
data,
event: event.type
});
}
} catch (error) {
console.error(`Error parsing EventSource message (${this.url}):`, error);
}
}
async handleError(error, context = {}) {
console.error(`EventSource error (${this.url}):`, error);
// EventSource will automatically try to reconnect,
// but we track reconnection attempts to eventually give up
this.reconnectAttempts++;
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.warn(`Max reconnection attempts exceeded (${this.maxReconnectAttempts})`);
// Close the connection
this.disconnect();
// Handle error through error handler if available
if (this.errorHandler) {
return this.errorHandler.handleError(
new NetworkError(`EventSource connection failed after ${this.maxReconnectAttempts} attempts`, {
retryable: false,
cause: error
}),
{
source: this.id,
operation: context.operation || 'eventsource',
...context
}
);
}
}
}
// Async generator for EventSource messages
async *stream() {
try {
// Ensure we're connected
if (!this.connected) {
await this.connect();
}
while (this.connected) {
let message;
// Get message from queue or wait for new one
if (this.messageQueue.length > 0) {
message = this.messageQueue.shift();
} else {
message = await new Promise((resolve) => {
this.resolveNextMessage = resolve;
});
}
// If message is null, the connection was closed
if (message === null) {
break;
}
yield message;
}
} finally {
console.log(`Stream ended for ${this.id}`);
}
}
}
// Factory function to create the appropriate data source
export function createDataSource(config) {
const { id, type, url, options = {} } = config;
switch (type.toLowerCase()) {
case 'rest':
case 'api':
return new RestApiSource(id, url, options);
case 'websocket':
case 'ws':
return new WebSocketSource(id, url, options);
case 'eventsource':
case 'sse':
return new EventSourceWrapper(id, url, options);
default:
throw new Error(`Unknown data source type: ${type}`);
}
}
Data Processing Pipeline
Now, let's implement a data processing pipeline using async generators:
Data Processing Pipeline
// data/processor.js - Data processing pipeline
import { DataError } from '../errors/index.js';
export class DataProcessor {
constructor() {
this.transformations = new Map();
this.filters = new Map();
// Register some common transformations
this.registerTransformation('map', this.mapTransformation);
this.registerTransformation('filter', this.filterTransformation);
this.registerTransformation('reduce', this.reduceTransformation);
this.registerTransformation('aggregate', this.aggregateTransformation);
this.registerTransformation('sort', this.sortTransformation);
this.registerTransformation('limit', this.limitTransformation);
}
// Register a new transformation
registerTransformation(name, transformFn) {
this.transformations.set(name, transformFn);
}
// Register a new filter
registerFilter(name, filterFn) {
this.filters.set(name, filterFn);
}
// Process a data stream through a pipeline of transformations
async *process(source, pipeline) {
try {
for await (const item of source) {
try {
// Apply each transformation in the pipeline
let result = item;
for (const step of pipeline) {
const { type, options } = step;
// Get the transformation function
const transformFn = this.transformations.get(type);
if (!transformFn) {
throw new DataError(`Unknown transformation: ${type}`, {
transformation: type,
options
});
}
// Apply the transformation
result = await transformFn(result, options);
// If the result is null or undefined, skip further processing
if (result === null || result === undefined) {
break;
}
}
// If we have a result after all transformations, yield it
if (result !== null && result !== undefined) {
yield result;
}
} catch (error) {
console.error('Error processing data item:', error);
// Yield an error result but continue processing
yield { error, sourceItem: item };
}
}
} catch (error) {
console.error('Error in data source:', error);
throw error;
}
}
// Standard transformations
// Map transformation
async mapTransformation(item, options) {
const { fn, fields } = options;
if (fn && typeof fn === 'function') {
// Apply custom function
return fn(item);
} else if (fields) {
// Pick specified fields
const result = {};
for (const field of fields) {
if (typeof field === 'string') {
result[field] = item.data ? item.data[field] : item[field];
} else if (typeof field === 'object') {
// { source: 'originalName', target: 'newName' }
const { source, target } = field;
result[target] = item.data ? item.data[source] : item[source];
}
}
return {
...item,
data: result
};
}
return item;
}
// Filter transformation
async filterTransformation(item, options) {
const { fn, condition } = options;
if (fn && typeof fn === 'function') {
// Apply custom function
if (fn(item)) {
return item;
}
return null;
} else if (condition) {
// { field: 'price', operator: 'gt', value: 100 }
const { field, operator, value } = condition;
const itemValue = item.data ? item.data[field] : item[field];
switch (operator) {
case 'eq':
if (itemValue === value) return item;
break;
case 'neq':
if (itemValue !== value) return item;
break;
case 'gt':
if (itemValue > value) return item;
break;
case 'gte':
if (itemValue >= value) return item;
break;
case 'lt':
if (itemValue < value) return item;
break;
case 'lte':
if (itemValue <= value) return item;
break;
case 'contains':
if (typeof itemValue === 'string' && itemValue.includes(value)) return item;
break;
case 'in':
if (Array.isArray(value) && value.includes(itemValue)) return item;
break;
default:
console.warn(`Unknown operator: ${operator}`);
return item;
}
return null;
}
return item;
}
// Reduce transformation (for windows of data)
async reduceTransformation(item, options) {
const { fn, initialValue } = options;
if (!Array.isArray(item.data)) {
return item;
}
const result = item.data.reduce(fn, initialValue);
return {
...item,
data: result
};
}
// Aggregate transformation (for financial calculations)
async aggregateTransformation(item, options) {
const { type, field } = options;
if (!item.data || !Array.isArray(item.data)) {
return item;
}
let result;
switch (type) {
case 'sum':
result = item.data.reduce((sum, current) => {
return sum + (current[field] || 0);
}, 0);
break;
case 'avg':
if (item.data.length === 0) {
result = 0;
} else {
const sum = item.data.reduce((acc, current) => {
return acc + (current[field] || 0);
}, 0);
result = sum / item.data.length;
}
break;
case 'min':
result = Math.min(...item.data.map(d => d[field] || 0));
break;
case 'max':
result = Math.max(...item.data.map(d => d[field] || 0));
break;
case 'count':
result = item.data.length;
break;
case 'sma': // Simple Moving Average
const period = options.period || 10;
if (item.data.length < period) {
result = null;
} else {
const values = item.data.slice(-period).map(d => d[field] || 0);
result = values.reduce((a, b) => a + b, 0) / period;
}
break;
default:
result = item.data;
break;
}
return {
...item,
data: result
};
}
// Sort transformation
async sortTransformation(item, options) {
const { field, direction = 'asc' } = options;
if (!item.data || !Array.isArray(item.data)) {
return item;
}
const sortedData = [...item.data].sort((a, b) => {
if (a[field] < b[field]) return direction === 'asc' ? -1 : 1;
if (a[field] > b[field]) return direction === 'asc' ? 1 : -1;
return 0;
});
return {
...item,
data: sortedData
};
}
// Limit transformation
async limitTransformation(item, options) {
const { count } = options;
if (!item.data || !Array.isArray(item.data)) {
return item;
}
return {
...item,
data: item.data.slice(0, count)
};
}
// Create a pipeline from a configuration object
createPipeline(config) {
if (!Array.isArray(config)) {
throw new Error('Pipeline configuration must be an array');
}
return config.map(step => {
const { type, options = {} } = step;
if (!type) {
throw new Error('Pipeline step must have a type');
}
return { type, options };
});
}
}
// Pipeline operators for financial calculations
export const financialOperators = {
// Calculate Simple Moving Average
sma(data, field, period) {
if (data.length < period) {
return null;
}
const values = data.slice(-period).map(d => d[field]);
return values.reduce((a, b) => a + b, 0) / period;
},
// Calculate Exponential Moving Average
ema(data, field, period, smoothing = 2) {
if (data.length < period) {
return null;
}
const values = data.map(d => d[field]);
const k = smoothing / (period + 1);
// First EMA is SMA
let ema = values.slice(0, period).reduce((a, b) => a + b, 0) / period;
// Calculate EMA for the rest
for (let i = period; i < values.length; i++) {
ema = values[i] * k + ema * (1 - k);
}
return ema;
},
// Calculate Relative Strength Index
rsi(data, field, period = 14) {
if (data.length <= period) {
return null;
}
const values = data.map(d => d[field]);
let gains = 0;
let losses = 0;
// Calculate initial average gain/loss
for (let i = 1; i <= period; i++) {
const change = values[i] - values[i - 1];
if (change >= 0) {
gains += change;
} else {
losses -= change;
}
}
let avgGain = gains / period;
let avgLoss = losses / period;
// Calculate RSI for the rest of the data
for (let i = period + 1; i < values.length; i++) {
const change = values[i] - values[i - 1];
if (change >= 0) {
avgGain = (avgGain * (period - 1) + change) / period;
avgLoss = (avgLoss * (period - 1)) / period;
} else {
avgGain = (avgGain * (period - 1)) / period;
avgLoss = (avgLoss * (period - 1) - change) / period;
}
}
if (avgLoss === 0) {
return 100;
}
const rs = avgGain / avgLoss;
return 100 - (100 / (1 + rs));
},
// Calculate MACD (Moving Average Convergence Divergence)
macd(data, field, shortPeriod = 12, longPeriod = 26, signalPeriod = 9) {
if (data.length < Math.max(shortPeriod, longPeriod) + signalPeriod) {
return null;
}
const values = data.map(d => d[field]);
// Calculate EMAs
const shortEMA = this.ema(data, field, shortPeriod);
const longEMA = this.ema(data, field, longPeriod);
// MACD Line = 12-day EMA - 26-day EMA
const macdLine = shortEMA - longEMA;
// TODO: Calculate signal line (9-day EMA of MACD Line)
// For a real implementation, you'd need to track MACD history
return {
macd: macdLine,
signal: null, // Not calculated in this simplified version
histogram: null // Not calculated in this simplified version
};
}
};
Visualization Component
Finally, let's implement a visualization component to display our processed data:
Chart Visualization Component
// visualization/chart.js - Chart visualization
export class ChartManager {
constructor() {
this.charts = new Map();
this.colors = [
'#4285F4', '#EA4335', '#FBBC05', '#34A853', // Google colors
'#3498db', '#e74c3c', '#2ecc71', '#f39c12', // Flat UI colors
'#9b59b6', '#1abc9c', '#e67e22', '#95a5a6' // More Flat UI colors
];
}
// Create a new chart
createChart(id, container, config) {
if (this.charts.has(id)) {
console.warn(`Chart with ID ${id} already exists and will be replaced`);
this.destroyChart(id);
}
const { type = 'line', options = {} } = config;
// Create canvas element
const canvas = document.createElement('canvas');
container.appendChild(canvas);
// Create default options based on chart type
const defaultOptions = this.getDefaultOptions(type);
// Merge with user options
const chartOptions = {
...defaultOptions,
...options
};
// Create the chart
const chart = new Chart(canvas.getContext('2d'), {
type,
data: {
labels: [],
datasets: []
},
options: chartOptions
});
// Store the chart with metadata
this.charts.set(id, {
instance: chart,
config,
container,
canvas,
datasets: new Map()
});
return chart;
}
// Get default options for chart types
getDefaultOptions(type) {
const baseOptions = {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 500
},
plugins: {
legend: {
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false
}
}
};
switch (type) {
case 'line':
return {
...baseOptions,
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'HH:mm:ss',
displayFormats: {
minute: 'HH:mm'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
beginAtZero: false,
title: {
display: true,
text: 'Value'
}
}
},
elements: {
line: {
tension: 0.3
},
point: {
radius: 0,
hitRadius: 10,
hoverRadius: 4
}
}
};
case 'bar':
return {
...baseOptions,
scales: {
x: {
grid: {
offset: true
}
},
y: {
beginAtZero: true
}
}
};
case 'pie':
case 'doughnut':
return {
...baseOptions,
plugins: {
...baseOptions.plugins,
legend: {
position: 'right'
}
}
};
case 'radar':
return {
...baseOptions,
elements: {
line: {
tension: 0.3
}
}
};
default:
return baseOptions;
}
}
// Add or update a dataset in a chart
updateDataset(chartId, datasetId, data) {
const chartInfo = this.charts.get(chartId);
if (!chartInfo) {
console.error(`Chart with ID ${chartId} not found`);
return;
}
const { instance: chart, datasets } = chartInfo;
// Check if the dataset already exists
if (!datasets.has(datasetId)) {
// Create a new dataset
const datasetIndex = chart.data.datasets.length;
const colorIndex = datasetIndex % this.colors.length;
const newDataset = {
label: datasetId,
data: [],
borderColor: this.colors[colorIndex],
backgroundColor: this.colors[colorIndex] + '80', // Add transparency
borderWidth: 2,
fill: false
};
// Add dataset to the chart
chart.data.datasets.push(newDataset);
// Store dataset info
datasets.set(datasetId, {
index: datasetIndex,
config: newDataset
});
}
// Get dataset info
const datasetInfo = datasets.get(datasetId);
const dataset = chart.data.datasets[datasetInfo.index];
// Update the data
if (Array.isArray(data)) {
// Replace all data
dataset.data = data;
} else {
// Append a single data point
dataset.data.push(data);
// If we have too many points, remove the oldest
const maxPoints = 100; // Configurable
if (dataset.data.length > maxPoints) {
dataset.data.shift();
}
}
// Update labels if needed (for time series)
if (data.x && chart.data.labels.length < dataset.data.length) {
chart.data.labels.push(data.x);
// Remove excess labels
while (chart.data.labels.length > dataset.data.length) {
chart.data.labels.shift();
}
}
// Update the chart
chart.update();
}
// Destroy a chart
destroyChart(id) {
const chartInfo = this.charts.get(id);
if (!chartInfo) {
console.warn(`Chart with ID ${id} not found`);
return;
}
const { instance, container, canvas } = chartInfo;
// Destroy Chart.js instance
instance.destroy();
// Remove canvas from container
if (container.contains(canvas)) {
container.removeChild(canvas);
}
// Remove from charts map
this.charts.delete(id);
}
// Get a chart instance
getChart(id) {
const chartInfo = this.charts.get(id);
return chartInfo ? chartInfo.instance : null;
}
// Clear all data from a chart
clearChart(id) {
const chartInfo = this.charts.get(id);
if (!chartInfo) {
console.warn(`Chart with ID ${id} not found`);
return;
}
const { instance: chart } = chartInfo;
// Clear all data
chart.data.labels = [];
chart.data.datasets.forEach(dataset => {
dataset.data = [];
});
// Update the chart
chart.update();
}
// Create a real-time streaming chart
createStreamingChart(id, container, config) {
const chart = this.createChart(id, container, {
...config,
options: {
...config.options,
scales: {
x: {
type: 'realtime',
realtime: {
duration: 60000, // 1 minute
refresh: 1000, // 1 second
delay: 1000, // 1 second
onRefresh: chart => {
// This will be handled by our update method
}
}
},
y: {
beginAtZero: false,
title: {
display: true,
text: config.options?.scales?.y?.title?.text || 'Value'
}
}
},
plugins: {
...config.options?.plugins,
streaming: {
frameRate: 30
}
}
}
});
return chart;
}
// Update a streaming chart with new data
updateStreamingChart(chartId, datasetId, value, timestamp = Date.now()) {
const chartInfo = this.charts.get(chartId);
if (!chartInfo) {
console.error(`Chart with ID ${chartId} not found`);
return;
}
this.updateDataset(chartId, datasetId, {
x: timestamp,
y: value
});
}
}
Step 4: Look Back and Reflect
Now that we've implemented the core components of our data-driven application, it's important to step back, reflect on our solution, and refine it.
Integration: Bringing Everything Together
Let's implement a central Dashboard Controller that integrates all our components:
Dashboard Controller
// DashboardController.js - Main application controller
import { ErrorHandler } from './errors/index.js';
import { createDataSource } from './data/sources.js';
import { DataProcessor, financialOperators } from './data/processor.js';
import { ChartManager } from './visualization/chart.js';
import { retry, withTimeout, limitConcurrency } from './utils/async.js';
export class DashboardController {
constructor(config = {}) {
// Initialize subsystems
this.errorHandler = new ErrorHandler();
this.processor = new DataProcessor();
this.chartManager = new ChartManager();
// Initialize state
this.sources = new Map();
this.widgets = new Map();
this.running = false;
this.abortController = new AbortController();
// Register financial operators
Object.entries(financialOperators).forEach(([name, fn]) => {
this.processor.registerTransformation(name, fn);
});
// Apply initial configuration
if (config.sources) {
this.configureSources(config.sources);
}
if (config.widgets) {
this.configureWidgets(config.widgets);
}
// Store user preferences
this.preferences = this.loadPreferences();
}
// Initialize the dashboard
async init() {
try {
// Create DOM elements for widgets
this.createWidgetContainers();
// Initialize data sources
await this.initializeSources();
// Initialize widgets
this.initializeWidgets();
console.log('Dashboard initialized successfully');
return true;
} catch (error) {
console.error('Failed to initialize dashboard:', error);
// Handle error
const errorResult = await this.errorHandler.handleError(error, {
operation: 'init'
});
// Display error message
this.showErrorMessage('Failed to initialize dashboard', errorResult);
return false;
}
}
// Start the dashboard
async start() {
if (this.running) {
console.warn('Dashboard is already running');
return;
}
try {
this.running = true;
this.abortController = new AbortController();
// Connect to all data sources
const connectPromises = Array.from(this.sources.values()).map(source =>
source.connect().catch(error => {
console.error(`Failed to connect to source ${source.id}:`, error);
return { error, source: source.id };
})
);
// Wait for all connections
const results = await Promise.allSettled(connectPromises);
// Check for connection failures
const failures = results
.filter(result => result.status === 'rejected' || result.value?.error)
.map(result => {
if (result.status === 'rejected') {
return { error: result.reason, source: 'unknown' };
}
return result.value;
});
if (failures.length > 0) {
console.warn(`${failures.length} data sources failed to connect:`, failures);
// Show warning to user
this.showWarningMessage(`${failures.length} data sources failed to connect. Some widgets may not display correctly.`);
}
// Start data streams for each widget
for (const [id, widget] of this.widgets.entries()) {
this.startWidgetStream(id, widget);
}
console.log('Dashboard started');
} catch (error) {
console.error('Failed to start dashboard:', error);
this.running = false;
// Handle error
const errorResult = await this.errorHandler.handleError(error, {
operation: 'start'
});
// Display error message
this.showErrorMessage('Failed to start dashboard', errorResult);
}
}
// Stop the dashboard
async stop() {
if (!this.running) {
console.warn('Dashboard is not running');
return;
}
try {
// Signal abort to all streams
this.abortController.abort();
// Disconnect from all data sources
const disconnectPromises = Array.from(this.sources.values()).map(source =>
source.disconnect().catch(error => {
console.error(`Failed to disconnect from source ${source.id}:`, error);
})
);
// Wait for all disconnections
await Promise.allSettled(disconnectPromises);
this.running = false;
console.log('Dashboard stopped');
} catch (error) {
console.error('Error stopping dashboard:', error);
// Force stop
this.running = false;
}
}
// Configure data sources
configureSources(sourcesConfig) {
for (const sourceConfig of sourcesConfig) {
const { id } = sourceConfig;
try {
// Create the data source
const source = createDataSource({
...sourceConfig,
options: {
...sourceConfig.options,
errorHandler: this.errorHandler
}
});
// Add to sources map
this.sources.set(id, source);
} catch (error) {
console.error(`Failed to configure source ${id}:`, error);
}
}
}
// Configure widgets
configureWidgets(widgetsConfig) {
for (const widgetConfig of widgetsConfig) {
const { id } = widgetConfig;
// Add to widgets map
this.widgets.set(id, widgetConfig);
}
}
// Initialize data sources
async initializeSources() {
const initPromises = Array.from(this.sources.values()).map(source => {
// Just create the source, don't connect yet
return Promise.resolve();
});
// Wait for all initializations
await Promise.all(initPromises);
}
// Create widget containers
createWidgetContainers() {
const dashboard = document.getElementById('dashboard');
if (!dashboard) {
console.error('Dashboard container not found');
return;
}
// Clear existing widgets
dashboard.innerHTML = '';
// Create containers for each widget
for (const [id, widget] of this.widgets.entries()) {
const { title, type, width = 6, height = 1 } = widget;
// Create widget container
const container = document.createElement('div');
container.className = `widget widget-${type}`;
container.style.gridColumn = `span ${width}`;
container.style.gridRow = `span ${height}`;
container.id = `widget-${id}`;
// Create widget header
const header = document.createElement('div');
header.className = 'widget-header';
const titleElem = document.createElement('h3');
titleElem.className = 'widget-title';
titleElem.textContent = title;
const actions = document.createElement('div');
actions.className = 'widget-actions';
// Add refresh button
const refreshBtn = document.createElement('button');
refreshBtn.className = 'widget-refresh';
refreshBtn.innerHTML = '↻'; // Refresh icon
refreshBtn.title = 'Refresh';
refreshBtn.addEventListener('click', () => this.refreshWidget(id));
// Add settings button
const settingsBtn = document.createElement('button');
settingsBtn.className = 'widget-settings';
settingsBtn.innerHTML = '⚙'; // Settings icon
settingsBtn.title = 'Settings';
settingsBtn.addEventListener('click', () => this.openWidgetSettings(id));
// Add buttons to actions
actions.appendChild(refreshBtn);
actions.appendChild(settingsBtn);
// Add elements to header
header.appendChild(titleElem);
header.appendChild(actions);
// Create widget content
const content = document.createElement('div');
content.className = 'widget-content';
content.id = `widget-content-${id}`;
// Create widget footer for status
const footer = document.createElement('div');
footer.className = 'widget-footer';
footer.id = `widget-footer-${id}`;
// Add status indicator
const status = document.createElement('span');
status.className = 'widget-status';
status.id = `widget-status-${id}`;
status.textContent = 'Initializing...';
footer.appendChild(status);
// Assemble widget
container.appendChild(header);
container.appendChild(content);
container.appendChild(footer);
// Add to dashboard
dashboard.appendChild(container);
}
}
// Initialize widgets
initializeWidgets() {
for (const [id, widget] of this.widgets.entries()) {
const { type, chartType, dataSource } = widget;
try {
// Get the content container
const container = document.getElementById(`widget-content-${id}`);
if (!container) {
console.error(`Container for widget ${id} not found`);
continue;
}
// Initialize based on widget type
switch (type) {
case 'chart':
// Create chart
this.chartManager.createChart(id, container, {
type: chartType || 'line',
options: widget.chartOptions || {}
});
break;
case 'table':
// Create table
container.innerHTML = `
${(widget.columns || []).map(col => `${col.label} `).join('')}
`;
break;
case 'metrics':
// Create metrics display
container.innerHTML = `
${(widget.metrics || []).map(metric => `
--
${metric.label}
`).join('')}
`;
break;
default:
console.warn(`Unknown widget type: ${type}`);
break;
}
// Update status
this.updateWidgetStatus(id, 'Ready');
} catch (error) {
console.error(`Error initializing widget ${id}:`, error);
this.updateWidgetStatus(id, 'Error initializing');
}
}
}
// Start data stream for widget
async startWidgetStream(id, widget) {
const { dataSource: sourceId, pipeline = [] } = widget;
// Get data source
const source = this.sources.get(sourceId);
if (!source) {
console.error(`Data source ${sourceId} not found for widget ${id}`);
this.updateWidgetStatus(id, 'Error: Data source not found');
return;
}
try {
// Create processing pipeline
const processingPipeline = this.processor.createPipeline(pipeline);
// Create data stream
const stream = this.processor.process(source.stream(), processingPipeline);
// Start consuming the stream
this.consumeWidgetStream(id, widget, stream);
// Update status
this.updateWidgetStatus(id, 'Live');
} catch (error) {
console.error(`Error starting stream for widget ${id}:`, error);
this.updateWidgetStatus(id, 'Error: Failed to start data stream');
}
}
// Consume data stream for widget
async consumeWidgetStream(id, widget, stream) {
const { type } = widget;
const signal = this.abortController.signal;
try {
// Process the stream based on widget type
for await (const item of stream) {
// Check if we've been aborted
if (signal.aborted) {
break;
}
// Handle error items
if (item.error) {
console.error(`Error in data stream for widget ${id}:`, item.error);
this.updateWidgetStatus(id, 'Error in data stream');
continue;
}
// Update the widget with new data
this.updateWidget(id, widget, item);
}
} catch (error) {
console.error(`Error consuming stream for widget ${id}:`, error);
this.updateWidgetStatus(id, 'Error: Stream processing failed');
}
}
// Update widget with new data
updateWidget(id, widget, data) {
const { type, dataField } = widget;
try {
switch (type) {
case 'chart':
this.updateChartWidget(id, widget, data);
break;
case 'table':
this.updateTableWidget(id, widget, data);
break;
case 'metrics':
this.updateMetricsWidget(id, widget, data);
break;
default:
console.warn(`Unknown widget type: ${type}`);
break;
}
// Update last updated timestamp
this.updateWidgetStatus(id, 'Updated ' + new Date().toLocaleTimeString());
} catch (error) {
console.error(`Error updating widget ${id}:`, error);
this.updateWidgetStatus(id, 'Error updating widget');
}
}
// Update chart widget
updateChartWidget(id, widget, data) {
const { datasets = [] } = widget;
// Get the data to display
const itemData = data.data;
// Update each dataset
datasets.forEach(dataset => {
const { id: datasetId, field, transform } = dataset;
let value = itemData[field];
// Apply transformation if specified
if (transform && typeof transform === 'function') {
value = transform(value, itemData);
}
// Update the chart
this.chartManager.updateDataset(id, datasetId, {
x: data.timestamp,
y: value
});
});
}
// Update table widget
updateTableWidget(id, widget, data) {
const { columns = [], maxRows = 10 } = widget;
// Get table body
const tableBody = document.querySelector(`#table-${id} tbody`);
if (!tableBody) {
console.error(`Table body for widget ${id} not found`);
return;
}
// Get the data to display
const itemData = data.data;
// Create a new row
const row = document.createElement('tr');
// Add cells for each column
columns.forEach(column => {
const { field, format } = column;
const cell = document.createElement('td');
// Get the value
let value = itemData[field];
// Format the value if specified
if (format && typeof format === 'function') {
value = format(value, itemData);
}
cell.textContent = value;
row.appendChild(cell);
});
// Add the row to the table
tableBody.appendChild(row);
// Remove excess rows
while (tableBody.children.length > maxRows) {
tableBody.removeChild(tableBody.firstChild);
}
}
// Update metrics widget
updateMetricsWidget(id, widget, data) {
const { metrics = [] } = widget;
// Get the data to display
const itemData = data.data;
// Update each metric
metrics.forEach(metric => {
const { id: metricId, field, format } = metric;
// Get the element
const element = document.getElementById(`metric-${id}-${metricId}`);
if (!element) {
console.warn(`Metric element ${metricId} not found for widget ${id}`);
return;
}
// Get the value
let value = itemData[field];
// Format the value if specified
if (format && typeof format === 'function') {
value = format(value, itemData);
} else if (typeof value === 'number') {
// Default number formatting
value = value.toLocaleString(undefined, {
maximumFractionDigits: 2
});
}
element.textContent = value;
// Add trend indicator if available
if (itemData.trend || itemData.change) {
const trend = itemData.trend || (itemData.change > 0 ? 'up' : itemData.change < 0 ? 'down' : 'flat');
element.dataset.trend = trend;
}
});
}
// Update widget status
updateWidgetStatus(id, status) {
const statusElement = document.getElementById(`widget-status-${id}`);
if (statusElement) {
statusElement.textContent = status;
}
}
// Refresh a widget
refreshWidget(id) {
const widget = this.widgets.get(id);
if (!widget) {
console.error(`Widget ${id} not found`);
return;
}
// Clear existing data
switch (widget.type) {
case 'chart':
this.chartManager.clearChart(id);
break;
case 'table':
const tableBody = document.querySelector(`#table-${id} tbody`);
if (tableBody) {
tableBody.innerHTML = '';
}
break;
case 'metrics':
(widget.metrics || []).forEach(metric => {
const element = document.getElementById(`metric-${id}-${metric.id}`);
if (element) {
element.textContent = '--';
delete element.dataset.trend;
}
});
break;
}
// Update status
this.updateWidgetStatus(id, 'Refreshing...');
// If the dashboard is running, restart the stream
if (this.running) {
// Stop current stream
// (The abort controller will handle this)
// Start new stream
this.startWidgetStream(id, widget);
} else {
this.updateWidgetStatus(id, 'Ready');
}
}
// Open widget settings
openWidgetSettings(id) {
const widget = this.widgets.get(id);
if (!widget) {
console.error(`Widget ${id} not found`);
return;
}
// Create settings modal
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Widget Settings: ${widget.title}</h2>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<form id="widget-settings-form">
<div class="form-group">
<label for="widget-title">Title</label>
<input type="text" id="widget-title" value="${widget.title}">
</div>
<!-- Add more settings based on widget type -->
${this.getWidgetSettingsFields(widget)}
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-cancel">Cancel</button>
</div>
</form>
</div>
</div>
`;
// Add to document
document.body.appendChild(modal);
// Add event listeners
modal.querySelector('.modal-close').addEventListener('click', () => {
document.body.removeChild(modal);
});
modal.querySelector('.btn-cancel').addEventListener('click', () => {
document.body.removeChild(modal);
});
modal.querySelector('#widget-settings-form').addEventListener('submit', (event) => {
event.preventDefault();
// Get updated settings
const title = modal.querySelector('#widget-title').value;
// Update widget
widget.title = title;
// Update widget-specific settings
this.updateWidgetFromSettings(widget, modal);
// Update widget display
document.querySelector(`#widget-${id} .widget-title`).textContent = title;
// Refresh widget
this.refreshWidget(id);
// Save preferences
this.savePreferences();
// Close modal
document.body.removeChild(modal);
});
}
// Get widget-specific settings fields
getWidgetSettingsFields(widget) {
const { type } = widget;
switch (type) {
case 'chart':
return `
<div class="form-group">
<label for="widget-chart-type">Chart Type</label>
<select id="widget-chart-type">
<option value="line" ${widget.chartType === 'line' ? 'selected' : ''}>Line</option>
<option value="bar" ${widget.chartType === 'bar' ? 'selected' : ''}>Bar</option>
<option value="pie" ${widget.chartType === 'pie' ? 'selected' : ''}>Pie</option>
</select>
</div>
`;
case 'table':
return `
<div class="form-group">
<label for="widget-max-rows">Maximum Rows</label>
<input type="number" id="widget-max-rows" value="${widget.maxRows || 10}" min="1" max="100">
</div>
`;
default:
return '';
}
}
// Update widget settings from form
updateWidgetFromSettings(widget, modal) {
const { type } = widget;
switch (type) {
case 'chart':
widget.chartType = modal.querySelector('#widget-chart-type').value;
break;
case 'table':
widget.maxRows = parseInt(modal.querySelector('#widget-max-rows').value, 10);
break;
}
}
// Load user preferences
loadPreferences() {
try {
const savedPreferences = localStorage.getItem('dashboardPreferences');
if (savedPreferences) {
return JSON.parse(savedPreferences);
}
} catch (error) {
console.error('Error loading preferences:', error);
}
return {};
}
// Save user preferences
savePreferences() {
try {
const preferences = {
widgets: Object.fromEntries(
Array.from(this.widgets.entries()).map(([id, widget]) => [
id,
{
title: widget.title,
chartType: widget.chartType,
maxRows: widget.maxRows
}
])
)
};
localStorage.setItem('dashboardPreferences', JSON.stringify(preferences));
} catch (error) {
console.error('Error saving preferences:', error);
}
}
// Show error message
showErrorMessage(message, details) {
// Create notification
const notification = document.createElement('div');
notification.className = 'notification error';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-title">Error</div>
<div class="notification-message">${message}</div>
${details ? `<div class="notification-details">${details.error || ''}</div>` : ''}
<button class="notification-close">×</button>
</div>
`;
// Add to notification container
const container = document.getElementById('notifications') || document.body;
container.appendChild(notification);
// Add event listener for close button
notification.querySelector('.notification-close').addEventListener('click', () => {
container.removeChild(notification);
});
// Auto-remove after 10 seconds
setTimeout(() => {
if (container.contains(notification)) {
container.removeChild(notification);
}
}, 10000);
}
// Show warning message
showWarningMessage(message) {
// Create notification
const notification = document.createElement('div');
notification.className = 'notification warning';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-title">Warning</div>
<div class="notification-message">${message}</div>
<button class="notification-close">×</button>
</div>
`;
// Add to notification container
const container = document.getElementById('notifications') || document.body;
container.appendChild(notification);
// Add event listener for close button
notification.querySelector('.notification-close').addEventListener('click', () => {
container.removeChild(notification);
});
// Auto-remove after 7 seconds
setTimeout(() => {
if (container.contains(notification)) {
container.removeChild(notification);
}
}, 7000);
}
}
Improvements and Optimizations
After reviewing our implementation, we can identify several areas for improvement:
- Memory Management: We need to implement proper cleanup for long-running streams to avoid memory leaks.
- Error Recovery: Improve error recovery by implementing more sophisticated retry strategies.
- Performance Optimization: Batch updates to charts and UI elements to reduce rendering overhead.
- Caching Strategy: Implement caching for historical data to reduce API calls.
- User Experience: Add loading indicators and smoother transitions between data updates.
Optimizing Chart Updates
// Optimization: Batch chart updates
class OptimizedChartManager extends ChartManager {
constructor() {
super();
this.updateQueue = new Map();
this.animationFrameRequested = false;
}
// Override update method to batch updates
updateDataset(chartId, datasetId, data) {
// Get or create queue for this chart
if (!this.updateQueue.has(chartId)) {
this.updateQueue.set(chartId, new Map());
}
const chartQueue = this.updateQueue.get(chartId);
// Queue this update
if (!chartQueue.has(datasetId)) {
chartQueue.set(datasetId, []);
}
chartQueue.get(datasetId).push(data);
// Request animation frame for processing
if (!this.animationFrameRequested) {
this.animationFrameRequested = true;
requestAnimationFrame(() => this.processUpdateQueue());
}
}
// Process all queued updates
processUpdateQueue() {
this.animationFrameRequested = false;
// Process each chart
for (const [chartId, chartQueue] of this.updateQueue.entries()) {
const chartInfo = this.charts.get(chartId);
if (!chartInfo) {
continue;
}
// Apply updates to datasets
for (const [datasetId, updates] of chartQueue.entries()) {
const datasetInfo = chartInfo.datasets.get(datasetId);
if (!datasetInfo) {
continue;
}
const dataset = chartInfo.instance.data.datasets[datasetInfo.index];
// Apply each update
for (const update of updates) {
if (Array.isArray(update)) {
// Replace all data
dataset.data = update;
} else {
// Append a single data point
dataset.data.push(update);
// If we have too many points, remove the oldest
const maxPoints = 100; // Configurable
if (dataset.data.length > maxPoints) {
dataset.data.shift();
}
// Update labels if needed (for time series)
if (update.x && chartInfo.instance.data.labels.length < dataset.data.length) {
chartInfo.instance.data.labels.push(update.x);
// Remove excess labels
while (chartInfo.instance.data.labels.length > dataset.data.length) {
chartInfo.instance.data.labels.shift();
}
}
}
}
}
// Update the chart
chartInfo.instance.update();
}
// Clear the queue
this.updateQueue.clear();
}
}
Project Setup and Configuration
To help you get started with this weekend project, here is a sample configuration for your dashboard:
Sample Dashboard Configuration
// Sample dashboard configuration
const dashboardConfig = {
// Data sources
sources: [
{
id: 'stock-api',
type: 'rest',
url: 'https://api.example.com/stocks',
options: {
interval: 60000, // 1 minute polling interval
rateLimit: {
maxRequests: 30,
interval: 60000 // 30 requests per minute
},
headers: {
'X-API-Key': 'your-api-key-here'
}
}
},
{
id: 'news-feed',
type: 'websocket',
url: 'wss://api.example.com/news',
options: {
onConnect: () => ({
action: 'subscribe',
topics: ['finance', 'markets', 'technology']
})
}
},
{
id: 'market-data',
type: 'eventsource',
url: 'https://api.example.com/market/events',
options: {
events: {
'market-update': (event) => {
console.log('Market update received:', event);
}
}
}
}
],
// Widgets
widgets: [
{
id: 'stock-chart',
title: 'Stock Price History',
type: 'chart',
dataSource: 'stock-api',
chartType: 'line',
width: 8,
height: 2,
chartOptions: {
scales: {
y: {
title: {
display: true,
text: 'Price ($)'
}
}
}
},
datasets: [
{
id: 'price',
field: 'price',
label: 'Stock Price'
},
{
id: 'sma',
field: 'price',
label: 'SMA (20)',
transform: (value, data) => {
// TODO: Calculate SMA
return value * 0.95;
}
}
],
pipeline: [
{
type: 'filter',
options: {
condition: {
field: 'symbol',
operator: 'eq',
value: 'AAPL'
}
}
}
]
},
{
id: 'news-table',
title: 'Latest Financial News',
type: 'table',
dataSource: 'news-feed',
width: 4,
height: 2,
maxRows: 10,
columns: [
{
field: 'time',
label: 'Time',
format: (value) => new Date(value).toLocaleTimeString()
},
{
field: 'headline',
label: 'Headline'
},
{
field: 'source',
label: 'Source'
}
],
pipeline: [
{
type: 'filter',
options: {
condition: {
field: 'category',
operator: 'in',
value: ['stocks', 'market', 'economy']
}
}
},
{
type: 'sort',
options: {
field: 'time',
direction: 'desc'
}
}
]
},
{
id: 'market-metrics',
title: 'Market Indicators',
type: 'metrics',
dataSource: 'market-data',
width: 12,
height: 1,
metrics: [
{
id: 'sp500',
field: 'sp500',
label: 'S&P 500',
format: (value) => value.toFixed(2)
},
{
id: 'nasdaq',
field: 'nasdaq',
label: 'NASDAQ',
format: (value) => value.toFixed(2)
},
{
id: 'djia',
field: 'djia',
label: 'Dow Jones',
format: (value) => value.toFixed(2)
},
{
id: 'volatility',
field: 'vix',
label: 'VIX',
format: (value) => value.toFixed(2)
}
],
pipeline: []
}
// Add more widgets as needed
]
};
// Initialize and start the dashboard
document.addEventListener('DOMContentLoaded', async () => {
// Create dashboard
const dashboard = new DashboardController(dashboardConfig);
// Initialize
const initialized = await dashboard.init();
if (initialized) {
// Start
await dashboard.start();
// Add event listener for page unload
window.addEventListener('beforeunload', () => {
dashboard.stop();
});
}
});
HTML Structure
Here's a basic HTML structure for your dashboard:
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Time Financial Dashboard</title>
<link rel="stylesheet" href="css/styles.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@2.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
</head>
<body>
<header>
<h1>Real-Time Financial Dashboard</h1>
<div class="controls">
<button id="start-btn" class="btn btn-primary">Start</button>
<button id="stop-btn" class="btn btn-danger">Stop</button>
<button id="settings-btn" class="btn btn-secondary">Settings</button>
</div>
</header>
<main>
<div id="dashboard" class="dashboard-grid">
<!-- Widgets will be dynamically inserted here -->
</div>
</main>
<div id="notifications" class="notifications-container"></div>
<footer>
<p>Data refreshes automatically. Last updated: <span id="last-updated">Never</span></p>
</footer>
<script src="js/utils/async.js" type="module"></script>
<script src="js/errors/index.js" type="module"></script>
<script src="js/data/sources.js" type="module"></script>
<script src="js/data/processor.js" type="module"></script>
<script src="js/visualization/chart.js" type="module"></script>
<script src="js/DashboardController.js" type="module"></script>
<script src="js/app.js" type="module"></script>
</body>
</html>
CSS Styling
Here's some basic CSS to style your dashboard:
CSS Styling
/* styles.css */
:root {
--primary-color: #4285F4;
--secondary-color: #34A853;
--error-color: #EA4335;
--warning-color: #FBBC05;
--text-color: #202124;
--light-gray: #F1F3F4;
--dark-gray: #5F6368;
--border-color: #DADCE0;
--shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', 'Segoe UI', Arial, sans-serif;
color: var(--text-color);
line-height: 1.6;
background-color: #fafafa;
}
header {
background-color: white;
padding: 1rem 2rem;
box-shadow: var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 1.5rem;
font-weight: 500;
}
.controls {
display: flex;
gap: 0.5rem;
}
main {
padding: 2rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-gap: 1rem;
margin-bottom: 2rem;
}
.widget {
background-color: white;
border-radius: 8px;
box-shadow: var(--shadow);
padding: 1rem;
display: flex;
flex-direction: column;
}
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.widget-title {
font-size: 1rem;
font-weight: 500;
}
.widget-actions {
display: flex;
gap: 0.5rem;
}
.widget-actions button {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: var(--dark-gray);
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.widget-actions button:hover {
background-color: var(--light-gray);
}
.widget-content {
flex: 1;
min-height: 200px;
position: relative;
}
.widget-footer {
font-size: 0.75rem;
color: var(--dark-gray);
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
text-align: right;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-danger {
background-color: var(--error-color);
color: white;
}
.btn-secondary {
background-color: var(--light-gray);
color: var(--text-color);
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 0.5rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.data-table th {
font-weight: 500;
background-color: var(--light-gray);
}
.metrics-container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
height: 100%;
align-items: center;
}
.metric {
text-align: center;
flex: 1;
min-width: 120px;
padding: 0.5rem;
}
.metric-value {
font-size: 2rem;
font-weight: 500;
}
.metric-value[data-trend="up"] {
color: var(--secondary-color);
}
.metric-value[data-trend="down"] {
color: var(--error-color);
}
.metric-label {
font-size: 0.875rem;
color: var(--dark-gray);
}
.notifications-container {
position: fixed;
top: 1rem;
right: 1rem;
width: 300px;
z-index: 1000;
}
.notification {
margin-bottom: 0.5rem;
padding: 1rem;
border-radius: 4px;
box-shadow: var(--shadow);
animation: slideIn 0.3s ease;
}
.notification.error {
background-color: #FFEBEE;
border-left: 4px solid var(--error-color);
}
.notification.warning {
background-color: #FFF8E1;
border-left: 4px solid var(--warning-color);
}
.notification-title {
font-weight: 500;
margin-bottom: 0.25rem;
}
.notification-details {
font-size: 0.75rem;
margin-top: 0.5rem;
color: var(--dark-gray);
}
.notification-close {
float: right;
background: none;
border: none;
cursor: pointer;
font-size: 1.25rem;
color: var(--dark-gray);
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background-color: white;
border-radius: 8px;
box-shadow: var(--shadow);
width: 100%;
max-width: 500px;
overflow: hidden;
}
.modal-header {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
font-size: 1.25rem;
font-weight: 500;
}
.modal-close {
background: none;
border: none;
cursor: pointer;
font-size: 1.5rem;
color: var(--dark-gray);
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
footer {
padding: 1rem 2rem;
text-align: center;
color: var(--dark-gray);
font-size: 0.875rem;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
Learning Objectives and Assessment
This weekend project is designed to solidify your understanding of the asynchronous JavaScript concepts covered this week. Here's what you should focus on:
Key Learning Objectives
- Implementing robust error handling strategies for asynchronous operations
- Managing concurrent operations efficiently with appropriate patterns
- Using async generators to process streaming data
- Applying Polya's problem-solving process to complex development tasks
- Building a complete data-driven application that integrates multiple asynchronous patterns
Assessment Criteria
Your project will be assessed based on:
- Functionality: Does the application work correctly and handle all required use cases?
- Error Handling: How well does the application handle errors and edge cases?
- Performance: Is the application efficient and responsive even with multiple data streams?
- Code Quality: Is the code well-organized, modular, and maintainable?
- User Experience: Is the interface intuitive and responsive?
- Problem Solving: Did you apply Polya's four-step process effectively?
Documentation Requirements
In addition to your code, please include:
- A README file explaining how to run the application
- A brief writeup on how you applied Polya's problem-solving process
- Documentation of any challenges you encountered and how you solved them
- Reflections on what you learned and how you would improve the application in the future
Conclusion
This weekend project brings together all the asynchronous JavaScript concepts you've learned throughout the week. By building a real-time data dashboard, you'll gain hands-on experience with:
- Creating robust error handling systems with custom error types and recovery strategies
- Managing concurrent operations with appropriate patterns like rate limiting and prioritization
- Processing streaming data with async generators
- Integrating multiple data sources with different protocols
- Visualizing real-time data in an interactive user interface
Most importantly, you'll learn to apply a structured problem-solving approach based on Polya's four steps: understanding the problem, devising a plan, carrying out the plan, and looking back to reflect and improve.
Remember that complex problems become manageable when broken down into smaller, well-defined components. Take your time to understand each part of the system before implementing it, and don't hesitate to revise your approach as you gain new insights during development.
Good luck, and enjoy building your real-time data dashboard!