npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cxyi7/request-manager

v0.7.0

Published

Framework-agnostic HTTP request lifecycle, transport adapters, and WebSocket connections.

Readme

@cxyi7/request-manager

与业务框架无关的 HTTP 请求生命周期、策略管线、Transport Adapter 与独立 WebSocket 连接管理。

特性

  • 多服务 RequestClient,实例之间配置和状态隔离。
  • 核心入口不依赖具体 HTTP 实现;原生 Fetch 与 Axios Transport 均通过独立入口按需加载。
  • 可配置响应协议,不绑定任何后端 codeokmessage 结构。
  • Token 注入、并发 single-flight 刷新、单次失效通知和最多一次认证重放。
  • 标准 AbortSignal、超时、总 Deadline 和请求 Scope。
  • share 请求共享与 latest 只保留最新请求。
  • 显式 TTL 内存缓存、LRU 容量限制和 key/tag/用户分区失效。
  • 幂等感知的有上限重试、Full Jitter 和 Retry-After
  • 稳定的 RequestError 错误模型。
  • 只读生命周期与策略观测事件、依赖排序和关键插件失败策略。
  • 独立 WebSocket 入口、有限重连、有界消息队列、显式 Codec 和一次性 Ticket 集成。
  • Fake Transport、Fake WebSocket、Fetch/Axios Transport 契约和浏览器下载工具。

安装

npm install @cxyi7/request-manager axios

只有使用 Axios Transport 时才需要安装 Axios。浏览器和 Node.js 20+ 可以直接使用内置 Fetch Transport:

import { createRequestClient } from '@cxyi7/request-manager';
import { createFetchTransport } from '@cxyi7/request-manager/fetch';

const client = createRequestClient({
  name: 'main',
  baseURL: 'https://api.example.com/v1/',
  transport: createFetchTransport(),
});

Client 创建时会克隆、归一化并冻结配置容器;之后修改调用方持有的 Header、重试规则、认证设置以及已注册的协议、插件或 Transport 方法,不会改变已有 Client。单次请求的策略和普通对象/数组参数也会在进入异步生命周期前建立内部快照。AbortSignal、Transport 内部状态、回调函数闭包和请求 body 等运行时协作者仍保留其预期的动态行为,库不会冻结调用方拥有的原对象。

