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

@sv-kit/query-kit

v1.0.0

Published

Headless query/cache core: dedupe, SWR, retry/backoff, TTL/GC, cancel, prefetch, invalidate, and hydrate/dehydrate.

Downloads

8

Readme

@sv-kit/query-kit

无 UI 的查询/缓存核心库:去重并发、SWR(stale-while-revalidate)、TTL/GC、重试退避、Abort 取消、预取、失效、持久化快照(hydrate/dehydrate)。 零第三方依赖,纯 TypeScript,可配任何上层(Svelte/React/Node 都能用)。🧠⚡

  • 🔁 去重并发:同一 key 并发只打一次网
  • 🔄 SWR:先回旧数据,再后台新鲜化
  • ⏱️ TTL/GC:可配置过期与垃圾回收
  • 🧯 重试退避:指数退避 + 抖动
  • 🛑 取消:AbortSignal & 精准匹配取消
  • 🚀 预取/失效:主动 warming 与刷新
  • 🔒 hydrate/dehydrate:SSR/持久化快照

只是“核心”,不绑定 UI、不绑定框架。你可以把它包成 Svelte/React hooks,或直接在 Node 里用。


安装

pnpm add @sv-kit/query-kit
# 或 npm / yarn

快速上手

import { QueryClient, httpJSON } from "@sv-kit/query-kit";

const qc = new QueryClient({
  staleTime: 10_000,                 // 命中 10s 内不重新请求
  gcTime: 5 * 60_000,                // 5 分钟无人订阅则回收
  swr: true,
  retry: { attempts: 2, baseMs: 500 } // 失败重试 2 次,指数退避
});

// 定义 key(可嵌套对象,内部会稳定哈希)
const key = ["GET", "/api/user", { id: 42 }];

// 1) 拉取(自动去重 + SWR + 重试)
const user = await qc.fetchQuery(key, httpJSON(() => `/api/user?id=42`));

// 2) 订阅(配合 UI)
const off = qc.subscribe(key, (ev) => {
  if (ev.type === "update" || ev.type === "fetchSuccess") {
    console.log("user updated", ev);
  }
});

// 3) 失效/预取
qc.invalidate((hash) => hash.includes("/api/user"));
qc.prefetchQuery(key, httpJSON(`/api/user?id=42`));

// 4) 取消(页面切走、组件卸载等)
qc.cancel((hash) => hash.startsWith('q$["GET","/api/user"'));

// 5) SSR:dehydrate/hydrate
const snap = qc.dehydrate();
// --- 传给客户端后 ---
qc.hydrate(snap);

核心概念

QueryKey

string | unknown[],推荐数组(更稳且可读)。库内通过稳定哈希序列化,最终变成以 q$ 开头的可前缀匹配字符串。

SWR(stale-while-revalidate)

命中但已过期时:立即返回旧数据,同时后台刷新,刷新成功会派发事件(订阅方更新 UI)。

TTL / GC

  • staleTime:多久后判定为“过期但可用”
  • gcTime:多久后“无人订阅 + 非进行中”可回收

API 参考

import { QueryClient, httpJSON, stableHash } from "@sv-kit/query-kit";
import type {
  QueryKey, QueryOptions, Fetcher, RetryPolicy, Match,
  DehydratedState, QueryEvent
} from "@sv-kit/query-kit";

class QueryClient(defaults?)

defaults(全部可选):

{
  staleTime?: number;          // 默认 0(立即过期 → SWR 背景刷新)
  gcTime?: number;             // 默认 300_000(5 分钟)
  swr?: boolean;               // 默认 true
  retry?: RetryPolicy;         // 默认 { attempts:3, baseMs:600, jitter:true }
}

fetchQuery<T>(key, fetcher, options?): Promise<T>

  • 命中未过期 → 直接返回缓存
  • 命中过期 + swr:true → 返回旧值,同时后台刷新
  • 其余 → 等待请求完成后返回 options:
type QueryOptions<T = unknown> = {
  staleTime?: number; gcTime?: number;
  retry?: RetryPolicy; swr?: boolean;
  signal?: AbortSignal;       // 外部取消
  select?: (data: any) => T;  // 投影/挑选子集
  meta?: Record<string, unknown>;
};

prefetchQuery<T>(key, fetcher, options?)

后台预拉取,失败忽略。

getQueryData<T>(key): T | undefined

读当前缓存(不触发网络)。

