@isdk/proxy 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.cn.md +138 -0
- package/README.md +129 -0
- package/dist/index.d.mts +213 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/docs/README.md +133 -0
- package/docs/classes/SmartCache.md +148 -0
- package/docs/functions/createCachedFetch.md +52 -0
- package/docs/functions/createFetchWithCache.md +52 -0
- package/docs/functions/extractData.md +39 -0
- package/docs/functions/fetchWithCache.md +39 -0
- package/docs/functions/generateCacheKey.md +27 -0
- package/docs/functions/isAllowed.md +38 -0
- package/docs/globals.md +29 -0
- package/docs/interfaces/CacheEntry.md +123 -0
- package/docs/interfaces/CacheMetadata.md +88 -0
- package/docs/interfaces/FetchWithCacheContext.md +161 -0
- package/docs/interfaces/FetchWithCacheOptions.md +103 -0
- package/docs/interfaces/KeyFilterConfig.md +33 -0
- package/docs/interfaces/ProxyConfig.md +41 -0
- package/docs/interfaces/SiteCacheConfig.md +61 -0
- package/docs/interfaces/SmartCacheOptions.md +41 -0
- package/package.json +101 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { KeyvCacheableMemoryOptions } from '@cacheable/memory';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 缓存键过滤配置
|
|
5
|
+
*
|
|
6
|
+
* 用于定义在生成缓存指纹时,哪些字段应该被包含或排除。
|
|
7
|
+
*/
|
|
8
|
+
interface KeyFilterConfig {
|
|
9
|
+
/** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算 */
|
|
10
|
+
include?: string[];
|
|
11
|
+
/** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段 */
|
|
12
|
+
exclude?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 站点级缓存配置
|
|
16
|
+
*/
|
|
17
|
+
interface SiteCacheConfig {
|
|
18
|
+
/** Query 参数过滤配置 */
|
|
19
|
+
query?: KeyFilterConfig;
|
|
20
|
+
/** 请求头过滤配置 */
|
|
21
|
+
headers?: KeyFilterConfig;
|
|
22
|
+
/** Cookie 过滤配置 */
|
|
23
|
+
cookies?: KeyFilterConfig;
|
|
24
|
+
/** 当后端请求失败且存在旧缓存时,是否强制返回旧缓存(容错机制) */
|
|
25
|
+
staleIfError?: boolean;
|
|
26
|
+
/** 是否强制缓存一切响应(无视 no-store 等不缓存指令),用于极端的离线可用容错场景 */
|
|
27
|
+
forceCache?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 缓存元数据
|
|
31
|
+
*
|
|
32
|
+
* 存储在 L1 内存和 L2 磁盘中的非 Body 信息。
|
|
33
|
+
* 即使 Body 过大未进入内存,此元数据也会驻留在内存中以供快速策略判定。
|
|
34
|
+
*/
|
|
35
|
+
interface CacheMetadata {
|
|
36
|
+
/** HTTP 状态码 */
|
|
37
|
+
status: number;
|
|
38
|
+
/** 响应头对象 */
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
/** http-cache-semantics 策略对象,包含 TTL 和缓存指令 */
|
|
41
|
+
policy: any;
|
|
42
|
+
/** 原始请求 URL */
|
|
43
|
+
url: string;
|
|
44
|
+
/** 原始请求方法 */
|
|
45
|
+
method: string;
|
|
46
|
+
/** 缓存写入时的时间戳 */
|
|
47
|
+
timestamp: number;
|
|
48
|
+
/** Body 的字节长度,用于精确区分“空响应”与“未入内存的大响应” */
|
|
49
|
+
size: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 完整的缓存条目
|
|
53
|
+
*/
|
|
54
|
+
interface CacheEntry extends CacheMetadata {
|
|
55
|
+
/** 响应体数据:小文件为 Buffer,大文件为可读流 */
|
|
56
|
+
body: Buffer | any;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 代理拦截器全局配置
|
|
60
|
+
*/
|
|
61
|
+
interface ProxyConfig {
|
|
62
|
+
/** 默认缓存配置,当请求的域名未在 sites 中匹配时使用 */
|
|
63
|
+
default: SiteCacheConfig;
|
|
64
|
+
/** 针对特定域名的精细化缓存配置 */
|
|
65
|
+
sites: Record<string, SiteCacheConfig>;
|
|
66
|
+
/** 磁盘缓存(cacache)的物理存储路径,可选,默认为系统临时目录 */
|
|
67
|
+
storagePath?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* SmartCache 选项
|
|
72
|
+
*/
|
|
73
|
+
interface SmartCacheOptions {
|
|
74
|
+
/** 磁盘缓存的物理路径。如果不提供,将默认使用系统临时目录。 */
|
|
75
|
+
storagePath?: string;
|
|
76
|
+
/** 内存缓存阈值(字节)。响应体大小超过此值时,Body 将只存入磁盘,而 Meta 仍保留在内存。默认 1MB。 */
|
|
77
|
+
maxMemorySize?: number;
|
|
78
|
+
/** 透传给 L1 (Memory) 的高级配置 */
|
|
79
|
+
memoryOptions?: Partial<KeyvCacheableMemoryOptions>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 智能混合缓存类 (Hybrid Cache)
|
|
83
|
+
*/
|
|
84
|
+
declare class SmartCache {
|
|
85
|
+
private memory;
|
|
86
|
+
private storagePath;
|
|
87
|
+
private maxMemorySize;
|
|
88
|
+
constructor(options?: SmartCacheOptions);
|
|
89
|
+
/**
|
|
90
|
+
* 获取缓存条目
|
|
91
|
+
* 如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。
|
|
92
|
+
*/
|
|
93
|
+
get(key: string): Promise<CacheEntry | null>;
|
|
94
|
+
/**
|
|
95
|
+
* 写入缓存
|
|
96
|
+
*/
|
|
97
|
+
set(key: string, body: Buffer, metadata: Omit<CacheMetadata, 'size'>): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* 内部方法:处理内存回填
|
|
100
|
+
*/
|
|
101
|
+
private saveToMemory;
|
|
102
|
+
getStream(key: string): NodeJS.ReadableStream;
|
|
103
|
+
setStream(key: string, metadata: Omit<CacheMetadata, 'size'>): NodeJS.WritableStream;
|
|
104
|
+
delete(key: string): Promise<void>;
|
|
105
|
+
clear(): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 根据 Request 和配置生成唯一的缓存键
|
|
110
|
+
*/
|
|
111
|
+
declare const generateCacheKey: (req: Request, config: SiteCacheConfig) => string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* fetchWithCache 选项
|
|
115
|
+
*/
|
|
116
|
+
interface FetchWithCacheOptions {
|
|
117
|
+
/** 混合缓存实例 */
|
|
118
|
+
cache: SmartCache;
|
|
119
|
+
/** 站点级缓存配置 */
|
|
120
|
+
config: SiteCacheConfig;
|
|
121
|
+
/** 是否启用后台异步更新 (SWR) */
|
|
122
|
+
backgroundUpdate?: boolean;
|
|
123
|
+
/** 后台更新 Promise 触发时的回调 */
|
|
124
|
+
onBackgroundUpdate?: (promise: Promise<Response>) => void;
|
|
125
|
+
/** 自定义缓存键生成函数 */
|
|
126
|
+
generateKey?: typeof generateCacheKey;
|
|
127
|
+
/**
|
|
128
|
+
* 并发写入任务追踪器
|
|
129
|
+
* 传入一个外部维护的 Map,用于在跨请求、跨实例时防止针对同一文件的并发重复下载。
|
|
130
|
+
* Map 的 Key 是缓存 Key,Value 是一个代表写入完成的 Promise。
|
|
131
|
+
*/
|
|
132
|
+
activeCacheWrites?: Map<string, Promise<void>>;
|
|
133
|
+
}
|
|
134
|
+
/** 内部流水线上下文,合并了入参和计算出的关键状态 */
|
|
135
|
+
interface FetchWithCacheContext extends FetchWithCacheOptions {
|
|
136
|
+
request: Request;
|
|
137
|
+
fetcher: (req: Request) => Promise<Response>;
|
|
138
|
+
cacheKey: string;
|
|
139
|
+
activeCacheWrites: Map<string, Promise<void>>;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 核心协调函数 (Fetcher Orchestrator)
|
|
143
|
+
*
|
|
144
|
+
* 实现了基于流的混合缓存代理核心逻辑,主要机制包括:
|
|
145
|
+
* - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM。
|
|
146
|
+
* - **SWR (Stale-While-Revalidate)**:后台静默更新机制。
|
|
147
|
+
* - **并发防击穿 (Request Coalescing)**:利用 `activeCacheWrites` 将并发请求合并。
|
|
148
|
+
* - **强制离线容灾**:支持 `staleIfError` 和 `forceCache`(无视 Cache-Control 强制入库)。
|
|
149
|
+
*
|
|
150
|
+
* 并且会在响应头中自动注入 `x-proxy-cache` 标明缓存命中状态 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`)。
|
|
151
|
+
*/
|
|
152
|
+
declare function fetchWithCache(request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions): Promise<Response>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 单一职责高阶函数:专门用于封装和隔离 activeCacheWrites 并发追踪器。
|
|
156
|
+
*
|
|
157
|
+
* 每次调用此函数,都会创建一个完全独立的闭包 Map(或复用传入的 Map),
|
|
158
|
+
* 并返回一个绑定了该 Map 的 `fetchWithCache` 变体函数。
|
|
159
|
+
* 从而让使用者无需关心 `activeCacheWrites` 的维护,杜绝了误传或不传导致的并发击穿风险。
|
|
160
|
+
*
|
|
161
|
+
* @param activeCacheWrites - 可选参数,用于跨实例共享的并发写入追踪器。
|
|
162
|
+
* 如果未提供,将自动创建一个新的 Map。
|
|
163
|
+
* 传入同一个 Map 可以让多个 `createFetchWithCache` 实例共享
|
|
164
|
+
* 并发追踪状态,从而在整个应用范围内防止缓存击穿。
|
|
165
|
+
* @returns 一个绑定了并发追踪器的 `fetchWithCache` 变体函数。
|
|
166
|
+
*/
|
|
167
|
+
declare function createFetchWithCache(activeCacheWrites?: Map<string, Promise<void>>): (request: Request, fetcher: (req: Request) => Promise<Response>, options: Omit<FetchWithCacheOptions, "activeCacheWrites">) => Promise<Response>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 缓存请求工厂函数 (针对终端用户的顶层高阶 API)
|
|
171
|
+
*
|
|
172
|
+
* 为用户提供一个只需配置一次(如 Cache 实例、默认 Config),
|
|
173
|
+
* 即可在整个应用生命周期中随处调用的 `cachedFetch` 方法。
|
|
174
|
+
*
|
|
175
|
+
* 底层调用了 `createFetchWithCache` 来保证单一职能隔离,内部自动维护并发追踪。
|
|
176
|
+
*
|
|
177
|
+
* @param defaultOptions - 默认缓存配置选项。
|
|
178
|
+
* 可以包含 `activeCacheWrites` 字段,用于跨多个 `createCachedFetch`
|
|
179
|
+
* 实例共享并发追踪状态,实现应用级别的缓存击穿防护。
|
|
180
|
+
* @returns 一个预配置的 `cachedFetch` 函数,可直接用于发起带缓存的请求。
|
|
181
|
+
*/
|
|
182
|
+
declare function createCachedFetch(defaultOptions: FetchWithCacheOptions): (request: Request, fetcher: (req: Request) => Promise<Response>, overrideOptions?: Partial<FetchWithCacheOptions>) => Promise<Response>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 从源对象中根据过滤配置提取数据并标准化。
|
|
186
|
+
*
|
|
187
|
+
* 此函数主要用于生成缓存指纹。它会:
|
|
188
|
+
* 1. 根据 `config` (include/exclude) 过滤键。
|
|
189
|
+
* 2. 对键进行排序以保证指纹的一致性。
|
|
190
|
+
* 3. 将所有键转换为小写。
|
|
191
|
+
* 4. 将值统一包装为数组并进行排序,消除数组项顺序差异。
|
|
192
|
+
*
|
|
193
|
+
* @param source 原始数据对象 (如 QueryParams, Headers, Cookies)
|
|
194
|
+
* @param config 过滤配置 (白名单或黑名单)
|
|
195
|
+
* @returns 标准化后的数据 Map,键为小写,值为字符串数组
|
|
196
|
+
*/
|
|
197
|
+
declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig) => Record<string, string[]>;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 判断给定的键是否允许参与缓存指纹计算。
|
|
201
|
+
*
|
|
202
|
+
* 优先级逻辑:
|
|
203
|
+
* 1. 如果配置了 `include` (白名单),则只有存在于 `include` 中的键才会被允许。
|
|
204
|
+
* 2. 否则,如果配置了 `exclude` (黑名单),则存在于 `exclude` 中的键将被拒绝。
|
|
205
|
+
* 3. 如果都没有配置,默认允许所有键。
|
|
206
|
+
*
|
|
207
|
+
* @param key 要检查的键名
|
|
208
|
+
* @param config 过滤配置
|
|
209
|
+
* @returns 是否允许
|
|
210
|
+
*/
|
|
211
|
+
declare function isAllowed(key: string, config?: KeyFilterConfig): boolean;
|
|
212
|
+
|
|
213
|
+
export { type CacheEntry, type CacheMetadata, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, isAllowed };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var t,e=Object.create,r=Object.defineProperty,a=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,i=(t,e,s,i)=>{if(e&&"object"==typeof e||"function"==typeof e)for(let u of n(e))c.call(t,u)||u===s||r(t,u,{get:()=>e[u],enumerable:!(i=a(e,u))||i.enumerable});return t},u=(t,a,n)=>(n=null!=t?e(s(t)):{},i(!a&&t&&t.__esModule?n:r(n,"default",{value:t,enumerable:!0}),t)),o={};((t,e)=>{for(var a in e)r(t,a,{get:e[a],enumerable:!0})})(o,{SmartCache:()=>d,createCachedFetch:()=>v,createFetchWithCache:()=>q,extractData:()=>b,fetchWithCache:()=>R,generateCacheKey:()=>p,isAllowed:()=>m}),module.exports=(t=o,i(r({},"__esModule",{value:!0}),t));var h=require("@cacheable/memory"),f=u(require("cacache")),l=u(require("os")),w=u(require("path")),d=class{memory;storagePath;maxMemorySize;constructor(t={}){this.storagePath=t.storagePath||w.default.join(l.default.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=t.maxMemorySize??1048576,this.memory=new h.KeyvCacheableMemory({lruSize:500,ttl:3e5,...t.memoryOptions})}async get(t){const e=await this.memory.get(t);if(e){if(e.body)return e;if(0===e.size)return{...e,body:Buffer.alloc(0)};const r=f.default.get.stream(this.storagePath,t);return{...e,body:r}}try{const e=await f.default.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:r}=await f.default.get(this.storagePath,t),a=r,n={...a,body:e};return await this.saveToMemory(t,e,a),n}{const e=(await f.default.get.info(this.storagePath,t)).metadata;return await this.saveToMemory(t,null,e),{...e,body:f.default.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,r){const a={...r,size:e.length};await f.default.put(this.storagePath,t,e,{metadata:a}),await this.saveToMemory(t,e,a)}async saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)await this.memory.set(t,{...r,body:e});else{const{...e}=r;await this.memory.set(t,e)}}getStream(t){return f.default.get.stream(this.storagePath,t)}setStream(t,e){return this.memory.delete(t).catch(()=>{}),f.default.put.stream(this.storagePath,t,{metadata:e})}async delete(t){await this.memory.delete(t),await f.default.rm.entry(this.storagePath,t)}async clear(){await this.memory.clear(),await f.default.rm.all(this.storagePath)}},y=require("crypto");function m(t,e){return e?.include?e.include.includes(t):!e?.exclude||!e.exclude.includes(t)}var b=(t,e)=>{const r={};return Object.keys(t).filter(t=>m(t,e)).sort().forEach(e=>{const a=t[e];null!=a&&(r[e.toLowerCase()]=Array.isArray(a)?[...a].sort():[a])}),r},p=(t,e)=>{const r=new URL(t.url),a=t.headers.get("cookie")||"",n=Object.fromEntries(a.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]})),s={m:t.method.toUpperCase(),h:r.host,p:r.pathname,q:b(Object.fromEntries(r.searchParams),e.query),hd:b(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:b(n,e.cookies)};return(0,y.createHash)("sha256").update(JSON.stringify(s)).digest("hex")},O=require("stream"),j=require("stream/promises"),S=u(require("http-cache-semantics"));function T(t,e){const r=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?O.Readable.toWeb(t):t}(t.body);return new Response(r,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function x(t,e){let r,a;const n=new Promise((e,n)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},a=e=>{t.activeCacheWrites.delete(t.cacheKey),n(e)}});n.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,n);try{const e=await t.fetcher(t.request.clone()),n=new S.default({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),s=new Headers(e.headers);if(s.set("x-proxy-cache","MISS"),!n.storable()&&!t.config.forceCache)return r(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:s});const c={status:e.status,headers:Object.fromEntries(e.headers),policy:n.toObject(),url:t.request.url,method:t.request.method,timestamp:Date.now()};if(!e.body)return await t.cache.set(t.cacheKey,Buffer.alloc(0),c),r(),new Response(null,{status:e.status,statusText:e.statusText,headers:s});const[i,u]=e.body.tee();return(0,j.pipeline)(O.Readable.fromWeb(u),t.cache.setStream(t.cacheKey,c)).then(r).catch(a),new Response(i,{status:e.status,statusText:e.statusText,headers:s})}catch(r){if(a(r),e&&t.config.staleIfError)return T(e,"STALE_IF_ERROR");throw r}}async function R(t,e,r){const a=function(t,e,r){const a=(r.generateKey||p)(t,r.config);return{...r,request:t,fetcher:e,cacheKey:a,activeCacheWrites:r.activeCacheWrites||new Map}}(t,e,r),n=await a.cache.get(a.cacheKey);if(n){const t=function(t,e){const r=S.default.fromObject(e.policy),a={url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(a)?"HIT":"STALE"}(a,n);if("HIT"===t)return T(n,"HIT");if("STALE"===t&&!1!==a.backgroundUpdate)return function(t,e){const r=x(t,e).catch(r=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,r),T(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(r)}(a,n),T(n,"STALE")}if(a.activeCacheWrites.has(a.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;try{await e;const r=await t.cache.get(t.cacheKey);if(r)return T(r,"HIT")}catch(e){console.warn(`[Cache Warning] Awaited active cache write failed for ${t.cacheKey}`)}return null}(a);if(t)return t}return x(a,n)}function q(t){return t||(t=new Map),async function(e,r,a){return R(e,r,{...a,activeCacheWrites:t})}}function v(t){const e=q(t.activeCacheWrites);return async function(r,a,n){return e(r,a,{...t,...n})}}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{KeyvCacheableMemory as t}from"@cacheable/memory";import e from"cacache";import r from"os";import n from"path";var a=class{memory;storagePath;maxMemorySize;constructor(e={}){this.storagePath=e.storagePath||n.join(r.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=e.maxMemorySize??1048576,this.memory=new t({lruSize:500,ttl:3e5,...e.memoryOptions})}async get(t){const r=await this.memory.get(t);if(r){if(r.body)return r;if(0===r.size)return{...r,body:Buffer.alloc(0)};const n=e.get.stream(this.storagePath,t);return{...r,body:n}}try{const r=await e.get.info(this.storagePath,t);if(!r)return null;if(r.size<=this.maxMemorySize){const{data:r,metadata:n}=await e.get(this.storagePath,t),a=n,s={...a,body:r};return await this.saveToMemory(t,r,a),s}{const r=(await e.get.info(this.storagePath,t)).metadata;return await this.saveToMemory(t,null,r),{...r,body:e.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,r,n){const a={...n,size:r.length};await e.put(this.storagePath,t,r,{metadata:a}),await this.saveToMemory(t,r,a)}async saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)await this.memory.set(t,{...r,body:e});else{const{...e}=r;await this.memory.set(t,e)}}getStream(t){return e.get.stream(this.storagePath,t)}setStream(t,r){return this.memory.delete(t).catch(()=>{}),e.put.stream(this.storagePath,t,{metadata:r})}async delete(t){await this.memory.delete(t),await e.rm.entry(this.storagePath,t)}async clear(){await this.memory.clear(),await e.rm.all(this.storagePath)}};import{createHash as s}from"crypto";function i(t,e){return e?.include?e.include.includes(t):!e?.exclude||!e.exclude.includes(t)}var c=(t,e)=>{const r={};return Object.keys(t).filter(t=>i(t,e)).sort().forEach(e=>{const n=t[e];null!=n&&(r[e.toLowerCase()]=Array.isArray(n)?[...n].sort():[n])}),r},o=(t,e)=>{const r=new URL(t.url),n=t.headers.get("cookie")||"",a=Object.fromEntries(n.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]})),i={m:t.method.toUpperCase(),h:r.host,p:r.pathname,q:c(Object.fromEntries(r.searchParams),e.query),hd:c(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:c(a,e.cookies)};return s("sha256").update(JSON.stringify(i)).digest("hex")};import{Readable as u}from"stream";import{pipeline as h}from"stream/promises";import f from"http-cache-semantics";function m(t,e){const r=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?u.toWeb(t):t}(t.body);return new Response(r,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function w(t,e){let r,n;const a=new Promise((e,a)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},n=e=>{t.activeCacheWrites.delete(t.cacheKey),a(e)}});a.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,a);try{const e=await t.fetcher(t.request.clone()),a=new f({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),s=new Headers(e.headers);if(s.set("x-proxy-cache","MISS"),!a.storable()&&!t.config.forceCache)return r(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:s});const i={status:e.status,headers:Object.fromEntries(e.headers),policy:a.toObject(),url:t.request.url,method:t.request.method,timestamp:Date.now()};if(!e.body)return await t.cache.set(t.cacheKey,Buffer.alloc(0),i),r(),new Response(null,{status:e.status,statusText:e.statusText,headers:s});const[c,o]=e.body.tee();return h(u.fromWeb(o),t.cache.setStream(t.cacheKey,i)).then(r).catch(n),new Response(c,{status:e.status,statusText:e.statusText,headers:s})}catch(r){if(n(r),e&&t.config.staleIfError)return m(e,"STALE_IF_ERROR");throw r}}async function l(t,e,r){const n=function(t,e,r){const n=(r.generateKey||o)(t,r.config);return{...r,request:t,fetcher:e,cacheKey:n,activeCacheWrites:r.activeCacheWrites||new Map}}(t,e,r),a=await n.cache.get(n.cacheKey);if(a){const t=function(t,e){const r=f.fromObject(e.policy),n={url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(n,a);if("HIT"===t)return m(a,"HIT");if("STALE"===t&&!1!==n.backgroundUpdate)return function(t,e){const r=w(t,e).catch(r=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,r),m(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(r)}(n,a),m(a,"STALE")}if(n.activeCacheWrites.has(n.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;try{await e;const r=await t.cache.get(t.cacheKey);if(r)return m(r,"HIT")}catch(e){console.warn(`[Cache Warning] Awaited active cache write failed for ${t.cacheKey}`)}return null}(n);if(t)return t}return w(n,a)}function y(t){return t||(t=new Map),async function(e,r,n){return l(e,r,{...n,activeCacheWrites:t})}}function d(t){const e=y(t.activeCacheWrites);return async function(r,n,a){return e(r,n,{...t,...a})}}export{a as SmartCache,d as createCachedFetch,y as createFetchWithCache,c as extractData,l as fetchWithCache,o as generateCacheKey,i as isAllowed};
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
**@isdk/proxy**
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
# @isdk/proxy
|
|
6
|
+
|
|
7
|
+
A high-performance, developer-friendly caching engine for Node.js, specifically designed to solve the complexity of managing HTTP response caches in data-intensive applications.
|
|
8
|
+
|
|
9
|
+
## Why @isdk/proxy?
|
|
10
|
+
|
|
11
|
+
In high-concurrency environments—like **API Proxies**, **Web Scrapers**, or **Microservices**—managing caches is often a trade-off between speed and memory.
|
|
12
|
+
|
|
13
|
+
`@isdk/proxy` provides a **Hybrid Multi-tier Architecture** that gives you the best of both worlds:
|
|
14
|
+
|
|
15
|
+
- **It solves the Memory vs. Capacity problem**: Keeps small, hot responses in memory (L1) for nanosecond access, while offloading large bodies to persistent disk (L2) without losing the ability to instantly evaluate cache policies.
|
|
16
|
+
- **It prevents Cache Stampede**: When a hot entry expires, it ensures only ONE network request is made, preventing your upstream from being crushed by concurrent misses.
|
|
17
|
+
- **It is Framework-Agnostic**: Built on Web standard `Request`/`Response` objects, it decouples your caching logic from your HTTP client (MSW, Axios, Fetch, Crawlee, etc.).
|
|
18
|
+
|
|
19
|
+
## Key Features
|
|
20
|
+
|
|
21
|
+
- **🚀 Hybrid Multi-tier Cache**: Extreme speed with L1 (LRU Memory) and persistence with L2 (Content Addressable Disk via `cacache`).
|
|
22
|
+
- **🌊 Streaming Native**: Fully stream-based internal pipeline natively prevents Out-Of-Memory (OOM) issues when proxying large files.
|
|
23
|
+
- **🧠 Intelligent Meta-Residency**: Metadata (Headers, Status, Policy) stays in memory regardless of body size, ensuring nanosecond cache policy evaluations.
|
|
24
|
+
- **🔄 Stale-While-Revalidate (SWR)**: Serve stale content instantly while updating the cache silently in the background.
|
|
25
|
+
- **🛡️ Request Coalescing (Anti-Stampede)**: Prevent cache stampede by coalescing identical concurrent requests using a shared tracker, ensuring only one network request is made.
|
|
26
|
+
- **🚑 Offline Resilience**: Automatically serve stale content if the upstream is down (`staleIfError`), or forcefully cache everything ignoring `Cache-Control: no-store` (`forceCache`).
|
|
27
|
+
- **🕵️ Transparent Cache Status**: Injects standard `x-proxy-cache` headers (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`) into responses for easy observability.
|
|
28
|
+
- **🌐 Framework Agnostic**: Works everywhere by using standard Web `Request`/`Response` APIs.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add @isdk/proxy
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start: The Core Orchestrator
|
|
37
|
+
|
|
38
|
+
The primary way to use `@isdk/proxy` is via the `fetchWithCache` function, which can wrap any HTTP request logic.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { SmartCache, createCachedFetch } from '@isdk/proxy';
|
|
42
|
+
|
|
43
|
+
// 1. Initialize the hybrid cache
|
|
44
|
+
const cache = new SmartCache({
|
|
45
|
+
storagePath: './.cache',
|
|
46
|
+
maxMemorySize: 1024 * 1024 // 1MB threshold
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 2. Create a pre-configured cached fetcher (automatically tracks concurrent requests)
|
|
50
|
+
const myFetch = createCachedFetch({
|
|
51
|
+
cache,
|
|
52
|
+
config: {
|
|
53
|
+
staleIfError: true,
|
|
54
|
+
forceCache: false // Set to true to cache everything (ignore no-store) for offline-first apps
|
|
55
|
+
},
|
|
56
|
+
backgroundUpdate: true // Enable SWR
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 3. Use it anywhere in your app!
|
|
60
|
+
const request = new Request('https://api.example.com/data');
|
|
61
|
+
const response = await myFetch(request, (req) => fetch(req));
|
|
62
|
+
|
|
63
|
+
console.log(response.headers.get('x-proxy-cache')); // "MISS", "HIT", "STALE", or "STALE_IF_ERROR"
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Adapters
|
|
68
|
+
|
|
69
|
+
`@isdk/proxy` is designed to be framework-agnostic. While the core library is pure, you can find (or build) adapters for specific environments:
|
|
70
|
+
|
|
71
|
+
- **MSW Adapter**: See `@isdk/proxy-msw` (separate package) to use this caching engine as an MSW interceptor.
|
|
72
|
+
- **Axios Adapter**: Easily implemented by converting Axios config to Web `Request`.
|
|
73
|
+
|
|
74
|
+
## Architecture
|
|
75
|
+
|
|
76
|
+
### Hybrid Storage Strategy
|
|
77
|
+
|
|
78
|
+
- **L1 (Memory)**: Powered by `@cacheable/memory`. Stores both Meta and Body for small files (< `maxMemorySize`).
|
|
79
|
+
- **L2 (Disk)**: Powered by `cacache`. Stores all data for persistence.
|
|
80
|
+
- **Optimization**: For large files, only the Metadata is kept in memory. The body is streamed or read from disk only when requested, saving significant memory.
|
|
81
|
+
|
|
82
|
+
### Request Collapsing
|
|
83
|
+
|
|
84
|
+
When multiple concurrent requests hit a missing or expired cache entry, `@isdk/proxy` ensures that only **one** request goes to the network. Subsequent requests will wait for the same promise or serve the background-updated data.
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### `createCachedFetch(options)` (Recommended)
|
|
89
|
+
|
|
90
|
+
A higher-order factory function designed for end-users. It creates a pre-configured `fetch` equivalent that automatically tracks concurrent requests internally to prevent cache stampedes.
|
|
91
|
+
|
|
92
|
+
- **`options.cache`**: An instance of `SmartCache`.
|
|
93
|
+
- **`options.config`**: A `SiteCacheConfig` object containing:
|
|
94
|
+
- `staleIfError` (boolean): Serve stale cache if the network fails.
|
|
95
|
+
- `forceCache` (boolean): Force cache everything, ignoring `Cache-Control: no-store`. Ideal for offline-first resilience.
|
|
96
|
+
- **`options.backgroundUpdate`**: Set to `true` to enable SWR behavior.
|
|
97
|
+
- **`options.activeCacheWrites`**: Optional. A `Map<string, Promise<void>>` that can be shared across multiple `createCachedFetch` instances to enable application-level cache stampede prevention.
|
|
98
|
+
- **Returns**: A reusable `(request: Request, fetcher: (req: Request) => Promise<Response>) => Promise<Response>` function.
|
|
99
|
+
|
|
100
|
+
### `createFetchWithCache(activeCacheWrites?)`
|
|
101
|
+
|
|
102
|
+
A single-responsibility higher-order function that encapsulates the `activeCacheWrites` concurrency tracker. It returns a variant of `fetchWithCache` that shares an internal Map to coalesce identical concurrent requests. Use this if you are building an intermediate wrapper but don't want to rely on the top-level `createCachedFetch` factory.
|
|
103
|
+
|
|
104
|
+
- **`activeCacheWrites`**: Optional. An external `Map<string, Promise<void>>` to be used as the concurrency tracker. If not provided, a new internal Map will be created. Sharing the same Map across multiple instances enables application-wide request coalescing.
|
|
105
|
+
- **Returns**: `(request: Request, fetcher: (req: Request) => Promise<Response>, options: Omit<FetchWithCacheOptions, 'activeCacheWrites'>) => Promise<Response>`
|
|
106
|
+
|
|
107
|
+
### `fetchWithCache(request, fetcher, options)`
|
|
108
|
+
|
|
109
|
+
The core caching orchestrator. Use this directly if you need low-level control or are building a library on top of it.
|
|
110
|
+
|
|
111
|
+
- **`request`**: Web Standard `Request`.
|
|
112
|
+
- **`fetcher`**: The raw fetching callback `(req: Request) => Promise<Response>`.
|
|
113
|
+
- **`options.activeCacheWrites`**: A `Map<string, Promise<void>>` that YOU must provide and maintain to coalesce concurrent requests. (If you don't want to manage this, use `createCachedFetch` or `createFetchWithCache` instead).
|
|
114
|
+
|
|
115
|
+
### `SmartCache`
|
|
116
|
+
|
|
117
|
+
The hybrid multi-tier storage engine.
|
|
118
|
+
|
|
119
|
+
- `new SmartCache(options)`
|
|
120
|
+
- **`options.maxMemorySize`**: Threshold (in bytes) for offloading bodies to disk (default `1048576`, i.e., 1MB).
|
|
121
|
+
- **`options.storagePath`**: Disk storage path for the `cacache` engine (defaults to a system temp folder).
|
|
122
|
+
|
|
123
|
+
### Cache Status Headers
|
|
124
|
+
|
|
125
|
+
Every response processed by `@isdk/proxy` will include an `x-proxy-cache` header indicating its lifecycle:
|
|
126
|
+
- `HIT`: Served entirely from L1 or L2 cache.
|
|
127
|
+
- `MISS`: Bypassed cache and fetched from the origin server.
|
|
128
|
+
- `STALE`: Served from stale cache while a background update was initiated (SWR).
|
|
129
|
+
- `STALE_IF_ERROR`: Origin fetch failed; served from stale cache as a fallback.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
[**@isdk/proxy**](../README.md)
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
[@isdk/proxy](../globals.md) / SmartCache
|
|
6
|
+
|
|
7
|
+
# Class: SmartCache
|
|
8
|
+
|
|
9
|
+
Defined in: [core/SmartCache.ts:22](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L22)
|
|
10
|
+
|
|
11
|
+
智能混合缓存类 (Hybrid Cache)
|
|
12
|
+
|
|
13
|
+
## Constructors
|
|
14
|
+
|
|
15
|
+
### Constructor
|
|
16
|
+
|
|
17
|
+
> **new SmartCache**(`options`): `SmartCache`
|
|
18
|
+
|
|
19
|
+
Defined in: [core/SmartCache.ts:27](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L27)
|
|
20
|
+
|
|
21
|
+
#### Parameters
|
|
22
|
+
|
|
23
|
+
##### options
|
|
24
|
+
|
|
25
|
+
[`SmartCacheOptions`](../interfaces/SmartCacheOptions.md) = `{}`
|
|
26
|
+
|
|
27
|
+
#### Returns
|
|
28
|
+
|
|
29
|
+
`SmartCache`
|
|
30
|
+
|
|
31
|
+
## Methods
|
|
32
|
+
|
|
33
|
+
### clear()
|
|
34
|
+
|
|
35
|
+
> **clear**(): `Promise`\<`void`\>
|
|
36
|
+
|
|
37
|
+
Defined in: [core/SmartCache.ts:122](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L122)
|
|
38
|
+
|
|
39
|
+
#### Returns
|
|
40
|
+
|
|
41
|
+
`Promise`\<`void`\>
|
|
42
|
+
|
|
43
|
+
***
|
|
44
|
+
|
|
45
|
+
### delete()
|
|
46
|
+
|
|
47
|
+
> **delete**(`key`): `Promise`\<`void`\>
|
|
48
|
+
|
|
49
|
+
Defined in: [core/SmartCache.ts:117](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L117)
|
|
50
|
+
|
|
51
|
+
#### Parameters
|
|
52
|
+
|
|
53
|
+
##### key
|
|
54
|
+
|
|
55
|
+
`string`
|
|
56
|
+
|
|
57
|
+
#### Returns
|
|
58
|
+
|
|
59
|
+
`Promise`\<`void`\>
|
|
60
|
+
|
|
61
|
+
***
|
|
62
|
+
|
|
63
|
+
### get()
|
|
64
|
+
|
|
65
|
+
> **get**(`key`): `Promise`\<[`CacheEntry`](../interfaces/CacheEntry.md) \| `null`\>
|
|
66
|
+
|
|
67
|
+
Defined in: [core/SmartCache.ts:41](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L41)
|
|
68
|
+
|
|
69
|
+
获取缓存条目
|
|
70
|
+
如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。
|
|
71
|
+
|
|
72
|
+
#### Parameters
|
|
73
|
+
|
|
74
|
+
##### key
|
|
75
|
+
|
|
76
|
+
`string`
|
|
77
|
+
|
|
78
|
+
#### Returns
|
|
79
|
+
|
|
80
|
+
`Promise`\<[`CacheEntry`](../interfaces/CacheEntry.md) \| `null`\>
|
|
81
|
+
|
|
82
|
+
***
|
|
83
|
+
|
|
84
|
+
### getStream()
|
|
85
|
+
|
|
86
|
+
> **getStream**(`key`): `ReadableStream`
|
|
87
|
+
|
|
88
|
+
Defined in: [core/SmartCache.ts:107](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L107)
|
|
89
|
+
|
|
90
|
+
#### Parameters
|
|
91
|
+
|
|
92
|
+
##### key
|
|
93
|
+
|
|
94
|
+
`string`
|
|
95
|
+
|
|
96
|
+
#### Returns
|
|
97
|
+
|
|
98
|
+
`ReadableStream`
|
|
99
|
+
|
|
100
|
+
***
|
|
101
|
+
|
|
102
|
+
### set()
|
|
103
|
+
|
|
104
|
+
> **set**(`key`, `body`, `metadata`): `Promise`\<`void`\>
|
|
105
|
+
|
|
106
|
+
Defined in: [core/SmartCache.ts:85](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L85)
|
|
107
|
+
|
|
108
|
+
写入缓存
|
|
109
|
+
|
|
110
|
+
#### Parameters
|
|
111
|
+
|
|
112
|
+
##### key
|
|
113
|
+
|
|
114
|
+
`string`
|
|
115
|
+
|
|
116
|
+
##### body
|
|
117
|
+
|
|
118
|
+
`Buffer`
|
|
119
|
+
|
|
120
|
+
##### metadata
|
|
121
|
+
|
|
122
|
+
`Omit`\<[`CacheMetadata`](../interfaces/CacheMetadata.md), `"size"`\>
|
|
123
|
+
|
|
124
|
+
#### Returns
|
|
125
|
+
|
|
126
|
+
`Promise`\<`void`\>
|
|
127
|
+
|
|
128
|
+
***
|
|
129
|
+
|
|
130
|
+
### setStream()
|
|
131
|
+
|
|
132
|
+
> **setStream**(`key`, `metadata`): `WritableStream`
|
|
133
|
+
|
|
134
|
+
Defined in: [core/SmartCache.ts:111](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L111)
|
|
135
|
+
|
|
136
|
+
#### Parameters
|
|
137
|
+
|
|
138
|
+
##### key
|
|
139
|
+
|
|
140
|
+
`string`
|
|
141
|
+
|
|
142
|
+
##### metadata
|
|
143
|
+
|
|
144
|
+
`Omit`\<[`CacheMetadata`](../interfaces/CacheMetadata.md), `"size"`\>
|
|
145
|
+
|
|
146
|
+
#### Returns
|
|
147
|
+
|
|
148
|
+
`WritableStream`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[**@isdk/proxy**](../README.md)
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
[@isdk/proxy](../globals.md) / createCachedFetch
|
|
6
|
+
|
|
7
|
+
# Function: createCachedFetch()
|
|
8
|
+
|
|
9
|
+
> **createCachedFetch**(`defaultOptions`): (`request`, `fetcher`, `overrideOptions?`) => `Promise`\<`Response`\>
|
|
10
|
+
|
|
11
|
+
Defined in: [core/createCachedFetch.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/createCachedFetch.ts#L17)
|
|
12
|
+
|
|
13
|
+
缓存请求工厂函数 (针对终端用户的顶层高阶 API)
|
|
14
|
+
|
|
15
|
+
为用户提供一个只需配置一次(如 Cache 实例、默认 Config),
|
|
16
|
+
即可在整个应用生命周期中随处调用的 `cachedFetch` 方法。
|
|
17
|
+
|
|
18
|
+
底层调用了 `createFetchWithCache` 来保证单一职能隔离,内部自动维护并发追踪。
|
|
19
|
+
|
|
20
|
+
## Parameters
|
|
21
|
+
|
|
22
|
+
### defaultOptions
|
|
23
|
+
|
|
24
|
+
[`FetchWithCacheOptions`](../interfaces/FetchWithCacheOptions.md)
|
|
25
|
+
|
|
26
|
+
默认缓存配置选项。
|
|
27
|
+
可以包含 `activeCacheWrites` 字段,用于跨多个 `createCachedFetch`
|
|
28
|
+
实例共享并发追踪状态,实现应用级别的缓存击穿防护。
|
|
29
|
+
|
|
30
|
+
## Returns
|
|
31
|
+
|
|
32
|
+
一个预配置的 `cachedFetch` 函数,可直接用于发起带缓存的请求。
|
|
33
|
+
|
|
34
|
+
> (`request`, `fetcher`, `overrideOptions?`): `Promise`\<`Response`\>
|
|
35
|
+
|
|
36
|
+
### Parameters
|
|
37
|
+
|
|
38
|
+
#### request
|
|
39
|
+
|
|
40
|
+
`Request`
|
|
41
|
+
|
|
42
|
+
#### fetcher
|
|
43
|
+
|
|
44
|
+
(`req`) => `Promise`\<`Response`\>
|
|
45
|
+
|
|
46
|
+
#### overrideOptions?
|
|
47
|
+
|
|
48
|
+
`Partial`\<[`FetchWithCacheOptions`](../interfaces/FetchWithCacheOptions.md)\>
|
|
49
|
+
|
|
50
|
+
### Returns
|
|
51
|
+
|
|
52
|
+
`Promise`\<`Response`\>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[**@isdk/proxy**](../README.md)
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
[@isdk/proxy](../globals.md) / createFetchWithCache
|
|
6
|
+
|
|
7
|
+
# Function: createFetchWithCache()
|
|
8
|
+
|
|
9
|
+
> **createFetchWithCache**(`activeCacheWrites?`): (`request`, `fetcher`, `options`) => `Promise`\<`Response`\>
|
|
10
|
+
|
|
11
|
+
Defined in: [core/createFetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/createFetchWithCache.ts#L16)
|
|
12
|
+
|
|
13
|
+
单一职责高阶函数:专门用于封装和隔离 activeCacheWrites 并发追踪器。
|
|
14
|
+
|
|
15
|
+
每次调用此函数,都会创建一个完全独立的闭包 Map(或复用传入的 Map),
|
|
16
|
+
并返回一个绑定了该 Map 的 `fetchWithCache` 变体函数。
|
|
17
|
+
从而让使用者无需关心 `activeCacheWrites` 的维护,杜绝了误传或不传导致的并发击穿风险。
|
|
18
|
+
|
|
19
|
+
## Parameters
|
|
20
|
+
|
|
21
|
+
### activeCacheWrites?
|
|
22
|
+
|
|
23
|
+
`Map`\<`string`, `Promise`\<`void`\>\>
|
|
24
|
+
|
|
25
|
+
可选参数,用于跨实例共享的并发写入追踪器。
|
|
26
|
+
如果未提供,将自动创建一个新的 Map。
|
|
27
|
+
传入同一个 Map 可以让多个 `createFetchWithCache` 实例共享
|
|
28
|
+
并发追踪状态,从而在整个应用范围内防止缓存击穿。
|
|
29
|
+
|
|
30
|
+
## Returns
|
|
31
|
+
|
|
32
|
+
一个绑定了并发追踪器的 `fetchWithCache` 变体函数。
|
|
33
|
+
|
|
34
|
+
> (`request`, `fetcher`, `options`): `Promise`\<`Response`\>
|
|
35
|
+
|
|
36
|
+
### Parameters
|
|
37
|
+
|
|
38
|
+
#### request
|
|
39
|
+
|
|
40
|
+
`Request`
|
|
41
|
+
|
|
42
|
+
#### fetcher
|
|
43
|
+
|
|
44
|
+
(`req`) => `Promise`\<`Response`\>
|
|
45
|
+
|
|
46
|
+
#### options
|
|
47
|
+
|
|
48
|
+
`Omit`\<[`FetchWithCacheOptions`](../interfaces/FetchWithCacheOptions.md), `"activeCacheWrites"`\>
|
|
49
|
+
|
|
50
|
+
### Returns
|
|
51
|
+
|
|
52
|
+
`Promise`\<`Response`\>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[**@isdk/proxy**](../README.md)
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
[@isdk/proxy](../globals.md) / extractData
|
|
6
|
+
|
|
7
|
+
# Function: extractData()
|
|
8
|
+
|
|
9
|
+
> **extractData**(`source`, `config?`): `Record`\<`string`, `string`[]\>
|
|
10
|
+
|
|
11
|
+
Defined in: [utils/extractData.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/utils/extractData.ts#L17)
|
|
12
|
+
|
|
13
|
+
从源对象中根据过滤配置提取数据并标准化。
|
|
14
|
+
|
|
15
|
+
此函数主要用于生成缓存指纹。它会:
|
|
16
|
+
1. 根据 `config` (include/exclude) 过滤键。
|
|
17
|
+
2. 对键进行排序以保证指纹的一致性。
|
|
18
|
+
3. 将所有键转换为小写。
|
|
19
|
+
4. 将值统一包装为数组并进行排序,消除数组项顺序差异。
|
|
20
|
+
|
|
21
|
+
## Parameters
|
|
22
|
+
|
|
23
|
+
### source
|
|
24
|
+
|
|
25
|
+
`Record`\<`string`, `any`\>
|
|
26
|
+
|
|
27
|
+
原始数据对象 (如 QueryParams, Headers, Cookies)
|
|
28
|
+
|
|
29
|
+
### config?
|
|
30
|
+
|
|
31
|
+
[`KeyFilterConfig`](../interfaces/KeyFilterConfig.md)
|
|
32
|
+
|
|
33
|
+
过滤配置 (白名单或黑名单)
|
|
34
|
+
|
|
35
|
+
## Returns
|
|
36
|
+
|
|
37
|
+
`Record`\<`string`, `string`[]\>
|
|
38
|
+
|
|
39
|
+
标准化后的数据 Map,键为小写,值为字符串数组
|