Fetch Transport 支持 JSON、text、blob、arrayBuffer 和 stream 响应,并自动序列化查询参数和普通对象 JSON 请求体。数值查询参数必须是有限值;NaN 和正负 Infinity 会在创建缓存或去重标识前被拒绝。普通对象只能搭配默认 Content-Type、application/jsonapplication/*+json;其他编码请先转换成对应的字符串、URLSearchParams 或二进制请求体。FormDataBlob、字符串、URLSearchParams、ArrayBuffer 与 ReadableStream 请求体会原样传递。原生 Fetch 没有标准上传进度 API,因此需要 onUploadProgress 时请使用 Axios Transport。

快速开始

import { createRequestClient, type ResponseProtocol } from '@cxyi7/request-manager';
import { createAxiosTransport } from '@cxyi7/request-manager/axios';

interface ApiEnvelope<T> {
  code: number;
  message?: string;
  data: T;
}

const responseProtocol: ResponseProtocol<ApiEnvelope<unknown>> = {
  isSuccess: (payload) => payload.code === 0,
  isAuthExpired: (payload) => payload.code === 30007,
  getData: (payload) => payload.data,
  getCode: (payload) => payload.code,
  getMessage: (payload) => payload.message,
};

const client = createRequestClient({
  name: 'main',
  baseURL: 'https://api.example.com/v1/',
  transport: createAxiosTransport(),
  responseProtocol,
  auth: {
    getAccessToken: () => session.getAccessToken(),
    refreshAccessToken: () => session.refreshAccessToken(),
    onAuthExpired: () => session.notifyExpired(),
  },
});

const user = await client.get<{ id: number; name: string }>('/users/1');

WebSocket

WebSocket 通过独立入口加载,不会进入 HTTP RequestClient、Transport、缓存、去重或请求重试管线:

import { createWebSocketClient } from '@cxyi7/request-manager/websocket';

interface IncomingMessage {
  type: string;
  payload?: unknown;
}

interface OutgoingMessage {
  type: string;
  topic: string;
}

const websocket = createWebSocketClient({
  name: 'realtime',
  baseURL: 'https://api.example.com',
});

const connection = websocket.connect<IncomingMessage, OutgoingMessage>('/realtime', {
  protocols: ['app.v1'],
  ticket: async ({ signal }) => {
    const result = await client.post<{ ticket: string }>('/websocket/ticket', undefined, { signal });
    return { query: { ticket: result.ticket } };
  },
  encode: JSON.stringify,
  decode: (data) => {
    if (typeof data !== 'string') throw new TypeError('Expected a text message');
    return JSON.parse(data) as IncomingMessage;
  },
});

await connection.opened;
await connection.send({ type: 'subscribe', topic: 'orders' });

for await (const message of connection.messages) {
  console.log(message);
}

浏览器默认使用原生 WebSocket。Node.js 20+ 通过 webSocketFactory 注入所选实现,例如 ws。异常关闭可按有限次数和累计时间重连,但不会自动重放已经发送的业务消息;可靠投递、ack、幂等和重订阅仍由应用协议负责。

messages 是单消费者异步迭代流。库因本地策略、子协议、解码或缓冲区超限主动关闭时,分别使用私有关闭码 4000400240074009;这些错误仍通过 WebSocketConnectionError 返回。

浏览器握手不能设置任意 Authorization Header。同源场景优先使用安全 Cookie;其他场景建议先通过 RequestClient 获取短时、单次使用的 ticket。目标地址会在 ticket 获取前通过 allowedOrigins 校验,不应把长期 access token 放进 WebSocket URL。原生 WebSocket API 无法让客户端审核握手重定向,因此携带 ticket 的端点不应重定向到其他 origin,服务端仍必须验证握手 Origin

完整响应信息

普通的 requestgetpost 等方法继续只返回协议解析后的业务数据。需要读取分页、限流、缓存协商或链路追踪响应头时,使用 requestDetailed()

const result = await client.requestDetailed<{ id: number }>({
  method: 'POST',
  url: '/users',
  body: { name: 'Ada' },
});

console.log(result.data.id);
console.log(result.status, result.headers.location, result.url);

RequestResult<T> 包含协议解析后的 data、最终成功响应的 statusstatusText、只读 headers 和可选 url。认证重放或普通重试发生时只返回最后一次成功响应;共享请求的普通调用方与详细调用方复用同一次 Transport 请求。缓存会连同响应信息一起保存,命中时不会重新触发网络生命周期插件。

多服务实例

每个后端服务建立独立 Client。不要通过单请求覆盖 baseURL,也不要让主系统 Token 被发送到第三方服务。

const publicClient = createRequestClient({
  name: 'public',
  baseURL: 'https://public.example.com',
  transport: createAxiosTransport(),
});

const internalClient = createRequestClient({
  name: 'internal',
  baseURL: 'https://internal.example.com',
  transport: createAxiosTransport(),
  auth: { getAccessToken: () => session.getAccessToken() },
});

allowedOrigins 只允许发送请求,不会自动授权携带 Token。确需在额外 Origin 发送相同认证信息时,必须单独配置 auth.allowedOrigins。配置托管 auth 后,匿名请求和未授权的额外 Origin 会移除对应认证 Header;未配置 auth 时,业务主动提供的静态认证 Header 保持不变。

取消与请求 Scope

const scope = client.createScope();

const profile = client.get('/profile', { signal: scope.signal });
const messages = client.get('/messages', { signal: scope.signal });

scope.abort('page unmounted');

client.abortAll() 只取消当前正在进行的请求,不会永久关闭 Client。总 Deadline 包含请求尝试和重试等待;无论在哪个阶段到期,都会返回不可重试的 timeout 错误。

去重模式

// 相同查询共享一个进行中的 Promise。
client.get('/dictionary', {
  anonymous: true,
  deduplicate: { mode: 'share' },
});

// 新搜索会取消旧搜索。
client.get('/search', {
  anonymous: true,
  params: { keyword },
  deduplicate: { mode: 'latest', key: 'global-search' },
});

读取请求的默认 Key 使用 method、URL、稳定序列化后的 params、认证模式、响应类型和最终静态请求头。非匿名请求或显式携带凭证头的请求还必须提供稳定的 deduplicate.partitionKey,防止登录用户或租户切换时共享旧身份的响应。凭证本身不会进入 Key:

client.get('/profile', {
  deduplicate: {
    mode: 'share',
    partitionKey: `user:${userId}`,
  },
});

share 只共享底层请求,每个调用者仍获得独立 Promise 和取消语义。单个调用者取消不会影响其他调用者;所有调用者均取消后才会取消底层请求。写请求必须提供显式 deduplicate.key;自定义 Key 时,调用方负责保证被合并请求的安全上下文和返回契约一致。上传和下载不自动启用去重。

TTL 内存缓存

缓存默认关闭,只在请求明确配置 cache 时启用。公共字典接口应标记为匿名请求:

const statusDictionary = await client.get('/dictionary/status', {
  anonymous: true,
  cache: {
    ttlMs: 5 * 60_000,
    key: 'dictionary:status',
    tags: ['dictionary'],
  },
  deduplicate: { mode: 'share' },
});

ttlMs 从成功响应写入缓存时开始计算。缓存命中直接返回协议解析后的最终结果,不访问 Transport,也不触发网络生命周期插件。相同缓存未命中只有在显式配置 share 时才合并;不同请求仍会并发发送。

每个 Client 都有独立的 LRU 内存缓存,默认最多保存 100 条,也可以限制容量:

const client = createRequestClient({
  name: 'main',
  baseURL: 'https://api.example.com',
  transport: createAxiosTransport(),
  cache: { maxEntries: 200 },
});

非匿名请求或显式携带认证、Cookie、Token、API Key 等凭证头的请求,必须提供稳定的用户分区,且凭证本身不会进入缓存 Key。anonymous: true 只应用于不依赖用户 Cookie 或其他身份信息的公共接口:

await client.get('/profile/preferences', {
  cache: {
    ttlMs: 60_000,
    partitionKey: `user:${userId}`,
    tags: ['preferences'],
  },
});

可以按业务 Key、任一 Tag、用户分区或全量主动失效:

client.invalidateCache({ key: 'dictionary:status' });
client.invalidateCache({ tags: ['dictionary'] });
client.invalidateCache({ partitionKey: `user:${userId}` });
client.clearCache();

失效操作会阻止失效前已启动的请求重新写回旧数据。第一版只缓存成功的 GET/HEAD JSON 或文本结果;HTTP、业务和认证失败不会缓存,写请求、Blob、Stream、ArrayBuffer、上传与下载不支持缓存。缓存只存在于内存中,页面刷新或进程退出后自动清空。

重试

GET、HEAD 默认允许重试。POST、PATCH 必须提供幂等键才能重试:

await client.post('/orders', order, {
  retry: {
    idempotencyKey: crypto.randomUUID(),
    maxAttempts: 2,
  },
});

幂等键只能在单次请求的 retry 中配置,不能作为 Client 默认值复用于多个写请求。客户端幂等键只有在服务端识别并持久化该键时才真正有效。不得仅依靠前端配置保证写操作幂等。

错误处理

import { isRequestError } from '@cxyi7/request-manager';

try {
  await client.get('/users/1');
} catch (error) {
  if (isRequestError(error)) {
    console.log(error.kind, error.status, error.code, error.requestId);
  }
}

错误种类:networktimeouthttpbusinessauthabortpolicyparse

成功 HTTP 响应中的非法 JSON 会返回不可重试的 parse 错误,不会按网络故障自动重试。

核心包不显示 Toast、不打开 Modal、不操作路由。UI 行为由项目层根据 RequestError 决定。

插件

插件只消费只读生命周期事件,适合日志和可观测性。认证、重试、Origin 等影响安全和正确性的能力属于内置策略,不通过万能 Hook 暴露。

const telemetryPlugin = {
  name: 'telemetry',
  onRequestEnd(event) {
    metrics.observe(event.clientName, event.durationMs, event.status);
  },
  onRetryScheduled(event) {
    metrics.retry(event.clientName, event.delayMs, event.error.kind);
  },
  onCacheHit(event) {
    metrics.cacheHit(event.clientName, event.url);
  },
};

除请求开始、尝试结束、请求结束和错误生命周期外,插件还可以观测以下内置策略:

| Hook | 事件内容 | |---|---| | onCacheHit / onCacheMiss | 显式缓存查询的命中或未命中 | | onDeduplicate | shareleader / subscriber,或 lateststart / supersede | | onRetryScheduled | 当前尝试、重试序号、下一次尝试、等待时间和只读错误 | | onAuthRefresh | startjoinsuccessfailureunavailable |

策略事件包含调用方自己的 requestIdshare 订阅者还会收到 leaderRequestId,用于关联实际执行 Transport 的主请求。事件不会暴露缓存 Key、去重 Key、分区、Token 或认证 Header。

策略观测 Hook 只用于诊断:按照插件顺序同步投递,但不会等待其返回的 Promise;同步异常和异步拒绝始终被隔离,即使插件声明了 critical: true 也不会改变请求结果。普通生命周期 Hook 继续遵循原有关键插件失败语义。缓存命中只触发专用 onCacheHit,不会伪造网络请求生命周期。

  • 插件名称必须唯一。
  • requires 中的依赖必须存在且不能成环。
  • 普通生命周期中的非关键插件异常会被隔离。
  • 普通生命周期中的 critical: true 插件异常会产生 policy 错误。
  • coreRange 会在创建 Client 时校验,支持精确版本、带完整版本的 ^/~x 通配、带完整版本的空格分隔比较器以及 || 备选范围。

浏览器下载

import { saveDownload } from '@cxyi7/request-manager/browser';

const response = await client.download('/reports/export', { month: '2026-08' });
saveDownload(response, { filename: 'report.xlsx' });

工具会解析 Content-Disposition、清理文件名、移除临时 DOM 节点并释放 Object URL。

上传使用标准 FormData,不要手工设置 multipart boundary:

const form = new FormData();
form.append('file', file);
await client.upload('/files', form, {
  onUploadProgress: ({ loaded, total, progress }) => {
    console.log({ loaded, total, progress });
  },
});

测试工具

import { createFakeTransport, jsonResponse } from '@cxyi7/request-manager/testing';

const transport = createFakeTransport((request) =>
  jsonResponse({ method: request.method, url: request.url }),
);

Fake Transport 不访问真实网络,并保留只读请求记录,适合单元测试和契约测试。

WebSocket 测试可使用同一入口的 createFakeWebSocketFactory(),主动控制 open、receive、异常关闭和发送缓冲区,不需要访问真实网络。

公共入口

| 入口 | 内容 | |---|---| | @cxyi7/request-manager | Client、错误、类型和重试工具 | | @cxyi7/request-manager/axios | Axios Transport | | @cxyi7/request-manager/fetch | 原生 Fetch Transport | | @cxyi7/request-manager/browser | 浏览器下载工具 | | @cxyi7/request-manager/websocket | 独立 WebSocket Client、连接、错误和类型 | | @cxyi7/request-manager/testing | Fake Transport 与 Fake WebSocket |

未在 package.json#exports 声明的内部路径不属于公共 API。

开发与发布验证

npm install
npm run typecheck
npm test
npm run build
npm pack --dry-run

npm run smoke:package 会构建真实 npm tarball,在独立临时项目中验证可选 Axios 依赖、全部公开运行时入口和 TypeScript 类型入口,并在结束后自动清理。npm run check 会依次执行版本一致性检查、Lint、类型检查、测试及这项真实安装验证。

V1 非目标

  • SSE、HTTP Streaming、长轮询和 WebTransport。
  • 默认缓存、SWR 和离线写队列。
  • Service Worker 或全局 fetch 猴子补丁。
  • Vue、React、Pinia 或 UI 组件。
  • 浏览器端服务密钥管理。

版本策略

  • Patch:不改变公共行为的内部修复。
  • Minor:向后兼容的新配置、事件或公共入口。
  • Major:默认重试、错误语义、导出或插件契约的不兼容变更。