setQueryData<T>(key, updater: T | (prev?: T) => T)

直接写缓存(乐观更新、表单草稿等)。

subscribe<T>(key, (ev: QueryEvent<T>) => void): () => void

订阅事件:fetchStart | fetchSuccess | fetchError | update | invalidate | remove

invalidate(match?: Match)

标记匹配项“已过期”,订阅者会收到 invalidate,下次 fetchQuery 会刷新。 Match 可以是前缀字符串RegExp(hash)=>boolean

cancel(match?: Match)

对匹配项调用 AbortController.abort(),会导致 in-flight 请求以 AbortError 失败。

dehydrate(): DehydratedState / hydrate(state, {gcTime?})

把缓存序列化(仅包含 data/updatedAt/staleAt),便于 SSR/持久化。hydrate 时可覆盖 gcTime


Fetcher & 工具

Fetcher<T>

type Fetcher<T> = (ctx: {
  key: QueryKey;
  hash: string;
  signal: AbortSignal;     // 用于取消
  meta?: Record<string, unknown>;
}) => Promise<T>;

httpJSON(input, init?)

便捷 JSON fetcher:

const getUser = httpJSON(() => `/api/user?id=42`);
const data = await qc.fetchQuery(["GET","/api/user",{id:42}], getUser);
  • input 可是字符串/URL,或函数(拿到 key 动态构造)
  • 按响应头 content-type 解析 JSON,否则返回文本

stableHash(value)

把任意对象稳定序列化成哈希字符串(键排序、跳过 undefined/function/symbol,处理循环引用),所有缓存与匹配都基于它。


常见模式

乐观更新 + 回滚

const key = ["todo", { id }];
const prev = qc.getQueryData<any>(key);
qc.setQueryData(key, (p) => ({ ...p, title: newTitle }));
try {
  await fetch("/api/todo/"+id, { method:"PUT", body: JSON.stringify({ title:newTitle }) });
  qc.invalidate(stableHash(key)); // 或直接 setQueryData 为服务端返回值
} catch (e) {
  qc.setQueryData(key, prev!); // 回滚
}

组合取消(外部 + 内部)

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 2000); // 2s 超时

await qc.fetchQuery(key, fetcher, { signal: ctrl.signal });

批量失效 / 预取

// 失效一类接口
qc.invalidate((h) => h.startsWith('q$["GET","/api/list"'));
// 预热热门数据
qc.prefetchQuery(["GET","/api/hot"], httpJSON("/api/hot"));

与框架集成(示例:把订阅包成 Svelte/React Store)

Svelte 派生 store

import { readable } from "svelte/store";
export function queryStore<T>(qc: QueryClient, key: QueryKey, fetcher: Fetcher<T>) {
  return readable<{data?:T; error?:any; loading:boolean}>({ loading: true }, (set) => {
    let mounted = true;
    const unsub = qc.subscribe<T>(key, (ev) => {
      if (!mounted) return;
      if (ev.type === "fetchStart") set((s) => ({ ...s, loading: true }));
      if (ev.type === "fetchSuccess" || ev.type === "update") set({ data: qc.getQueryData<T>(key), loading: false, error: undefined });
      if (ev.type === "fetchError") set({ data: qc.getQueryData<T>(key), loading: false, error: ev.error });
    });
    qc.fetchQuery(key, fetcher).catch(()=>{});
    return () => { mounted = false; unsub(); };
  });
}

React Hook(简化版思路):用 useSyncExternalStore 订阅 qc.subscribe,在 useEffectfetchQuery


FAQ

Q:和 React Query/SWR 的关系? A:同类能力的“最小实现”,无框架耦合、无依赖,适合你在多端/多框架统一封装。

Q:为什么 key 用数组更好? A:对象会被稳定序列化,避免顺序抖动;数组语义清晰([method, url, params])。

Q:SWR 会不会导致旧数据覆盖新数据? A:不会。SWR 的刷新完成后会以“更新”事件覆盖旧值;你也可关闭 swr:false 改为“强一致等待”。

Q:如何只按 pathname 而忽略查询做失效? A:用前缀或正则匹配 hashqc.invalidate(h => h.startsWith('q$["GET","/api/list"'))


许可

MIT


路线图

  • 并发限速(per-domain concurrency)
  • 依赖联动(A 成功后失效/刷新 B)
  • 细粒度监听(只监听某个 select 结果)
  • 更丰富的 http 工具(重定向/分页游标助手)