JavaScript Promises Tutorial - Section 8: Course Project
// tests/utils/retry.test.js
import { retry } from '../../src/utils/retry.js';
describe('retry', () => {
test('succeeds on first attempt', async () => {
const fn = jest.fn().mockResolvedValue('success');
const result = await retry(fn, { retries: 3 });
expect(result).toBe('success');
expect(fn).toHaveBeenCalledTimes(1);
});
test('retries on failure', async () => {
const fn = jest.fn()
.mockRejectedValueOnce(new Error('Fail 1'))
.mockRejectedValueOnce(new Error('Fail 2'))
.mockResolvedValueOnce('success');
const result = await retry(fn, { retries: 3 });
expect(result).toBe('success');
expect(fn).toHaveBeenCalledTimes(3);
});
test('throws after max retries', async () => {
const fn = jest.fn().mockRejectedValue(new Error('Always fails'));
await expect(retry(fn, { retries: 2 }))
.rejects.toThrow('Always fails');
expect(fn).toHaveBeenCalledTimes(2);
});
});
// tests/integration/aggregator.test.js
import { EnhancedAggregator } from '../../src/aggregator/enhanced.js';
describe('EnhancedAggregator Integration', () => {
let aggregator;
beforeEach(() => {
aggregator = new EnhancedAggregator({ logLevel: 'error' });
});
afterEach(() => {
aggregator.clearCache();
});
describe('fetchAll', () => {
test('fetches data from all sources', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
const result = await aggregator.fetchAll(config);
expect(result.success).toBe(true);
expect(result.data).toHaveProperty('weather');
expect(result.data).toHaveProperty('news');
expect(result.data).toHaveProperty('stocks');
expect(result.metadata).toHaveProperty('duration');
expect(result.metadata).toHaveProperty('stats');
});
test('handles partial failures gracefully', async () => {
const config = {
city: 'InvalidCity',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
const result = await aggregator.fetchAll(config);
expect(result.success).toBe(true);
// Some data may be null due to failures
expect(result.data).toBeDefined();
});
});
describe('caching', () => {
test('uses cache on subsequent requests', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
// First request
await aggregator.fetchAll(config);
// Second request (should use cache)
await aggregator.fetchAll(config);
const stats = aggregator.getStats();
// At least one cache hit
const totalCacheHits =
stats.weather.cacheHits +
stats.news.cacheHits +
stats.stocks.cacheHits;
expect(totalCacheHits).toBeGreaterThan(0);
});
test('cache can be cleared', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
await aggregator.fetchAll(config);
aggregator.clearCache();
const stats = aggregator.getStats();
expect(stats.weather.cache.size).toBe(0);
});
});
});
// tests/performance/benchmark.test.js
import { EnhancedAggregator } from '../../src/aggregator/enhanced.js';
describe('Performance Benchmarks', () => {
let aggregator;
beforeEach(() => {
aggregator = new EnhancedAggregator({ logLevel: 'error' });
});
test('completes within acceptable time', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
const start = performance.now();
await aggregator.fetchAll(config);
const duration = performance.now() - start;
// Should complete in under 5 seconds
expect(duration).toBeLessThan(5000);
});
test('cache improves performance', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
// First request (no cache)
const start1 = performance.now();
await aggregator.fetchAll(config);
const duration1 = performance.now() - start1;
// Second request (with cache)
const start2 = performance.now();
await aggregator.fetchAll(config);
const duration2 = performance.now() - start2;
// Cached request should be faster
expect(duration2).toBeLessThan(duration1);
});
test('handles concurrent requests efficiently', async () => {
const config = {
city: 'London',
newsCategory: 'tech',
stockSymbol: 'AAPL'
};
const start = performance.now();
await Promise.all([
aggregator.fetchAll(config),
aggregator.fetchAll(config),
aggregator.fetchAll(config)
]);
const duration = performance.now() - start;
// Should handle 3 concurrent requests efficiently
expect(duration).toBeLessThan(6000);
});
});
export default {
testEnvironment: 'node',
transform: {},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
# Data Aggregator
A production-ready data aggregation system built with JavaScript Promises.
## Features
- ✅ Parallel data fetching from multiple sources
- ✅ Intelligent caching with TTL
- ✅ Rate limiting to prevent API throttling
- ✅ Automatic retry with exponential backoff
- ✅ Comprehensive error handling
- ✅ Performance monitoring and statistics
- ✅ Full test coverage
## Installation
```bash
npm install
```
## Usage
```javascript
import { EnhancedAggregator } from './src/aggregator/enhanced.js';
const aggregator = new EnhancedAggregator({
logLevel: 'info'
});
const result = await aggregator.fetchAll({
city: 'London',
newsCategory: 'technology',
stockSymbol: 'AAPL'
});
console.log(result);
```
## Running Tests
```bash
# Run all tests
npm test
# Run with coverage
npm test -- --coverage
# Run specific test file
npm test -- tests/utils/retry.test.js
```
## Performance
- Average response time: < 2 seconds
- Cache hit rate: > 80% after warm-up
- Supports 10+ concurrent requests
- Memory efficient with LRU cache
## Architecture
```
src/
├── api/ # API clients
├── cache/ # Caching layer
├── utils/ # Utility functions
└── aggregator/ # Main aggregator logic
```
## Configuration
Configure via constructor options:
```javascript
const aggregator = new EnhancedAggregator({
logLevel: 'info', // Log level
cacheTTL: 300000, # Cache TTL in ms
requestsPerSecond: 10 // Rate limit
});
```
## License
MIT
WEATHER_API_KEY=your_key_here
NEWS_API_KEY=your_key_here
STOCKS_API_KEY=your_key_here
CACHE_TTL=300000
RATE_LIMIT_RPS=10
LOG_LEVEL=info
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src ./src
EXPOSE 3000
CMD ["node", "src/index.js"]
version: '3.8'
services:
aggregator:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- LOG_LEVEL=info
restart: unless-stopped
// src/server.js
import express from 'express';
import { EnhancedAggregator } from './aggregator/enhanced.js';
const app = express();
const aggregator = new EnhancedAggregator();
app.get('/health', (req, res) => {
const stats = aggregator.getStats();
res.json({
status: 'healthy',
uptime: process.uptime(),
memory: process.memoryUsage(),
stats
});
});
app.get('/api/aggregate', async (req, res) => {
try {
const result = await aggregator.fetchAll({
city: req.query.city || 'London',
newsCategory: req.query.category || 'tech',
stockSymbol: req.query.symbol || 'AAPL'
});
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
data-aggregator/
├── src/
│ ├── api/
│ │ ├── client.js
│ │ ├── cachedClient.js
│ │ ├── weatherAPI.js
│ │ ├── newsAPI.js
│ │ └── stocksAPI.js
│ ├── cache/
│ │ └── index.js
│ ├── utils/
│ │ ├── delay.js
│ │ ├── timeout.js
│ │ ├── retry.js
│ │ ├── rateLimiter.js
│ │ └── logger.js
│ ├── aggregator/
│ │ ├── index.js
│ │ └── enhanced.js
│ ├── server.js
│ └── index.js
├── tests/
│ ├── utils/
│ ├── integration/
│ └── performance/
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── jest.config.js
├── package.json
└── README.md
You've completed the JavaScript Promises course and built a production-ready data aggregator!
Thank you for completing this comprehensive JavaScript Promises tutorial. You now have the skills to build robust, performant async applications. Keep practicing and happy coding! 🎉