singleflight-js
v1.0.3
Published
Go-style singleflight implementation for Node.js/TypeScript
Downloads
28
Maintainers
Readme
singleflight-js
Go-style singleflight implementation for Node.js / TypeScript.
Deduplicate concurrent function calls with the same key and share the result.
Installation
yarn add singleflight-jsQuick Start
import { Group } from 'singleflight-js';
const group = new Group();
// Expensive operation (API call, database query, etc.)
async function fetchUser(userId: string) {
console.log(`Fetching user ${userId}...`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay
return { id: userId, name: `User ${userId}` };
}
// Multiple concurrent calls with the same key
const promises = [
group.do('user:123', () => fetchUser('123')),
group.do('user:123', () => fetchUser('123')),
group.do('user:123', () => fetchUser('123'))
];
const results = await Promise.all(promises);
// Only one actual fetchUser() call is made!
// First call: { value: { id: '123', name: 'User 123' }, shared: false }
// Other calls: { value: { id: '123', name: 'User 123' }, shared: true }Express.js Example
import express from 'express';
import { Group } from 'singleflight-js';
const app = express();
const group = new Group();
// Simulate database query
async function getUserFromDB(userId: string) {
console.log(`🔍 Querying database for user ${userId}...`);
await new Promise(resolve => setTimeout(resolve, 2000)); // 2s delay
return { id: userId, name: `User ${userId}`, email: `user${userId}@example.com` };
}
app.get('/users/:id', async (req, res) => {
const userId = req.params.id;
try {
// Multiple concurrent requests for the same user will be deduplicated
const result = await group.do(`user:${userId}`, () => getUserFromDB(userId));
res.json({
data: result.value,
shared: result.shared, // true if this request shared the result
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
console.log('Try: curl http://localhost:3000/users/123');
});Key Benefits
- Prevents duplicate work: Multiple concurrent requests for the same resource execute only once
- Automatic result sharing: All waiting requests get the same result
- Error propagation: If the operation fails, all waiting requests receive the same error
- Memory efficient: No caching overhead, results are shared only during execution
- TypeScript support: Full type safety with generic return types
API
Group
do<T>(key: string, fn: () => Promise<T>): Promise<{ value: T; shared: boolean }>
Execute function fn for the given key. If there's already a call in progress for the same key, wait for it and share the result.
Returns:
value: The result from the functionshared:falsefor the original call,truefor subsequent calls that shared the result
forget(key: string): void
Remove the key from the group, allowing new calls for the same key to execute independently. Useful for cache invalidation.
// Update user data and invalidate ongoing requests
group.forget('user:123');
await updateUser('123', newData);Use Cases
- API deduplication: Prevent multiple identical API calls
- Database query optimization: Share expensive query results
- Cache warming: Coordinate cache updates
- Rate limiting: Control concurrent operations to external services
- Microservices: Reduce load on downstream services
Examples
See the example/ directory for more detailed examples:
basic-usage.ts- Basic deduplication patternsapi-cache.ts- API caching with error handlingadvanced-usage.ts- Circuit breaker and advanced patterns
Development
# Install dependencies
yarn install
# Build
yarn build
# Run examples
make run-example EXAMPLE=basic-usage
make run-example EXAMPLE=api-cache
# See all available commands
make helpLicense
MIT
🔗 Links:
