JavaScript Promises Tutorial - Section 8: Course Project
export class Cache {
constructor(options = {}) {
this.cache = new Map();
this.ttl = options.ttl || 300000; // 5 minutes default
this.maxSize = options.maxSize || 100;
}
get(key) {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
// Check if expired
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return entry.value;
}
set(key, value) {
// Evict oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
has(key) {
return this.get(key) !== null;
}
delete(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
ttl: this.ttl
};
}
}
export class RateLimiter {
constructor(requestsPerSecond) {
this.requestsPerSecond = requestsPerSecond;
this.interval = 1000 / requestsPerSecond;
this.lastCallTime = 0;
this.queue = [];
this.processing = false;
}
async execute(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
while (this.queue.length > 0) {
const now = Date.now();
const timeSinceLastCall = now - this.lastCallTime;
if (timeSinceLastCall < this.interval) {
const delay = this.interval - timeSinceLastCall;
await new Promise(resolve => setTimeout(resolve, delay));
}
const { fn, resolve, reject } = this.queue.shift();
this.lastCallTime = Date.now();
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
import { APIClient } from './client.js';
import { Cache } from '../cache/index.js';
import { RateLimiter } from '../utils/rateLimiter.js';
export class CachedAPIClient extends APIClient {
constructor(baseURL, options = {}) {
super(baseURL, options);
this.cache = new Cache({
ttl: options.cacheTTL || 300000,
maxSize: options.cacheMaxSize || 100
});
this.rateLimiter = new RateLimiter(
options.requestsPerSecond || 10
);
this.stats = {
requests: 0,
cacheHits: 0,
cacheMisses: 0,
errors: 0
};
}
async request(endpoint, options = {}) {
const cacheKey = this.getCacheKey(endpoint, options);
// Check cache for GET requests
if (!options.method || options.method === 'GET') {
const cached = this.cache.get(cacheKey);
if (cached) {
this.stats.cacheHits++;
console.log(`Cache hit: ${endpoint}`);
return cached;
}
this.stats.cacheMisses++;
}
// Rate limit the request
return await this.rateLimiter.execute(async () => {
try {
this.stats.requests++;
const data = await super.request(endpoint, options);
// Cache GET responses
if (!options.method || options.method === 'GET') {
this.cache.set(cacheKey, data);
}
return data;
} catch (error) {
this.stats.errors++;
throw error;
}
});
}
getCacheKey(endpoint, options) {
const method = options.method || 'GET';
return `${method}:${endpoint}`;
}
invalidateCache(pattern) {
if (typeof pattern === 'string') {
this.cache.delete(pattern);
}
}
getStats() {
return {
...this.stats,
cache: this.cache.getStats(),
cacheHitRate: (
(this.stats.cacheHits / (this.stats.cacheHits + this.stats.cacheMisses)) * 100
).toFixed(2) + '%'
};
}
}
export class Logger {
constructor(options = {}) {
this.level = options.level || 'info';
this.levels = {
error: 0,
warn: 1,
info: 2,
debug: 3
};
}
log(level, message, data = {}) {
if (this.levels[level] <= this.levels[this.level]) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level: level.toUpperCase(),
message,
...data
};
console.log(JSON.stringify(logEntry, null, 2));
}
}
error(message, data) {
this.log('error', message, data);
}
warn(message, data) {
this.log('warn', message, data);
}
info(message, data) {
this.log('info', message, data);
}
debug(message, data) {
this.log('debug', message, data);
}
}
import { CachedAPIClient } from '../api/cachedClient.js';
import { Logger } from '../utils/logger.js';
export class EnhancedAggregator {
constructor(options = {}) {
this.logger = new Logger({ level: options.logLevel || 'info' });
this.weatherAPI = new CachedAPIClient(
'https://api.weather.example.com',
{
timeout: 3000,
retries: 2,
cacheTTL: 600000, // 10 minutes
requestsPerSecond: 5
}
);
this.newsAPI = new CachedAPIClient(
'https://api.news.example.com',
{
timeout: 3000,
retries: 2,
cacheTTL: 300000, // 5 minutes
requestsPerSecond: 10
}
);
this.stocksAPI = new CachedAPIClient(
'https://api.stocks.example.com',
{
timeout: 3000,
retries: 2,
cacheTTL: 60000, // 1 minute
requestsPerSecond: 20
}
);
}
async fetchAll(config) {
this.logger.info('Starting data aggregation', { config });
const startTime = Date.now();
try {
const results = await Promise.allSettled([
this.fetchWeather(config.city),
this.fetchNews(config.newsCategory),
this.fetchStocks(config.stockSymbol)
]);
const data = {
weather: this.extractResult(results[0], 'weather'),
news: this.extractResult(results[1], 'news'),
stocks: this.extractResult(results[2], 'stocks')
};
const duration = Date.now() - startTime;
this.logger.info('Aggregation complete', {
duration: `${duration}ms`,
success: results.filter(r => r.status === 'fulfilled').length,
failed: results.filter(r => r.status === 'rejected').length
});
return {
success: true,
data,
metadata: {
duration,
timestamp: new Date().toISOString(),
stats: this.getStats()
}
};
} catch (error) {
this.logger.error('Aggregation failed', { error: error.message });
return {
success: false,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
extractResult(result, source) {
if (result.status === 'fulfilled') {
return result.value;
}
this.logger.warn(`Failed to fetch ${source}`, {
error: result.reason.message
});
return null;
}
async fetchWeather(city) {
// Mock implementation - replace with actual API call
return {
city,
temperature: Math.round(Math.random() * 30 + 10),
condition: 'Sunny',
timestamp: new Date().toISOString()
};
}
async fetchNews(category) {
// Mock implementation
return {
category,
articles: [
{ title: 'News 1', source: 'Source 1' },
{ title: 'News 2', source: 'Source 2' }
],
timestamp: new Date().toISOString()
};
}
async fetchStocks(symbol) {
// Mock implementation
return {
symbol,
price: (100 + Math.random() * 10).toFixed(2),
change: (Math.random() - 0.5).toFixed(2),
timestamp: new Date().toISOString()
};
}
getStats() {
return {
weather: this.weatherAPI.getStats(),
news: this.newsAPI.getStats(),
stocks: this.stocksAPI.getStats()
};
}
clearCache() {
this.weatherAPI.cache.clear();
this.newsAPI.cache.clear();
this.stocksAPI.cache.clear();
this.logger.info('Cache cleared');
}
}
import { EnhancedAggregator } from './aggregator/enhanced.js';
async function main() {
const aggregator = new EnhancedAggregator({
logLevel: 'info'
});
const config = {
city: 'London',
newsCategory: 'technology',
stockSymbol: 'AAPL'
};
console.log('=== Data Aggregator ===\n');
// First fetch
console.log('First fetch (no cache)...');
const result1 = await aggregator.fetchAll(config);
console.log('\nResult:', JSON.stringify(result1, null, 2));
// Second fetch (should use cache)
console.log('\n\nSecond fetch (with cache)...');
const result2 = await aggregator.fetchAll(config);
console.log('\nResult:', JSON.stringify(result2, null, 2));
// Print statistics
console.log('\n\n=== Statistics ===');
console.log(JSON.stringify(aggregator.getStats(), null, 2));
}
main().catch(console.error);
import { EnhancedAggregator } from '../src/aggregator/enhanced.js';
describe('EnhancedAggregator', () => {
let aggregator;
beforeEach(() => {
aggregator = new EnhancedAggregator({ logLevel: 'error' });
});
test('fetches all data successfully', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
const result = await aggregator.fetchAll(config);
expect(result.success).toBe(true);
expect(result.data.weather).toBeDefined();
expect(result.data.news).toBeDefined();
expect(result.data.stocks).toBeDefined();
});
test('uses cache on second request', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
await aggregator.fetchAll(config);
const result2 = await aggregator.fetchAll(config);
const stats = aggregator.getStats();
expect(stats.weather.cacheHits).toBeGreaterThan(0);
});
});
In Part 3, we'll add final testing and deployment preparation!