@skyroc/utils
Singleflight
合并同 key 的并发请求,保证同一时刻相同 key 只有一个 Promise 在飞行,结果共享给所有调用方
概述
Singleflight 解决的问题:同一个异步操作被并发重复调用时,不必要地发起多次请求。
❌ 没有 Singleflight
用户 A → fetchProfile('alice') → 发起请求①
用户 B → fetchProfile('alice') → 发起请求②(重复!)
用户 C → fetchProfile('alice') → 发起请求③(重复!)
✅ 有 Singleflight
用户 A → fetchProfile('alice') → 发起请求①
用户 B → fetchProfile('alice') → 等待请求①的结果
用户 C → fetchProfile('alice') → 等待请求①的结果
请求①完成 → 三方共享同一个结果名称来自 Go 标准库的 singleflight 包,语义完全一致。
两种使用形式
类形式 — Singleflight
适合在 Service 类中组合使用,一个 Service 持有一个 Singleflight 实例:
import { Singleflight } from '@skyroc/utils';
class UserService {
private sf = new Singleflight();
async fetchProfile(id: string) {
// 相同 id 的并发调用共享同一个 Promise
return this.sf.do(`profile:${id}`, () =>
fetch(`/api/users/${id}/profile`).then(r => r.json())
);
}
async fetchPermissions(userId: string) {
return this.sf.do(`perms:${userId}`, () =>
fetch(`/api/users/${userId}/permissions`).then(r => r.json())
);
}
}
const userService = new UserService();
// 三次调用同时触发,只会实际发出一次 /api/users/alice/profile 请求
const [a, b, c] = await Promise.all([
userService.fetchProfile('alice'),
userService.fetchProfile('alice'),
userService.fetchProfile('alice'),
]);工厂函数形式 — createSingleflight
适合在模块作用域创建一个独立的 singleflight 调用函数,更轻量:
import { createSingleflight } from '@skyroc/utils';
const sf = createSingleflight();
async function fetchConfig() {
return sf('app-config', () =>
fetch('/api/config').then(r => r.json())
);
}API
Singleflight 类
sf.do(key, fn)
执行 fn,返回 Promise。
- 若当前
key已有飞行中的 Promise,直接返回同一个 Promise(fn不会再被调用) fn的 Promise 落定(resolve 或 reject)后,该key的缓存自动清除- 失败也会清除缓存,下次调用会重新执行
fn
const result = await sf.do('my-key', async () => {
return fetch('/api/data').then(r => r.json());
});sf.forget(key)
手动移除某个 key 的飞行中 Promise,强制下次调用重新执行 fn:
sf.forget('profile:alice');
// 此后 sf.do('profile:alice', fn) 会重新发起请求sf.reset()
清除所有 key 的缓存。
sf.reset();createSingleflight()
工厂函数,返回一个 (key, fn) => Promise<T> 函数,内部持有 Singleflight 实例。
const sf = createSingleflight<string>(); // 泛型指定 key 的类型,默认 string
const data = await sf('my-key', fetchData);Key 的设计建议
Key 是区分"是否同一个请求"的唯一依据,设计时建议带上业务维度:
// ✅ 带 id,不同用户互不干扰
sf.do(`profile:${userId}`, () => fetchProfile(userId));
// ✅ 带参数组合
sf.do(`search:${keyword}:${page}`, () => search(keyword, page));
// ❌ 过于宽泛:所有用户的 profile 请求都会被合并
sf.do('profile', () => fetchProfile(userId));与其他去重方案对比
| 方案 | 合并并发 | 缓存结果 | 适合场景 |
|---|---|---|---|
Singleflight | ✅ | ❌(落定即清除) | 防止并发重复请求,不需要缓存 |
| TanStack Query | ✅ | ✅(可配置 staleTime) | 完整的请求状态管理 |
手动 Map<key, Promise> | ✅ | 需自己管理 | 自定义缓存策略 |
Singleflight 定位是最轻量的并发合并工具,不做缓存、不管失败重试、不管 loading 状态——这些交给调用层自行决定。