Core Docs
@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 状态——这些交给调用层自行决定。

On this page