JavaScript Promises Tutorial - Section 8: Course Project
mkdir data-aggregator
cd data-aggregator
npm init -y
npm install --save-dev jest
{
"name": "data-aggregator",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "jest",
"start": "node src/index.js"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
/**
* Create a Promise that resolves after specified milliseconds
*/
export function delay(ms, value) {
return new Promise(resolve => {
setTimeout(() => resolve(value), ms);
});
}
/**
* Create a delay with cancellation support
*/
export function cancellableDelay(ms, signal) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => resolve(), ms);
if (signal) {
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error('Cancelled'));
});
}
});
}
/**
* Add timeout to any Promise
*/
export function timeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
})
]);
}
/**
* Timeout with custom error
*/
export function timeoutWithError(promise, ms, errorMessage) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => reject(new Error(errorMessage)), ms);
})
]);
}
import { delay } from './delay.js';
/**
* Retry failed operations with exponential backoff
*/
export async function retry(
fn,
options = {}
) {
const {
retries = 3,
baseDelay = 1000,
maxDelay = 30000,
onRetry = null
} = options;
for (let attempt = 0; attempt < retries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === retries - 1) {
throw error;
}
const delayTime = Math.min(
baseDelay * Math.pow(2, attempt),
maxDelay
);
if (onRetry) {
onRetry(attempt + 1, retries, error);
}
await delay(delayTime);
}
}
}
/**
* Retry with jitter
*/
export async function retryWithJitter(fn, options = {}) {
const {
retries = 3,
baseDelay = 1000,
maxDelay = 30000
} = options;
for (let attempt = 0; attempt < retries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === retries - 1) {
throw error;
}
const exponentialDelay = baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * exponentialDelay;
const delayTime = Math.min(exponentialDelay + jitter, maxDelay);
await delay(delayTime);
}
}
}
import { timeout } from '../utils/timeout.js';
import { retry } from '../utils/retry.js';
export class APIClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.timeout = options.timeout || 5000;
this.retries = options.retries || 3;
this.headers = options.headers || {};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const fetchWithTimeout = timeout(
fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...this.headers,
...options.headers
}
}),
this.timeout
);
return await retry(
async () => {
const response = await fetchWithTimeout;
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
{
retries: this.retries,
onRetry: (attempt, total, error) => {
console.log(`Retry ${attempt}/${total}: ${error.message}`);
}
}
);
}
async get(endpoint, options = {}) {
return await this.request(endpoint, {
...options,
method: 'GET'
});
}
async post(endpoint, data, options = {}) {
return await this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
}
import { APIClient } from './client.js';
import { delay } from '../utils/delay.js';
export class WeatherAPI extends APIClient {
constructor() {
super('https://api.weather.example.com', {
timeout: 3000,
retries: 2
});
}
async getWeather(city) {
// Simulate API call
await delay(Math.random() * 1000);
return {
city,
temperature: Math.round(Math.random() * 30 + 10),
condition: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)],
humidity: Math.round(Math.random() * 100),
timestamp: new Date().toISOString()
};
}
}
import { APIClient } from './client.js';
import { delay } from '../utils/delay.js';
export class NewsAPI extends APIClient {
constructor() {
super('https://api.news.example.com', {
timeout: 3000,
retries: 2
});
}
async getHeadlines(category = 'general') {
// Simulate API call
await delay(Math.random() * 1000);
return {
category,
articles: [
{
title: 'Breaking News 1',
source: 'News Source',
publishedAt: new Date().toISOString()
},
{
title: 'Breaking News 2',
source: 'News Source',
publishedAt: new Date().toISOString()
}
],
timestamp: new Date().toISOString()
};
}
}
import { APIClient } from './client.js';
import { delay } from '../utils/delay.js';
export class StocksAPI extends APIClient {
constructor() {
super('https://api.stocks.example.com', {
timeout: 3000,
retries: 2
});
}
async getQuote(symbol) {
// Simulate API call
await delay(Math.random() * 1000);
const basePrice = 100;
const change = (Math.random() - 0.5) * 10;
return {
symbol,
price: (basePrice + change).toFixed(2),
change: change.toFixed(2),
changePercent: ((change / basePrice) * 100).toFixed(2),
volume: Math.floor(Math.random() * 1000000),
timestamp: new Date().toISOString()
};
}
}
import { WeatherAPI } from '../api/weatherAPI.js';
import { NewsAPI } from '../api/newsAPI.js';
import { StocksAPI } from '../api/stocksAPI.js';
export class DataAggregator {
constructor() {
this.weatherAPI = new WeatherAPI();
this.newsAPI = new NewsAPI();
this.stocksAPI = new StocksAPI();
}
async fetchAll(config) {
const { city, newsCategory, stockSymbol } = config;
try {
const [weather, news, stocks] = await Promise.all([
this.weatherAPI.getWeather(city),
this.newsAPI.getHeadlines(newsCategory),
this.stocksAPI.getQuote(stockSymbol)
]);
return {
success: true,
data: {
weather,
news,
stocks
},
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
async fetchWithFallback(config) {
const results = await Promise.allSettled([
this.weatherAPI.getWeather(config.city),
this.newsAPI.getHeadlines(config.newsCategory),
this.stocksAPI.getQuote(config.stockSymbol)
]);
return {
weather: results[0].status === 'fulfilled' ? results[0].value : null,
news: results[1].status === 'fulfilled' ? results[1].value : null,
stocks: results[2].status === 'fulfilled' ? results[2].value : null,
timestamp: new Date().toISOString()
};
}
}
import { DataAggregator } from './aggregator/index.js';
async function main() {
const aggregator = new DataAggregator();
const config = {
city: 'London',
newsCategory: 'technology',
stockSymbol: 'AAPL'
};
console.log('Fetching data...\n');
const result = await aggregator.fetchAll(config);
if (result.success) {
console.log('✅ Data fetched successfully!\n');
console.log('Weather:', result.data.weather);
console.log('\nNews:', result.data.news);
console.log('\nStocks:', result.data.stocks);
} else {
console.error('❌ Failed to fetch data:', result.error);
}
}
main().catch(console.error);
import { delay } from '../../src/utils/delay.js';
describe('delay', () => {
test('resolves after specified time', async () => {
const start = Date.now();
await delay(100);
const elapsed = Date.now() - start;
expect(elapsed).toBeGreaterThanOrEqual(100);
expect(elapsed).toBeLessThan(150);
});
test('resolves with value', async () => {
const value = await delay(10, 'test');
expect(value).toBe('test');
});
});
# Run the application
npm start
# Run tests
npm test
In Part 2, we'll add caching, rate limiting, and advanced error handling!