@isdk/proxy
Advanced tools
| [**@isdk/proxy**](../README.md) | ||
| *** | ||
| [@isdk/proxy](../globals.md) / getSiteConfig | ||
| # Function: getSiteConfig() | ||
| > **getSiteConfig**(`urlString`, `proxyConfig`): [`SiteCacheConfig`](../interfaces/SiteCacheConfig.md) | ||
| Defined in: [utils/getSiteConfig.ts:17](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/getSiteConfig.ts#L17) | ||
| 根据 URL 获取对应的站点缓存配置 | ||
| 匹配逻辑: | ||
| 1. 遍历 sites 中的所有 key。 | ||
| 2. 如果 key 是正则或 Glob 格式字符串,则对完整 URL 进行匹配。 | ||
| 3. 如果 key 是普通字符串,则作为 URL 前缀进行匹配。 | ||
| 4. 返回第一个匹配到的配置;若均未匹配,则返回 defaultConfig。 | ||
| ## Parameters | ||
| ### urlString | ||
| `string` | ||
| 请求的完整 URL | ||
| ### proxyConfig | ||
| [`ProxyConfig`](../interfaces/ProxyConfig.md) | ||
| 全局代理配置 | ||
| ## Returns | ||
| [`SiteCacheConfig`](../interfaces/SiteCacheConfig.md) | ||
| 匹配到的站点配置 |
| [**@isdk/proxy**](../README.md) | ||
| *** | ||
| [@isdk/proxy](../globals.md) / isGlob | ||
| # Function: isGlob() | ||
| > **isGlob**(`str`): `boolean` | ||
| Defined in: [utils/matcher.ts:7](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/matcher.ts#L7) | ||
| 判断一个模式是否为 Glob 模式 | ||
| ## Parameters | ||
| ### str | ||
| `string` | ||
| ## Returns | ||
| `boolean` |
| [**@isdk/proxy**](../README.md) | ||
| *** | ||
| [@isdk/proxy](../globals.md) / isMatch | ||
| # Function: isMatch() | ||
| > **isMatch**(`pattern`, `value`, `usePrefix`): `boolean` | ||
| Defined in: [utils/matcher.ts:25](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/matcher.ts#L25) | ||
| 通用匹配函数 | ||
| 逻辑优先级: | ||
| 1. 如果 pattern 是数组,遵循:(匹配任一正向模式) 且 (不匹配任一负向模式)。 | ||
| 2. 如果 pattern 是 RegExp 对象,直接使用 regex.test(value)。 | ||
| 3. 如果 pattern 是 "/regex/flags" 格式的字符串,转为 RegExp 后使用 test。 | ||
| 4. 如果 pattern 是 Glob 字符串,使用 picomatch 进行匹配。 | ||
| 5. 否则,根据 usePrefix 参数进行前缀匹配或精确匹配。 | ||
| ## Parameters | ||
| ### pattern | ||
| 匹配模式 (RegExp 或 字符串 或 数组) | ||
| `string` | `RegExp` | (`string` \| `RegExp`)[] | ||
| ### value | ||
| `string` | ||
| 要匹配的值 | ||
| ### usePrefix | ||
| `boolean` = `false` | ||
| 是否在普通字符串匹配时启用前缀匹配 (默认为 false,即精确匹配) | ||
| ## Returns | ||
| `boolean` |
| [**@isdk/proxy**](../README.md) | ||
| *** | ||
| [@isdk/proxy](../globals.md) / BodyFilterConfig | ||
| # Interface: BodyFilterConfig | ||
| Defined in: [types.ts:13](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L13) | ||
| 缓存键过滤配置 | ||
| 用于定义在生成缓存指纹时,哪些字段应该被包含或排除。 | ||
| ## Extends | ||
| - [`KeyFilterConfig`](KeyFilterConfig.md) | ||
| ## Properties | ||
| ### exclude? | ||
| > `optional` **exclude**: (`string` \| `RegExp`)[] | ||
| Defined in: [types.ts:10](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L10) | ||
| 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。 | ||
| #### Inherited from | ||
| [`KeyFilterConfig`](KeyFilterConfig.md).[`exclude`](KeyFilterConfig.md#exclude) | ||
| *** | ||
| ### extract? | ||
| > `optional` **extract**: `string` \| `RegExp` | ||
| Defined in: [types.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L18) | ||
| 用于非 JSON (文本) Body 的提取正则表达式。 | ||
| 如果包含捕获组,则提取捕获组内容作为指纹;否则提取整个匹配部分。 | ||
| *** | ||
| ### include? | ||
| > `optional` **include**: (`string` \| `RegExp`)[] | ||
| Defined in: [types.ts:8](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L8) | ||
| 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。 | ||
| #### Inherited from | ||
| [`KeyFilterConfig`](KeyFilterConfig.md).[`include`](KeyFilterConfig.md#include) | ||
| *** | ||
| ### maxLength? | ||
| > `optional` **maxLength**: `number` | ||
| Defined in: [types.ts:25](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L25) | ||
| 用于正则匹配/提取 Body 时的最大长度限制,默认 1024 (1KB) | ||
| *** | ||
| ### sort? | ||
| > `optional` **sort**: `boolean` | ||
| Defined in: [types.ts:23](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L23) | ||
| 是否对提取出的捕获组进行排序。 | ||
| 开启后可解决 Body 中参数顺序不一致导致的指纹失效问题。 |
| [**@isdk/proxy**](../README.md) | ||
| *** | ||
| [@isdk/proxy](../globals.md) / CacheRule | ||
| # Interface: CacheRule | ||
| Defined in: [types.ts:35](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L35) | ||
| 精细化缓存匹配规则 | ||
| 用于在 `methods` 过滤的基础上,进一步限定哪些具体的请求路径或参数需要被缓存。 | ||
| 多个规则之间是 **OR (逻辑或)** 关系,即请求只需匹配其中一条规则即可。 | ||
| 在单个规则对象内部,各字段之间是 **AND (逻辑与)** 关系。 | ||
| ## Properties | ||
| ### body? | ||
| > `optional` **body**: `string` \| `RegExp` \| (`string` \| `RegExp`)[] | ||
| Defined in: [types.ts:75](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L75) | ||
| Body 内容匹配。 | ||
| 仅当 Body 为文本或 JSON 时有效。 | ||
| - 字符串: 支持 Glob 模式匹配。 | ||
| - 正则表达式: 检查 Body 内容是否匹配。 | ||
| - 数组: 支持传入多个模式。 | ||
| *** | ||
| ### bodyType? | ||
| > `optional` **bodyType**: `"json"` \| `"text"` \| `"binary"` | ||
| Defined in: [types.ts:67](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L67) | ||
| 强制指定 Body 类型。 | ||
| 如果不指定,则根据 `Content-Type` 自动判断。 | ||
| *** | ||
| ### method? | ||
| > `optional` **method**: `string` | ||
| Defined in: [types.ts:40](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L40) | ||
| 匹配的方法 (如 "POST")。 | ||
| 如果指定,则必须方法完全一致;如果不指定,则匹配所有 `methods` 中允许的方法。 | ||
| *** | ||
| ### path? | ||
| > `optional` **path**: `string` \| `RegExp` \| (`string` \| `RegExp`)[] | ||
| Defined in: [types.ts:52](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L52) | ||
| 路径匹配。 | ||
| - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。 | ||
| - 正则表达式: 检查 `url.pathname` 是否匹配。 | ||
| - 数组: 支持传入多个模式(含否定模式),只要其中一个匹配即可。 | ||
| *** | ||
| ### query? | ||
| > `optional` **query**: `Record`\<`string`, `string` \| `boolean` \| `RegExp`\> | ||
| Defined in: [types.ts:62](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L62) | ||
| Query 参数匹配规则。 | ||
| - 键名: 支持字符串、Glob 或正则。 | ||
| - 值: | ||
| - 字符串: 支持 Glob 模式匹配。 | ||
| - 正则表达式: 检查参数值是否匹配。 | ||
| - `true`: 要求该参数必须存在于 URL 中。 | ||
| - `false`: 要求该参数必须 **不** 存在于 URL 中。 |
+272
-30
@@ -1,3 +0,1 @@ | ||
| import { KeyvCacheableMemoryOptions } from '@cacheable/memory'; | ||
| /** | ||
@@ -9,20 +7,100 @@ * 缓存键过滤配置 | ||
| interface KeyFilterConfig { | ||
| /** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算 */ | ||
| include?: string[]; | ||
| /** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段 */ | ||
| exclude?: string[]; | ||
| /** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。 */ | ||
| include?: (string | RegExp)[]; | ||
| /** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。 */ | ||
| exclude?: (string | RegExp)[]; | ||
| } | ||
| interface BodyFilterConfig extends KeyFilterConfig { | ||
| /** | ||
| * 用于非 JSON (文本) Body 的提取正则表达式。 | ||
| * 如果包含捕获组,则提取捕获组内容作为指纹;否则提取整个匹配部分。 | ||
| */ | ||
| extract?: string | RegExp; | ||
| /** | ||
| * 是否对提取出的捕获组进行排序。 | ||
| * 开启后可解决 Body 中参数顺序不一致导致的指纹失效问题。 | ||
| */ | ||
| sort?: boolean; | ||
| /** 用于正则匹配/提取 Body 时的最大长度限制,默认 1024 (1KB) */ | ||
| maxLength?: number; | ||
| } | ||
| /** | ||
| * 精细化缓存匹配规则 | ||
| * | ||
| * 用于在 `methods` 过滤的基础上,进一步限定哪些具体的请求路径或参数需要被缓存。 | ||
| * 多个规则之间是 **OR (逻辑或)** 关系,即请求只需匹配其中一条规则即可。 | ||
| * 在单个规则对象内部,各字段之间是 **AND (逻辑与)** 关系。 | ||
| */ | ||
| interface CacheRule { | ||
| /** | ||
| * 匹配的方法 (如 "POST")。 | ||
| * 如果指定,则必须方法完全一致;如果不指定,则匹配所有 `methods` 中允许的方法。 | ||
| */ | ||
| method?: string; | ||
| /** | ||
| * 路径匹配。 | ||
| * - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。 | ||
| * - 正则表达式: 检查 `url.pathname` 是否匹配。 | ||
| */ | ||
| /** | ||
| * 路径匹配。 | ||
| * - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。 | ||
| * - 正则表达式: 检查 `url.pathname` 是否匹配。 | ||
| * - 数组: 支持传入多个模式(含否定模式),只要其中一个匹配即可。 | ||
| */ | ||
| path?: string | RegExp | (string | RegExp)[]; | ||
| /** | ||
| * Query 参数匹配规则。 | ||
| * - 键名: 支持字符串、Glob 或正则。 | ||
| * - 值: | ||
| * - 字符串: 支持 Glob 模式匹配。 | ||
| * - 正则表达式: 检查参数值是否匹配。 | ||
| * - `true`: 要求该参数必须存在于 URL 中。 | ||
| * - `false`: 要求该参数必须 **不** 存在于 URL 中。 | ||
| */ | ||
| query?: Record<string, string | boolean | RegExp>; | ||
| /** | ||
| * 强制指定 Body 类型。 | ||
| * 如果不指定,则根据 `Content-Type` 自动判断。 | ||
| */ | ||
| bodyType?: 'json' | 'text' | 'binary'; | ||
| /** | ||
| * Body 内容匹配。 | ||
| * 仅当 Body 为文本或 JSON 时有效。 | ||
| * - 字符串: 支持 Glob 模式匹配。 | ||
| * - 正则表达式: 检查 Body 内容是否匹配。 | ||
| * - 数组: 支持传入多个模式。 | ||
| */ | ||
| body?: string | RegExp | (string | RegExp)[]; | ||
| } | ||
| /** | ||
| * 站点级缓存配置 | ||
| */ | ||
| interface SiteCacheConfig { | ||
| /** Query 参数过滤配置 */ | ||
| /** | ||
| * 允许缓存的 HTTP 方法列表。 | ||
| * 默认值: ['GET', 'HEAD']。 | ||
| * 若要缓存 POST/PUT,必须在此显式添加,并确保后端响应满足缓存条件(或开启 `forceCache`)。 | ||
| */ | ||
| methods?: string[]; | ||
| /** | ||
| * 精细化缓存规则列表。 | ||
| * 如果配置了此项,请求必须匹配其中至少一条规则才会被允许进入缓存流程。 | ||
| * 适用于只希望缓存特定 API 接口的场景。 | ||
| */ | ||
| cacheRules?: CacheRule[]; | ||
| /** Query 参数过滤配置:决定哪些查询参数参与缓存指纹 (Cache Key) 的计算 */ | ||
| query?: KeyFilterConfig; | ||
| /** 请求头过滤配置 */ | ||
| /** 请求头过滤配置:决定哪些 Header 参与缓存指纹计算 */ | ||
| headers?: KeyFilterConfig; | ||
| /** Cookie 过滤配置 */ | ||
| /** Cookie 过滤配置:决定哪些 Cookie 字段参与缓存指纹计算 */ | ||
| cookies?: KeyFilterConfig; | ||
| /** 当后端请求失败且存在旧缓存时,是否强制返回旧缓存(容错机制) */ | ||
| /** | ||
| * 请求体过滤配置 (仅限 JSON 类型)。 | ||
| * 当方法为 POST/PUT/PATCH 且为 JSON 格式时,用于从 Body 中提取特定字段参与指纹计算。 | ||
| */ | ||
| body?: BodyFilterConfig; | ||
| /** 容错机制:当后端请求失败(网络错误或 5xx)且存在旧缓存时,是否强制返回旧缓存 */ | ||
| staleIfError?: boolean; | ||
| /** 是否强制缓存一切响应(无视 no-store 等不缓存指令),用于极端的离线可用容错场景 */ | ||
| /** 强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。 */ | ||
| forceCache?: boolean; | ||
@@ -79,7 +157,24 @@ } | ||
| maxMemorySize?: number; | ||
| /** 透传给 L1 (Memory) 的高级配置 */ | ||
| memoryOptions?: Partial<KeyvCacheableMemoryOptions>; | ||
| /** 内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。 */ | ||
| maxTotalMemorySize?: number; | ||
| /** 透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options) */ | ||
| memoryOptions?: { | ||
| capacity?: number; | ||
| expires?: number; | ||
| cleanInterval?: number; | ||
| [key: string]: any; | ||
| }; | ||
| } | ||
| /** | ||
| * 智能混合缓存类 (Hybrid Cache) | ||
| * 智能混合缓存类 (Hybrid Multi-tier Cache) | ||
| * | ||
| * 该类实现了 L1 (内存) 和 L2 (磁盘) 的双层混合存储架构,旨在提供高性能且大容量的缓存能力。 | ||
| * | ||
| * ### 核心特性: | ||
| * - **双层架构**: L1 使用 LRU 内存缓存(基于 `secondary-cache` 的 LRUCache),L2 使用持久化磁盘缓存(基于 `cacache`)。 | ||
| * - **大小感知存储**: 自动识别响应体大小。小于阈值的文件同时存于内存和磁盘;超过阈值的文件仅存于磁盘,但其元数据仍保留在内存中。 | ||
| * - **元数据驻留 (Meta-Residency)**: 无论 Body 多大,Headers、Status、Policy 等信息始终优先从内存读取,确保缓存判定性能。 | ||
| * - **流式支持**: 支持通过 `setStream` 和 `getStream` 直接操作大数据流,防止 OOM。 | ||
| * - **一致性保障**: 在并发写入时自动清理内存,确保后续读取不会拿到被污染的旧数据。 | ||
| * - **内存限制**: 通过 `maxTotalMemorySize` 控制 L1 缓存的总内存占用。 | ||
| */ | ||
@@ -93,7 +188,21 @@ declare class SmartCache { | ||
| * 获取缓存条目 | ||
| * 如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。 | ||
| * | ||
| * 逻辑: | ||
| * 1. 首先尝试从 L1 内存获取。 | ||
| * 2. 如果内存中有 Body,直接返回(Buffer 类型)。 | ||
| * 3. 如果内存中只有 Meta(大文件),则从 L2 磁盘创建并返回 ReadStream。 | ||
| * 4. 如果内存完全未命中,从磁盘 L2 检索,并根据大小决定是否回填 L1。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @returns 完整的缓存条目(带 Buffer 或 Stream 的 Body),未命中返回 null | ||
| */ | ||
| get(key: string): Promise<CacheEntry | null>; | ||
| /** | ||
| * 写入缓存 | ||
| * 写入缓存条目 (原子写入) | ||
| * | ||
| * 适用于已知长度的小型数据块。该操作会同时写入磁盘并回填内存(如果大小未超标)。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @param body - 响应体数据 Buffer | ||
| * @param metadata - 响应元数据(不含 size,由本方法自动计算) | ||
| */ | ||
@@ -105,3 +214,23 @@ set(key: string, body: Buffer, metadata: Omit<CacheMetadata, 'size'>): Promise<void>; | ||
| private saveToMemory; | ||
| /** | ||
| * 获取磁盘读取流 | ||
| * | ||
| * 允许直接从 L2 磁盘层以流的形式读取数据,适用于大文件代理。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @returns Node.js 可读流 | ||
| */ | ||
| getStream(key: string): NodeJS.ReadableStream; | ||
| /** | ||
| * 获取磁盘写入流 (流式缓存) | ||
| * | ||
| * 该方法用于支持真正的流式代理。它会执行以下一致性操作: | ||
| * 1. 立即清除 L1 内存中的对应键,防止读到旧数据。 | ||
| * 2. 返回一个可写流,数据将直接流入磁盘。 | ||
| * 3. **一致性修复**: 在流写入完成(finish)时再次清理内存,防止写入期间的并发读取将旧数据再次回填进内存。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @param metadata - 响应元数据 | ||
| * @returns Node.js 可写流 | ||
| */ | ||
| setStream(key: string, metadata: Omit<CacheMetadata, 'size'>): NodeJS.WritableStream; | ||
@@ -113,5 +242,32 @@ delete(key: string): Promise<void>; | ||
| /** | ||
| * 根据 Request 和配置生成唯一的缓存键 | ||
| * 根据 Request 对象和站点配置生成唯一的缓存指纹 (异步) | ||
| * | ||
| * 该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。 | ||
| * 它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。 | ||
| * | ||
| * ### 生成指纹包含的要素: | ||
| * 1. **Method**: 请求方法(统一转为大写)。 | ||
| * 2. **Host & Path**: 请求的域名和路径。 | ||
| * 3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。 | ||
| * 4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。 | ||
| * 5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。 | ||
| * 6. **Request Body**: | ||
| * - 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。 | ||
| * - **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。 | ||
| * - **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。 | ||
| * - **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。 | ||
| * | ||
| * @param req - 原始 Web 标准 Request 对象。 | ||
| * @param config - 站点级缓存配置,决定了哪些字段参与指纹计算。 | ||
| * @returns 返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const cacheKey = await generateCacheKey(request, { | ||
| * query: { exclude: ['timestamp'] }, | ||
| * body: { include: ['id', 'action'] } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare const generateCacheKey: (req: Request, config: SiteCacheConfig) => string; | ||
| declare function generateCacheKey(req: Request, config: SiteCacheConfig): Promise<string>; | ||
@@ -150,2 +306,4 @@ /** | ||
| * 实现了基于流的混合缓存代理核心逻辑,主要机制包括: | ||
| * - **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。 | ||
| * - **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。 | ||
| * - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM。 | ||
@@ -157,2 +315,7 @@ * - **SWR (Stale-While-Revalidate)**:后台静默更新机制。 | ||
| * 并且会在响应头中自动注入 `x-proxy-cache` 标明缓存命中状态 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`)。 | ||
| * | ||
| * @param request - 原始 Web 标准 Request 对象 | ||
| * @param fetcher - 实际执行网络请求的函数 | ||
| * @param options - 缓存配置选项 | ||
| * @returns 带有缓存标识头和流式 Body 的 Response 对象 | ||
| */ | ||
@@ -195,3 +358,3 @@ declare function fetchWithCache(request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions): Promise<Response>; | ||
| * 此函数主要用于生成缓存指纹。它会: | ||
| * 1. 根据 `config` (include/exclude) 过滤键。 | ||
| * 1. 根据 `config` (include/exclude) 过滤键,调用 `isAllowed` 判断每个键是否允许。 | ||
| * 2. 对键进行排序以保证指纹的一致性。 | ||
@@ -201,7 +364,30 @@ * 3. 将所有键转换为小写。 | ||
| * | ||
| * **关于 `defaultAllowed` 参数**: | ||
| * - 只有当没有配置 `include` 和 `exclude` 时,`defaultAllowed` 才会生效。 | ||
| * - 如果配置了 `include`(即使为空数组),`defaultAllowed` 也不会生效。 | ||
| * - 详见 `isAllowed` 函数的优先级逻辑。 | ||
| * | ||
| * @param source 原始数据对象 (如 QueryParams, Headers, Cookies) | ||
| * @param config 过滤配置 (白名单或黑名单) | ||
| * @returns 标准化后的数据 Map,键为小写,值为字符串数组 | ||
| * @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单) | ||
| * @param defaultAllowed 当没有配置时的默认值(默认 `false`,即不提取任何键) | ||
| * @returns 标准化后的数据 Map,键为小写,值为排序后的字符串数组 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' }; | ||
| * | ||
| * // 默认不提取任何键 | ||
| * extractData(headers); // {} | ||
| * | ||
| * // 提取所有键 | ||
| * extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] } | ||
| * | ||
| * // 白名单 | ||
| * extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] } | ||
| * | ||
| * // 黑名单(需要 include 或 defaultAllowed) | ||
| * extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] } | ||
| * ``` | ||
| */ | ||
| declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig) => Record<string, string[]>; | ||
| declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig, defaultAllowed?: boolean) => Record<string, string[]>; | ||
@@ -211,13 +397,69 @@ /** | ||
| * | ||
| * 优先级逻辑: | ||
| * 1. 如果配置了 `include` (白名单),则只有存在于 `include` 中的键才会被允许。 | ||
| * 2. 否则,如果配置了 `exclude` (黑名单),则存在于 `exclude` 中的键将被拒绝。 | ||
| * 3. 如果都没有配置,默认允许所有键。 | ||
| * **优先级逻辑**: | ||
| * 1. `exclude` 命中 → 返回 `false`(优先级最高,会覆盖前面的结果) | ||
| * 2. `include` 存在且命中 → 返回 `true` | ||
| * 3. `include` 存在但不命中 → 返回 `false` | ||
| * 4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`) | ||
| * | ||
| * **注意**:`include` 和 `exclude` 可以同时配置,此时 `exclude` 优先级更高。 | ||
| * | ||
| * @param key 要检查的键名 | ||
| * @param config 过滤配置 | ||
| * @returns 是否允许 | ||
| * @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单) | ||
| * @param defaultAllowed 当没有配置或配置未命中时的默认值(可选) | ||
| * @returns 是否允许。返回 `boolean` 或 `undefined`(当没有配置且未传 defaultAllowed 时) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // 无配置 | ||
| * isAllowed('key'); // undefined | ||
| * | ||
| * // 白名单 | ||
| * isAllowed('id', { include: ['id', 'name'] }); // true | ||
| * isAllowed('email', { include: ['id', 'name'] }); // false | ||
| * | ||
| * // 黑名单 | ||
| * isAllowed('password', { exclude: ['password'] }); // false | ||
| * isAllowed('name', { exclude: ['password'] }); // undefined | ||
| * | ||
| * // 设置默认值 | ||
| * isAllowed('name', { exclude: ['password'] }, true); // true | ||
| * ``` | ||
| */ | ||
| declare function isAllowed(key: string, config?: KeyFilterConfig): boolean; | ||
| declare function isAllowed(key: string, config?: KeyFilterConfig, defaultAllowed?: boolean): boolean; | ||
| export { type CacheEntry, type CacheMetadata, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, isAllowed }; | ||
| /** | ||
| * 判断一个模式是否为 Glob 模式 | ||
| */ | ||
| declare function isGlob(str: string): boolean; | ||
| /** | ||
| * 通用匹配函数 | ||
| * | ||
| * 逻辑优先级: | ||
| * 1. 如果 pattern 是数组,遵循:(匹配任一正向模式) 且 (不匹配任一负向模式)。 | ||
| * 2. 如果 pattern 是 RegExp 对象,直接使用 regex.test(value)。 | ||
| * 3. 如果 pattern 是 "/regex/flags" 格式的字符串,转为 RegExp 后使用 test。 | ||
| * 4. 如果 pattern 是 Glob 字符串,使用 picomatch 进行匹配。 | ||
| * 5. 否则,根据 usePrefix 参数进行前缀匹配或精确匹配。 | ||
| * | ||
| * @param pattern 匹配模式 (RegExp 或 字符串 或 数组) | ||
| * @param value 要匹配的值 | ||
| * @param usePrefix 是否在普通字符串匹配时启用前缀匹配 (默认为 false,即精确匹配) | ||
| */ | ||
| declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value: string, usePrefix?: boolean): boolean; | ||
| /** | ||
| * 根据 URL 获取对应的站点缓存配置 | ||
| * | ||
| * 匹配逻辑: | ||
| * 1. 遍历 sites 中的所有 key。 | ||
| * 2. 如果 key 是正则或 Glob 格式字符串,则对完整 URL 进行匹配。 | ||
| * 3. 如果 key 是普通字符串,则作为 URL 前缀进行匹配。 | ||
| * 4. 返回第一个匹配到的配置;若均未匹配,则返回 defaultConfig。 | ||
| * | ||
| * @param urlString 请求的完整 URL | ||
| * @param proxyConfig 全局代理配置 | ||
| * @returns 匹配到的站点配置 | ||
| */ | ||
| declare function getSiteConfig(urlString: string, proxyConfig: ProxyConfig): SiteCacheConfig; | ||
| export { type BodyFilterConfig, type CacheEntry, type CacheMetadata, type CacheRule, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, getSiteConfig, isAllowed, isGlob, isMatch }; |
+272
-30
@@ -1,3 +0,1 @@ | ||
| import { KeyvCacheableMemoryOptions } from '@cacheable/memory'; | ||
| /** | ||
@@ -9,20 +7,100 @@ * 缓存键过滤配置 | ||
| interface KeyFilterConfig { | ||
| /** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算 */ | ||
| include?: string[]; | ||
| /** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段 */ | ||
| exclude?: string[]; | ||
| /** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。 */ | ||
| include?: (string | RegExp)[]; | ||
| /** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。 */ | ||
| exclude?: (string | RegExp)[]; | ||
| } | ||
| interface BodyFilterConfig extends KeyFilterConfig { | ||
| /** | ||
| * 用于非 JSON (文本) Body 的提取正则表达式。 | ||
| * 如果包含捕获组,则提取捕获组内容作为指纹;否则提取整个匹配部分。 | ||
| */ | ||
| extract?: string | RegExp; | ||
| /** | ||
| * 是否对提取出的捕获组进行排序。 | ||
| * 开启后可解决 Body 中参数顺序不一致导致的指纹失效问题。 | ||
| */ | ||
| sort?: boolean; | ||
| /** 用于正则匹配/提取 Body 时的最大长度限制,默认 1024 (1KB) */ | ||
| maxLength?: number; | ||
| } | ||
| /** | ||
| * 精细化缓存匹配规则 | ||
| * | ||
| * 用于在 `methods` 过滤的基础上,进一步限定哪些具体的请求路径或参数需要被缓存。 | ||
| * 多个规则之间是 **OR (逻辑或)** 关系,即请求只需匹配其中一条规则即可。 | ||
| * 在单个规则对象内部,各字段之间是 **AND (逻辑与)** 关系。 | ||
| */ | ||
| interface CacheRule { | ||
| /** | ||
| * 匹配的方法 (如 "POST")。 | ||
| * 如果指定,则必须方法完全一致;如果不指定,则匹配所有 `methods` 中允许的方法。 | ||
| */ | ||
| method?: string; | ||
| /** | ||
| * 路径匹配。 | ||
| * - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。 | ||
| * - 正则表达式: 检查 `url.pathname` 是否匹配。 | ||
| */ | ||
| /** | ||
| * 路径匹配。 | ||
| * - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。 | ||
| * - 正则表达式: 检查 `url.pathname` 是否匹配。 | ||
| * - 数组: 支持传入多个模式(含否定模式),只要其中一个匹配即可。 | ||
| */ | ||
| path?: string | RegExp | (string | RegExp)[]; | ||
| /** | ||
| * Query 参数匹配规则。 | ||
| * - 键名: 支持字符串、Glob 或正则。 | ||
| * - 值: | ||
| * - 字符串: 支持 Glob 模式匹配。 | ||
| * - 正则表达式: 检查参数值是否匹配。 | ||
| * - `true`: 要求该参数必须存在于 URL 中。 | ||
| * - `false`: 要求该参数必须 **不** 存在于 URL 中。 | ||
| */ | ||
| query?: Record<string, string | boolean | RegExp>; | ||
| /** | ||
| * 强制指定 Body 类型。 | ||
| * 如果不指定,则根据 `Content-Type` 自动判断。 | ||
| */ | ||
| bodyType?: 'json' | 'text' | 'binary'; | ||
| /** | ||
| * Body 内容匹配。 | ||
| * 仅当 Body 为文本或 JSON 时有效。 | ||
| * - 字符串: 支持 Glob 模式匹配。 | ||
| * - 正则表达式: 检查 Body 内容是否匹配。 | ||
| * - 数组: 支持传入多个模式。 | ||
| */ | ||
| body?: string | RegExp | (string | RegExp)[]; | ||
| } | ||
| /** | ||
| * 站点级缓存配置 | ||
| */ | ||
| interface SiteCacheConfig { | ||
| /** Query 参数过滤配置 */ | ||
| /** | ||
| * 允许缓存的 HTTP 方法列表。 | ||
| * 默认值: ['GET', 'HEAD']。 | ||
| * 若要缓存 POST/PUT,必须在此显式添加,并确保后端响应满足缓存条件(或开启 `forceCache`)。 | ||
| */ | ||
| methods?: string[]; | ||
| /** | ||
| * 精细化缓存规则列表。 | ||
| * 如果配置了此项,请求必须匹配其中至少一条规则才会被允许进入缓存流程。 | ||
| * 适用于只希望缓存特定 API 接口的场景。 | ||
| */ | ||
| cacheRules?: CacheRule[]; | ||
| /** Query 参数过滤配置:决定哪些查询参数参与缓存指纹 (Cache Key) 的计算 */ | ||
| query?: KeyFilterConfig; | ||
| /** 请求头过滤配置 */ | ||
| /** 请求头过滤配置:决定哪些 Header 参与缓存指纹计算 */ | ||
| headers?: KeyFilterConfig; | ||
| /** Cookie 过滤配置 */ | ||
| /** Cookie 过滤配置:决定哪些 Cookie 字段参与缓存指纹计算 */ | ||
| cookies?: KeyFilterConfig; | ||
| /** 当后端请求失败且存在旧缓存时,是否强制返回旧缓存(容错机制) */ | ||
| /** | ||
| * 请求体过滤配置 (仅限 JSON 类型)。 | ||
| * 当方法为 POST/PUT/PATCH 且为 JSON 格式时,用于从 Body 中提取特定字段参与指纹计算。 | ||
| */ | ||
| body?: BodyFilterConfig; | ||
| /** 容错机制:当后端请求失败(网络错误或 5xx)且存在旧缓存时,是否强制返回旧缓存 */ | ||
| staleIfError?: boolean; | ||
| /** 是否强制缓存一切响应(无视 no-store 等不缓存指令),用于极端的离线可用容错场景 */ | ||
| /** 强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。 */ | ||
| forceCache?: boolean; | ||
@@ -79,7 +157,24 @@ } | ||
| maxMemorySize?: number; | ||
| /** 透传给 L1 (Memory) 的高级配置 */ | ||
| memoryOptions?: Partial<KeyvCacheableMemoryOptions>; | ||
| /** 内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。 */ | ||
| maxTotalMemorySize?: number; | ||
| /** 透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options) */ | ||
| memoryOptions?: { | ||
| capacity?: number; | ||
| expires?: number; | ||
| cleanInterval?: number; | ||
| [key: string]: any; | ||
| }; | ||
| } | ||
| /** | ||
| * 智能混合缓存类 (Hybrid Cache) | ||
| * 智能混合缓存类 (Hybrid Multi-tier Cache) | ||
| * | ||
| * 该类实现了 L1 (内存) 和 L2 (磁盘) 的双层混合存储架构,旨在提供高性能且大容量的缓存能力。 | ||
| * | ||
| * ### 核心特性: | ||
| * - **双层架构**: L1 使用 LRU 内存缓存(基于 `secondary-cache` 的 LRUCache),L2 使用持久化磁盘缓存(基于 `cacache`)。 | ||
| * - **大小感知存储**: 自动识别响应体大小。小于阈值的文件同时存于内存和磁盘;超过阈值的文件仅存于磁盘,但其元数据仍保留在内存中。 | ||
| * - **元数据驻留 (Meta-Residency)**: 无论 Body 多大,Headers、Status、Policy 等信息始终优先从内存读取,确保缓存判定性能。 | ||
| * - **流式支持**: 支持通过 `setStream` 和 `getStream` 直接操作大数据流,防止 OOM。 | ||
| * - **一致性保障**: 在并发写入时自动清理内存,确保后续读取不会拿到被污染的旧数据。 | ||
| * - **内存限制**: 通过 `maxTotalMemorySize` 控制 L1 缓存的总内存占用。 | ||
| */ | ||
@@ -93,7 +188,21 @@ declare class SmartCache { | ||
| * 获取缓存条目 | ||
| * 如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。 | ||
| * | ||
| * 逻辑: | ||
| * 1. 首先尝试从 L1 内存获取。 | ||
| * 2. 如果内存中有 Body,直接返回(Buffer 类型)。 | ||
| * 3. 如果内存中只有 Meta(大文件),则从 L2 磁盘创建并返回 ReadStream。 | ||
| * 4. 如果内存完全未命中,从磁盘 L2 检索,并根据大小决定是否回填 L1。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @returns 完整的缓存条目(带 Buffer 或 Stream 的 Body),未命中返回 null | ||
| */ | ||
| get(key: string): Promise<CacheEntry | null>; | ||
| /** | ||
| * 写入缓存 | ||
| * 写入缓存条目 (原子写入) | ||
| * | ||
| * 适用于已知长度的小型数据块。该操作会同时写入磁盘并回填内存(如果大小未超标)。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @param body - 响应体数据 Buffer | ||
| * @param metadata - 响应元数据(不含 size,由本方法自动计算) | ||
| */ | ||
@@ -105,3 +214,23 @@ set(key: string, body: Buffer, metadata: Omit<CacheMetadata, 'size'>): Promise<void>; | ||
| private saveToMemory; | ||
| /** | ||
| * 获取磁盘读取流 | ||
| * | ||
| * 允许直接从 L2 磁盘层以流的形式读取数据,适用于大文件代理。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @returns Node.js 可读流 | ||
| */ | ||
| getStream(key: string): NodeJS.ReadableStream; | ||
| /** | ||
| * 获取磁盘写入流 (流式缓存) | ||
| * | ||
| * 该方法用于支持真正的流式代理。它会执行以下一致性操作: | ||
| * 1. 立即清除 L1 内存中的对应键,防止读到旧数据。 | ||
| * 2. 返回一个可写流,数据将直接流入磁盘。 | ||
| * 3. **一致性修复**: 在流写入完成(finish)时再次清理内存,防止写入期间的并发读取将旧数据再次回填进内存。 | ||
| * | ||
| * @param key - 缓存指纹键 | ||
| * @param metadata - 响应元数据 | ||
| * @returns Node.js 可写流 | ||
| */ | ||
| setStream(key: string, metadata: Omit<CacheMetadata, 'size'>): NodeJS.WritableStream; | ||
@@ -113,5 +242,32 @@ delete(key: string): Promise<void>; | ||
| /** | ||
| * 根据 Request 和配置生成唯一的缓存键 | ||
| * 根据 Request 对象和站点配置生成唯一的缓存指纹 (异步) | ||
| * | ||
| * 该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。 | ||
| * 它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。 | ||
| * | ||
| * ### 生成指纹包含的要素: | ||
| * 1. **Method**: 请求方法(统一转为大写)。 | ||
| * 2. **Host & Path**: 请求的域名和路径。 | ||
| * 3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。 | ||
| * 4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。 | ||
| * 5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。 | ||
| * 6. **Request Body**: | ||
| * - 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。 | ||
| * - **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。 | ||
| * - **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。 | ||
| * - **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。 | ||
| * | ||
| * @param req - 原始 Web 标准 Request 对象。 | ||
| * @param config - 站点级缓存配置,决定了哪些字段参与指纹计算。 | ||
| * @returns 返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const cacheKey = await generateCacheKey(request, { | ||
| * query: { exclude: ['timestamp'] }, | ||
| * body: { include: ['id', 'action'] } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare const generateCacheKey: (req: Request, config: SiteCacheConfig) => string; | ||
| declare function generateCacheKey(req: Request, config: SiteCacheConfig): Promise<string>; | ||
@@ -150,2 +306,4 @@ /** | ||
| * 实现了基于流的混合缓存代理核心逻辑,主要机制包括: | ||
| * - **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。 | ||
| * - **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。 | ||
| * - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM。 | ||
@@ -157,2 +315,7 @@ * - **SWR (Stale-While-Revalidate)**:后台静默更新机制。 | ||
| * 并且会在响应头中自动注入 `x-proxy-cache` 标明缓存命中状态 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`)。 | ||
| * | ||
| * @param request - 原始 Web 标准 Request 对象 | ||
| * @param fetcher - 实际执行网络请求的函数 | ||
| * @param options - 缓存配置选项 | ||
| * @returns 带有缓存标识头和流式 Body 的 Response 对象 | ||
| */ | ||
@@ -195,3 +358,3 @@ declare function fetchWithCache(request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions): Promise<Response>; | ||
| * 此函数主要用于生成缓存指纹。它会: | ||
| * 1. 根据 `config` (include/exclude) 过滤键。 | ||
| * 1. 根据 `config` (include/exclude) 过滤键,调用 `isAllowed` 判断每个键是否允许。 | ||
| * 2. 对键进行排序以保证指纹的一致性。 | ||
@@ -201,7 +364,30 @@ * 3. 将所有键转换为小写。 | ||
| * | ||
| * **关于 `defaultAllowed` 参数**: | ||
| * - 只有当没有配置 `include` 和 `exclude` 时,`defaultAllowed` 才会生效。 | ||
| * - 如果配置了 `include`(即使为空数组),`defaultAllowed` 也不会生效。 | ||
| * - 详见 `isAllowed` 函数的优先级逻辑。 | ||
| * | ||
| * @param source 原始数据对象 (如 QueryParams, Headers, Cookies) | ||
| * @param config 过滤配置 (白名单或黑名单) | ||
| * @returns 标准化后的数据 Map,键为小写,值为字符串数组 | ||
| * @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单) | ||
| * @param defaultAllowed 当没有配置时的默认值(默认 `false`,即不提取任何键) | ||
| * @returns 标准化后的数据 Map,键为小写,值为排序后的字符串数组 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' }; | ||
| * | ||
| * // 默认不提取任何键 | ||
| * extractData(headers); // {} | ||
| * | ||
| * // 提取所有键 | ||
| * extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] } | ||
| * | ||
| * // 白名单 | ||
| * extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] } | ||
| * | ||
| * // 黑名单(需要 include 或 defaultAllowed) | ||
| * extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] } | ||
| * ``` | ||
| */ | ||
| declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig) => Record<string, string[]>; | ||
| declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig, defaultAllowed?: boolean) => Record<string, string[]>; | ||
@@ -211,13 +397,69 @@ /** | ||
| * | ||
| * 优先级逻辑: | ||
| * 1. 如果配置了 `include` (白名单),则只有存在于 `include` 中的键才会被允许。 | ||
| * 2. 否则,如果配置了 `exclude` (黑名单),则存在于 `exclude` 中的键将被拒绝。 | ||
| * 3. 如果都没有配置,默认允许所有键。 | ||
| * **优先级逻辑**: | ||
| * 1. `exclude` 命中 → 返回 `false`(优先级最高,会覆盖前面的结果) | ||
| * 2. `include` 存在且命中 → 返回 `true` | ||
| * 3. `include` 存在但不命中 → 返回 `false` | ||
| * 4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`) | ||
| * | ||
| * **注意**:`include` 和 `exclude` 可以同时配置,此时 `exclude` 优先级更高。 | ||
| * | ||
| * @param key 要检查的键名 | ||
| * @param config 过滤配置 | ||
| * @returns 是否允许 | ||
| * @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单) | ||
| * @param defaultAllowed 当没有配置或配置未命中时的默认值(可选) | ||
| * @returns 是否允许。返回 `boolean` 或 `undefined`(当没有配置且未传 defaultAllowed 时) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // 无配置 | ||
| * isAllowed('key'); // undefined | ||
| * | ||
| * // 白名单 | ||
| * isAllowed('id', { include: ['id', 'name'] }); // true | ||
| * isAllowed('email', { include: ['id', 'name'] }); // false | ||
| * | ||
| * // 黑名单 | ||
| * isAllowed('password', { exclude: ['password'] }); // false | ||
| * isAllowed('name', { exclude: ['password'] }); // undefined | ||
| * | ||
| * // 设置默认值 | ||
| * isAllowed('name', { exclude: ['password'] }, true); // true | ||
| * ``` | ||
| */ | ||
| declare function isAllowed(key: string, config?: KeyFilterConfig): boolean; | ||
| declare function isAllowed(key: string, config?: KeyFilterConfig, defaultAllowed?: boolean): boolean; | ||
| export { type CacheEntry, type CacheMetadata, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, isAllowed }; | ||
| /** | ||
| * 判断一个模式是否为 Glob 模式 | ||
| */ | ||
| declare function isGlob(str: string): boolean; | ||
| /** | ||
| * 通用匹配函数 | ||
| * | ||
| * 逻辑优先级: | ||
| * 1. 如果 pattern 是数组,遵循:(匹配任一正向模式) 且 (不匹配任一负向模式)。 | ||
| * 2. 如果 pattern 是 RegExp 对象,直接使用 regex.test(value)。 | ||
| * 3. 如果 pattern 是 "/regex/flags" 格式的字符串,转为 RegExp 后使用 test。 | ||
| * 4. 如果 pattern 是 Glob 字符串,使用 picomatch 进行匹配。 | ||
| * 5. 否则,根据 usePrefix 参数进行前缀匹配或精确匹配。 | ||
| * | ||
| * @param pattern 匹配模式 (RegExp 或 字符串 或 数组) | ||
| * @param value 要匹配的值 | ||
| * @param usePrefix 是否在普通字符串匹配时启用前缀匹配 (默认为 false,即精确匹配) | ||
| */ | ||
| declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value: string, usePrefix?: boolean): boolean; | ||
| /** | ||
| * 根据 URL 获取对应的站点缓存配置 | ||
| * | ||
| * 匹配逻辑: | ||
| * 1. 遍历 sites 中的所有 key。 | ||
| * 2. 如果 key 是正则或 Glob 格式字符串,则对完整 URL 进行匹配。 | ||
| * 3. 如果 key 是普通字符串,则作为 URL 前缀进行匹配。 | ||
| * 4. 返回第一个匹配到的配置;若均未匹配,则返回 defaultConfig。 | ||
| * | ||
| * @param urlString 请求的完整 URL | ||
| * @param proxyConfig 全局代理配置 | ||
| * @returns 匹配到的站点配置 | ||
| */ | ||
| declare function getSiteConfig(urlString: string, proxyConfig: ProxyConfig): SiteCacheConfig; | ||
| export { type BodyFilterConfig, type CacheEntry, type CacheMetadata, type CacheRule, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, getSiteConfig, isAllowed, isGlob, isMatch }; |
+1
-1
@@ -1,1 +0,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})}} | ||
| "use strict";var t,e=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,s=Object.prototype.hasOwnProperty,c=(t,e,a,c)=>{if(e&&"object"==typeof e||"function"==typeof e)for(let o of i(e))s.call(t,o)||o===a||n(t,o,{get:()=>e[o],enumerable:!(c=r(e,o))||c.enumerable});return t},o=(t,r,i)=>(i=null!=t?e(a(t)):{},c(!r&&t&&t.__esModule?i:n(i,"default",{value:t,enumerable:!0}),t)),u={};((t,e)=>{for(var r in e)n(t,r,{get:e[r],enumerable:!0})})(u,{SmartCache:()=>w,createCachedFetch:()=>I,createFetchWithCache:()=>H,extractData:()=>T,fetchWithCache:()=>C,generateCacheKey:()=>R,getSiteConfig:()=>g,isAllowed:()=>j,isGlob:()=>x,isMatch:()=>O}),module.exports=(t=u,c(n({},"__esModule",{value:!0}),t));var f=require("secondary-cache"),h=o(require("cacache")),l=o(require("os")),y=o(require("path")),w=class{memory;storagePath;maxMemorySize;constructor(t={}){this.storagePath=t.storagePath||y.default.join(l.default.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=t.maxMemorySize??1048576;const e={capacity:0,expires:3e5,maxWeight:t.maxTotalMemorySize||104857600,weightOf:t=>{let e=0;return t.body&&Buffer.isBuffer(t.body)&&(e+=t.body.length),e+=512,e},...t.memoryOptions};this.memory=new f.LRUCache(e)}async get(t){const e=this.memory.get(t);if(e){if(e.body)return e;if(0===e.size)return{...e,body:Buffer.alloc(0)};const n=h.default.get.stream(this.storagePath,t);return{...e,body:n}}try{const e=await h.default.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:n}=await h.default.get(this.storagePath,t),r=n,i={...r,body:e};return this.saveToMemory(t,e,r),i}{const e=(await h.default.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,e),{...e,body:h.default.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,n){const r={...n,size:e.length};await h.default.put(this.storagePath,t,e,{metadata:r}),this.saveToMemory(t,e,r)}saveToMemory(t,e,n){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...n,body:e});else{const{...e}=n;this.memory.set(t,e)}}getStream(t){return h.default.get.stream(this.storagePath,t)}setStream(t,e){this.memory.del(t);const n=h.default.put.stream(this.storagePath,t,{metadata:e});return n.on("finish",()=>{this.memory.del(t)}),n}async delete(t){this.memory.del(t),await h.default.rm.entry(this.storagePath,t)}async clear(){this.memory.clear(),await h.default.rm.all(this.storagePath)}},d=require("crypto"),p=require("util-ex"),m=o(require("picomatch")),b=require("util-ex");function x(t){return/[!*?{}[\]()]/.test(t)}function O(t,e,n=!1){if(Array.isArray(t)&&t.length){const r=[],i=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?i.push(t.slice(1)):r.push(t)}),!(i.length>0&&i.some(t=>O(t,e,n)))&&(0===r.length||r.some(t=>O(t,e,n)))}return t instanceof RegExp?t.test(e):"string"==typeof t&&((0,b.isRegExpStr)(t)?(0,b.toRegExp)(t).test(e):x(t)?(0,m.default)(t,{dot:!0})(e):n?e.startsWith(t):e===t)}function j(t,e,n){let r;return e?.include&&(r=O(e.include,t)),e?.exclude&&O(e.exclude,t)&&(r=!1),void 0===r&&(r=n),r}var T=(t,e,n)=>{const r={};return Object.keys(t).filter(t=>j(t,e,n)).sort().forEach(e=>{const n=t[e];null!=n&&(r[e.toLowerCase()]=Array.isArray(n)?[...n].sort():[n])}),r};function g(t,e){const{sites:n,default:r}=e;if(!n)return r;for(const[e,r]of Object.entries(n))if(O(e,t,!0))return r;return r}async function R(t,e){const n=new URL(t.url),r=t.headers.get("cookie")||"",i=Object.fromEntries(r.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let a=null;const s=t.method.toUpperCase();if(["POST","PUT","PATCH"].includes(s))try{const n=t.headers.get("content-type")||"";if(n.includes("application/json")){const n=await t.clone().json();a=T(n,e.body,!0)}else if(e.body?.extract&&(n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded"))){const n=e.body?.maxLength||1024,r=(await t.clone().text()).slice(0,n),i=e.body.extract,s="string"==typeof i&&(0,p.isRegExpStr)(i)?(0,p.toRegExp)(i):i instanceof RegExp?i:null;if(s){const t=r.match(s);if(t)if(t.length>1){const n=t.slice(1);e.body?.sort&&n.sort(),a=n.join(":")}else a=t[0]}else a=(0,d.createHash)("sha256").update(r).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(a=(0,d.createHash)("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const c={m:s,h:n.host,p:n.pathname,q:T(Object.fromEntries(n.searchParams),e.query,!0),hd:T(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:T(i,e.cookies)};return null!==a&&(c.b=a),(0,d.createHash)("sha256").update(JSON.stringify(c)).digest("hex")}var S=require("stream"),q=require("stream/promises"),A=o(require("http-cache-semantics"));function E(t,e){const n=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?S.Readable.toWeb(t):t}(t.body);return new Response(n,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function v(t,e){let n,r;const i=new Promise((e,i)=>{n=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},r=e=>{t.activeCacheWrites.delete(t.cacheKey),i(e)}});i.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,i);try{const e=await t.fetcher(t.request.clone()),i=new A.default({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),a=new Headers(e.headers);if(a.set("x-proxy-cache","MISS"),!i.storable()&&!t.config.forceCache)return n(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:a});const s={status:e.status,headers:Object.fromEntries(e.headers),policy:i.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),s),n(),new Response(null,{status:e.status,statusText:e.statusText,headers:a});const[c,o]=e.body.tee();return(0,q.pipeline)(S.Readable.fromWeb(o),t.cache.setStream(t.cacheKey,s)).then(n).catch(r),new Response(c,{status:e.status,statusText:e.statusText,headers:a})}catch(n){if(r(n),e&&t.config.staleIfError)return E(e,"STALE_IF_ERROR");throw n}}async function C(t,e,n){if(!await async function(t,e){const n=t.method.toUpperCase();if(!(e.methods||["GET","HEAD"]).includes(n))return!1;if(e.cacheRules&&e.cacheRules.length>0){const r=new URL(t.url),i=r.searchParams;let a=null,s=!1;for(const o of e.cacheRules)if(await c(o,n,r,i,t))return!0;return!1;async function c(t,n,r,i,c){if(t.method&&t.method.toUpperCase()!==n)return!1;if(t.path&&!O(t.path,r.pathname,!0))return!1;if(t.query)for(const[e,n]of Object.entries(t.query)){const t=i.has(e),r=i.get(e)||"";if("boolean"==typeof n){if(n&&!t)return!1;if(!n&&t)return!1}else if(!O(n,r))return!1}if(t.bodyType||t.body){const n=c.headers.get("content-type")||"",r=n.includes("application/json")?"json":n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded")?"text":"binary";if(t.bodyType&&t.bodyType!==r)return!1;if(t.body){if("binary"===r)return!1;if(!s){try{const t=e.body?.maxLength||1024,n=await c.clone().text();a=n.slice(0,t)}catch(t){a=""}s=!0}if(!a||!O(t.body,a))return!1}}return!0}}return!0}(t,n.config))return e(t);const r=await async function(t,e,n){const r=n.generateKey||R,i=await r(t,n.config);return{...n,request:t,fetcher:e,cacheKey:i,activeCacheWrites:n.activeCacheWrites||new Map}}(t,e,n),i=await r.cache.get(r.cacheKey);if(i){const t=function(t,e){const n=A.default.fromObject(e.policy),r={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return n.satisfiesWithoutRevalidation(r)?"HIT":"STALE"}(r,i);if("HIT"===t)return E(i,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const n=v(t,e).catch(n=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,n),E(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(n)}(r,i),E(i,"STALE")}if(r.activeCacheWrites.has(r.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const n=await t.cache.get(t.cacheKey);return n?E(n,"HIT"):null}(r);if(t)return t}return v(r,i)}function H(t){return t||(t=new Map),async function(e,n,r){return C(e,n,{...r,activeCacheWrites:t})}}function I(t){const e=H(t.activeCacheWrites);return async function(n,r,i){return e(n,r,{...t,...i})}} |
+1
-1
@@ -1,1 +0,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}; | ||
| import{LRUCache as t}from"secondary-cache";import e from"cacache";import n from"os";import r from"path";var s=class{memory;storagePath;maxMemorySize;constructor(e={}){this.storagePath=e.storagePath||r.join(n.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=e.maxMemorySize??1048576;const s={capacity:0,expires:3e5,maxWeight:e.maxTotalMemorySize||104857600,weightOf:t=>{let e=0;return t.body&&Buffer.isBuffer(t.body)&&(e+=t.body.length),e+=512,e},...e.memoryOptions};this.memory=new t(s)}async get(t){const n=this.memory.get(t);if(n){if(n.body)return n;if(0===n.size)return{...n,body:Buffer.alloc(0)};const r=e.get.stream(this.storagePath,t);return{...n,body:r}}try{const n=await e.get.info(this.storagePath,t);if(!n)return null;if(n.size<=this.maxMemorySize){const{data:n,metadata:r}=await e.get(this.storagePath,t),s=r,i={...s,body:n};return this.saveToMemory(t,n,s),i}{const n=(await e.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,n),{...n,body:e.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,n,r){const s={...r,size:n.length};await e.put(this.storagePath,t,n,{metadata:s}),this.saveToMemory(t,n,s)}saveToMemory(t,e,n){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...n,body:e});else{const{...e}=n;this.memory.set(t,e)}}getStream(t){return e.get.stream(this.storagePath,t)}setStream(t,n){this.memory.del(t);const r=e.put.stream(this.storagePath,t,{metadata:n});return r.on("finish",()=>{this.memory.del(t)}),r}async delete(t){this.memory.del(t),await e.rm.entry(this.storagePath,t)}async clear(){this.memory.clear(),await e.rm.all(this.storagePath)}};import{createHash as i}from"crypto";import{isRegExpStr as o,toRegExp as a}from"util-ex";import c from"picomatch";import{isRegExpStr as u,toRegExp as f}from"util-ex";function h(t){return/[!*?{}[\]()]/.test(t)}function l(t,e,n=!1){if(Array.isArray(t)&&t.length){const r=[],s=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?s.push(t.slice(1)):r.push(t)}),!(s.length>0&&s.some(t=>l(t,e,n)))&&(0===r.length||r.some(t=>l(t,e,n)))}return t instanceof RegExp?t.test(e):"string"==typeof t&&(u(t)?f(t).test(e):h(t)?c(t,{dot:!0})(e):n?e.startsWith(t):e===t)}function p(t,e,n){let r;return e?.include&&(r=l(e.include,t)),e?.exclude&&l(e.exclude,t)&&(r=!1),void 0===r&&(r=n),r}var m=(t,e,n)=>{const r={};return Object.keys(t).filter(t=>p(t,e,n)).sort().forEach(e=>{const n=t[e];null!=n&&(r[e.toLowerCase()]=Array.isArray(n)?[...n].sort():[n])}),r};function y(t,e){const{sites:n,default:r}=e;if(!n)return r;for(const[e,r]of Object.entries(n))if(l(e,t,!0))return r;return r}async function w(t,e){const n=new URL(t.url),r=t.headers.get("cookie")||"",s=Object.fromEntries(r.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let c=null;const u=t.method.toUpperCase();if(["POST","PUT","PATCH"].includes(u))try{const n=t.headers.get("content-type")||"";if(n.includes("application/json")){const n=await t.clone().json();c=m(n,e.body,!0)}else if(e.body?.extract&&(n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded"))){const n=e.body?.maxLength||1024,r=(await t.clone().text()).slice(0,n),s=e.body.extract,u="string"==typeof s&&o(s)?a(s):s instanceof RegExp?s:null;if(u){const t=r.match(u);if(t)if(t.length>1){const n=t.slice(1);e.body?.sort&&n.sort(),c=n.join(":")}else c=t[0]}else c=i("sha256").update(r).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(c=i("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const f={m:u,h:n.host,p:n.pathname,q:m(Object.fromEntries(n.searchParams),e.query,!0),hd:m(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:m(s,e.cookies)};return null!==c&&(f.b=c),i("sha256").update(JSON.stringify(f)).digest("hex")}import{Readable as d}from"stream";import{pipeline as x}from"stream/promises";import b from"http-cache-semantics";function R(t,e){const n=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?d.toWeb(t):t}(t.body);return new Response(n,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function T(t,e){let n,r;const s=new Promise((e,s)=>{n=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},r=e=>{t.activeCacheWrites.delete(t.cacheKey),s(e)}});s.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,s);try{const e=await t.fetcher(t.request.clone()),s=new b({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),i=new Headers(e.headers);if(i.set("x-proxy-cache","MISS"),!s.storable()&&!t.config.forceCache)return n(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:i});const o={status:e.status,headers:Object.fromEntries(e.headers),policy:s.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),o),n(),new Response(null,{status:e.status,statusText:e.statusText,headers:i});const[a,c]=e.body.tee();return x(d.fromWeb(c),t.cache.setStream(t.cacheKey,o)).then(n).catch(r),new Response(a,{status:e.status,statusText:e.statusText,headers:i})}catch(n){if(r(n),e&&t.config.staleIfError)return R(e,"STALE_IF_ERROR");throw n}}async function O(t,e,n){if(!await async function(t,e){const n=t.method.toUpperCase();if(!(e.methods||["GET","HEAD"]).includes(n))return!1;if(e.cacheRules&&e.cacheRules.length>0){const r=new URL(t.url),s=r.searchParams;let i=null,o=!1;for(const c of e.cacheRules)if(await a(c,n,r,s,t))return!0;return!1;async function a(t,n,r,s,a){if(t.method&&t.method.toUpperCase()!==n)return!1;if(t.path&&!l(t.path,r.pathname,!0))return!1;if(t.query)for(const[e,n]of Object.entries(t.query)){const t=s.has(e),r=s.get(e)||"";if("boolean"==typeof n){if(n&&!t)return!1;if(!n&&t)return!1}else if(!l(n,r))return!1}if(t.bodyType||t.body){const n=a.headers.get("content-type")||"",r=n.includes("application/json")?"json":n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded")?"text":"binary";if(t.bodyType&&t.bodyType!==r)return!1;if(t.body){if("binary"===r)return!1;if(!o){try{const t=e.body?.maxLength||1024,n=await a.clone().text();i=n.slice(0,t)}catch(t){i=""}o=!0}if(!i||!l(t.body,i))return!1}}return!0}}return!0}(t,n.config))return e(t);const r=await async function(t,e,n){const r=n.generateKey||w,s=await r(t,n.config);return{...n,request:t,fetcher:e,cacheKey:s,activeCacheWrites:n.activeCacheWrites||new Map}}(t,e,n),s=await r.cache.get(r.cacheKey);if(s){const t=function(t,e){const n=b.fromObject(e.policy),r={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return n.satisfiesWithoutRevalidation(r)?"HIT":"STALE"}(r,s);if("HIT"===t)return R(s,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const n=T(t,e).catch(n=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,n),R(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(n)}(r,s),R(s,"STALE")}if(r.activeCacheWrites.has(r.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const n=await t.cache.get(t.cacheKey);return n?R(n,"HIT"):null}(r);if(t)return t}return T(r,s)}function E(t){return t||(t=new Map),async function(e,n,r){return O(e,n,{...r,activeCacheWrites:t})}}function S(t){const e=E(t.activeCacheWrites);return async function(n,r,s){return e(n,r,{...t,...s})}}export{s as SmartCache,S as createCachedFetch,E as createFetchWithCache,m as extractData,O as fetchWithCache,w as generateCacheKey,y as getSiteConfig,p as isAllowed,h as isGlob,l as isMatch}; |
@@ -9,6 +9,16 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [core/SmartCache.ts:22](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L22) | ||
| Defined in: [core/SmartCache.ts:39](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L39) | ||
| 智能混合缓存类 (Hybrid Cache) | ||
| 智能混合缓存类 (Hybrid Multi-tier Cache) | ||
| 该类实现了 L1 (内存) 和 L2 (磁盘) 的双层混合存储架构,旨在提供高性能且大容量的缓存能力。 | ||
| ### 核心特性: | ||
| - **双层架构**: L1 使用 LRU 内存缓存(基于 `secondary-cache` 的 LRUCache),L2 使用持久化磁盘缓存(基于 `cacache`)。 | ||
| - **大小感知存储**: 自动识别响应体大小。小于阈值的文件同时存于内存和磁盘;超过阈值的文件仅存于磁盘,但其元数据仍保留在内存中。 | ||
| - **元数据驻留 (Meta-Residency)**: 无论 Body 多大,Headers、Status、Policy 等信息始终优先从内存读取,确保缓存判定性能。 | ||
| - **流式支持**: 支持通过 `setStream` 和 `getStream` 直接操作大数据流,防止 OOM。 | ||
| - **一致性保障**: 在并发写入时自动清理内存,确保后续读取不会拿到被污染的旧数据。 | ||
| - **内存限制**: 通过 `maxTotalMemorySize` 控制 L1 缓存的总内存占用。 | ||
| ## Constructors | ||
@@ -20,3 +30,3 @@ | ||
| Defined in: [core/SmartCache.ts:27](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L27) | ||
| Defined in: [core/SmartCache.ts:44](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L44) | ||
@@ -39,3 +49,3 @@ #### Parameters | ||
| Defined in: [core/SmartCache.ts:122](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L122) | ||
| Defined in: [core/SmartCache.ts:193](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L193) | ||
@@ -52,3 +62,3 @@ #### Returns | ||
| Defined in: [core/SmartCache.ts:117](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L117) | ||
| Defined in: [core/SmartCache.ts:188](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L188) | ||
@@ -71,7 +81,12 @@ #### Parameters | ||
| Defined in: [core/SmartCache.ts:41](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L41) | ||
| Defined in: [core/SmartCache.ts:79](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L79) | ||
| 获取缓存条目 | ||
| 如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。 | ||
| 逻辑: | ||
| 1. 首先尝试从 L1 内存获取。 | ||
| 2. 如果内存中有 Body,直接返回(Buffer 类型)。 | ||
| 3. 如果内存中只有 Meta(大文件),则从 L2 磁盘创建并返回 ReadStream。 | ||
| 4. 如果内存完全未命中,从磁盘 L2 检索,并根据大小决定是否回填 L1。 | ||
| #### Parameters | ||
@@ -83,2 +98,4 @@ | ||
| 缓存指纹键 | ||
| #### Returns | ||
@@ -88,2 +105,4 @@ | ||
| 完整的缓存条目(带 Buffer 或 Stream 的 Body),未命中返回 null | ||
| *** | ||
@@ -95,4 +114,8 @@ | ||
| Defined in: [core/SmartCache.ts:107](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L107) | ||
| Defined in: [core/SmartCache.ts:159](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L159) | ||
| 获取磁盘读取流 | ||
| 允许直接从 L2 磁盘层以流的形式读取数据,适用于大文件代理。 | ||
| #### Parameters | ||
@@ -104,2 +127,4 @@ | ||
| 缓存指纹键 | ||
| #### Returns | ||
@@ -109,2 +134,4 @@ | ||
| Node.js 可读流 | ||
| *** | ||
@@ -116,6 +143,8 @@ | ||
| Defined in: [core/SmartCache.ts:85](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L85) | ||
| Defined in: [core/SmartCache.ts:129](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L129) | ||
| 写入缓存 | ||
| 写入缓存条目 (原子写入) | ||
| 适用于已知长度的小型数据块。该操作会同时写入磁盘并回填内存(如果大小未超标)。 | ||
| #### Parameters | ||
@@ -127,2 +156,4 @@ | ||
| 缓存指纹键 | ||
| ##### body | ||
@@ -132,2 +163,4 @@ | ||
| 响应体数据 Buffer | ||
| ##### metadata | ||
@@ -137,2 +170,4 @@ | ||
| 响应元数据(不含 size,由本方法自动计算) | ||
| #### Returns | ||
@@ -148,4 +183,11 @@ | ||
| Defined in: [core/SmartCache.ts:111](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L111) | ||
| Defined in: [core/SmartCache.ts:175](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L175) | ||
| 获取磁盘写入流 (流式缓存) | ||
| 该方法用于支持真正的流式代理。它会执行以下一致性操作: | ||
| 1. 立即清除 L1 内存中的对应键,防止读到旧数据。 | ||
| 2. 返回一个可写流,数据将直接流入磁盘。 | ||
| 3. **一致性修复**: 在流写入完成(finish)时再次清理内存,防止写入期间的并发读取将旧数据再次回填进内存。 | ||
| #### Parameters | ||
@@ -157,2 +199,4 @@ | ||
| 缓存指纹键 | ||
| ##### metadata | ||
@@ -162,4 +206,8 @@ | ||
| 响应元数据 | ||
| #### Returns | ||
| `WritableStream` | ||
| Node.js 可写流 |
@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [core/createCachedFetch.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/createCachedFetch.ts#L17) | ||
| Defined in: [core/createCachedFetch.ts:17](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/createCachedFetch.ts#L17) | ||
@@ -14,0 +14,0 @@ 缓存请求工厂函数 (针对终端用户的顶层高阶 API) |
@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [core/createFetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/createFetchWithCache.ts#L16) | ||
| Defined in: [core/createFetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/createFetchWithCache.ts#L16) | ||
@@ -14,0 +14,0 @@ 单一职责高阶函数:专门用于封装和隔离 activeCacheWrites 并发追踪器。 |
@@ -9,5 +9,5 @@ [**@isdk/proxy**](../README.md) | ||
| > **extractData**(`source`, `config?`): `Record`\<`string`, `string`[]\> | ||
| > **extractData**(`source`, `config?`, `defaultAllowed?`): `Record`\<`string`, `string`[]\> | ||
| Defined in: [utils/extractData.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/utils/extractData.ts#L17) | ||
| Defined in: [utils/extractData.ts:40](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/extractData.ts#L40) | ||
@@ -17,3 +17,3 @@ 从源对象中根据过滤配置提取数据并标准化。 | ||
| 此函数主要用于生成缓存指纹。它会: | ||
| 1. 根据 `config` (include/exclude) 过滤键。 | ||
| 1. 根据 `config` (include/exclude) 过滤键,调用 `isAllowed` 判断每个键是否允许。 | ||
| 2. 对键进行排序以保证指纹的一致性。 | ||
@@ -23,2 +23,7 @@ 3. 将所有键转换为小写。 | ||
| **关于 `defaultAllowed` 参数**: | ||
| - 只有当没有配置 `include` 和 `exclude` 时,`defaultAllowed` 才会生效。 | ||
| - 如果配置了 `include`(即使为空数组),`defaultAllowed` 也不会生效。 | ||
| - 详见 `isAllowed` 函数的优先级逻辑。 | ||
| ## Parameters | ||
@@ -36,4 +41,10 @@ | ||
| 过滤配置 (白名单或黑名单) | ||
| 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单) | ||
| ### defaultAllowed? | ||
| `boolean` | ||
| 当没有配置时的默认值(默认 `false`,即不提取任何键) | ||
| ## Returns | ||
@@ -43,2 +54,20 @@ | ||
| 标准化后的数据 Map,键为小写,值为字符串数组 | ||
| 标准化后的数据 Map,键为小写,值为排序后的字符串数组 | ||
| ## Example | ||
| ```typescript | ||
| const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' }; | ||
| // 默认不提取任何键 | ||
| extractData(headers); // {} | ||
| // 提取所有键 | ||
| extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] } | ||
| // 白名单 | ||
| extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] } | ||
| // 黑名单(需要 include 或 defaultAllowed) | ||
| extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] } | ||
| ``` |
@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [core/fetchWithCache.ts:244](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L244) | ||
| Defined in: [core/fetchWithCache.ts:370](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L370) | ||
@@ -17,2 +17,4 @@ 核心协调函数 (Fetcher Orchestrator) | ||
| 实现了基于流的混合缓存代理核心逻辑,主要机制包括: | ||
| - **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。 | ||
| - **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。 | ||
| - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM。 | ||
@@ -31,2 +33,4 @@ - **SWR (Stale-While-Revalidate)**:后台静默更新机制。 | ||
| 原始 Web 标准 Request 对象 | ||
| ### fetcher | ||
@@ -36,2 +40,4 @@ | ||
| 实际执行网络请求的函数 | ||
| ### options | ||
@@ -41,4 +47,8 @@ | ||
| 缓存配置选项 | ||
| ## Returns | ||
| `Promise`\<`Response`\> | ||
| 带有缓存标识头和流式 Body 的 Response 对象 |
@@ -9,8 +9,23 @@ [**@isdk/proxy**](../README.md) | ||
| > **generateCacheKey**(`req`, `config`): `string` | ||
| > **generateCacheKey**(`req`, `config`): `Promise`\<`string`\> | ||
| Defined in: [core/generateCacheKey.ts:8](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/generateCacheKey.ts#L8) | ||
| Defined in: [core/generateCacheKey.ts:36](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/generateCacheKey.ts#L36) | ||
| 根据 Request 和配置生成唯一的缓存键 | ||
| 根据 Request 对象和站点配置生成唯一的缓存指纹 (异步) | ||
| 该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。 | ||
| 它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。 | ||
| ### 生成指纹包含的要素: | ||
| 1. **Method**: 请求方法(统一转为大写)。 | ||
| 2. **Host & Path**: 请求的域名和路径。 | ||
| 3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。 | ||
| 4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。 | ||
| 5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。 | ||
| 6. **Request Body**: | ||
| - 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。 | ||
| - **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。 | ||
| - **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。 | ||
| - **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。 | ||
| ## Parameters | ||
@@ -22,2 +37,4 @@ | ||
| 原始 Web 标准 Request 对象。 | ||
| ### config | ||
@@ -27,4 +44,17 @@ | ||
| 站点级缓存配置,决定了哪些字段参与指纹计算。 | ||
| ## Returns | ||
| `string` | ||
| `Promise`\<`string`\> | ||
| 返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。 | ||
| ## Example | ||
| ```typescript | ||
| const cacheKey = await generateCacheKey(request, { | ||
| query: { exclude: ['timestamp'] }, | ||
| body: { include: ['id', 'action'] } | ||
| }); | ||
| ``` |
@@ -9,13 +9,16 @@ [**@isdk/proxy**](../README.md) | ||
| > **isAllowed**(`key`, `config?`): `boolean` | ||
| > **isAllowed**(`key`, `config?`, `defaultAllowed?`): `boolean` | ||
| Defined in: [utils/isAllowed.ts:15](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/utils/isAllowed.ts#L15) | ||
| Defined in: [utils/isAllowed.ts:37](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/isAllowed.ts#L37) | ||
| 判断给定的键是否允许参与缓存指纹计算。 | ||
| 优先级逻辑: | ||
| 1. 如果配置了 `include` (白名单),则只有存在于 `include` 中的键才会被允许。 | ||
| 2. 否则,如果配置了 `exclude` (黑名单),则存在于 `exclude` 中的键将被拒绝。 | ||
| 3. 如果都没有配置,默认允许所有键。 | ||
| **优先级逻辑**: | ||
| 1. `exclude` 命中 → 返回 `false`(优先级最高,会覆盖前面的结果) | ||
| 2. `include` 存在且命中 → 返回 `true` | ||
| 3. `include` 存在但不命中 → 返回 `false` | ||
| 4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`) | ||
| **注意**:`include` 和 `exclude` 可以同时配置,此时 `exclude` 优先级更高。 | ||
| ## Parameters | ||
@@ -33,4 +36,10 @@ | ||
| 过滤配置 | ||
| 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单) | ||
| ### defaultAllowed? | ||
| `boolean` | ||
| 当没有配置或配置未命中时的默认值(可选) | ||
| ## Returns | ||
@@ -40,2 +49,20 @@ | ||
| 是否允许 | ||
| 是否允许。返回 `boolean` 或 `undefined`(当没有配置且未传 defaultAllowed 时) | ||
| ## Example | ||
| ```typescript | ||
| // 无配置 | ||
| isAllowed('key'); // undefined | ||
| // 白名单 | ||
| isAllowed('id', { include: ['id', 'name'] }); // true | ||
| isAllowed('email', { include: ['id', 'name'] }); // false | ||
| // 黑名单 | ||
| isAllowed('password', { exclude: ['password'] }); // false | ||
| isAllowed('name', { exclude: ['password'] }); // undefined | ||
| // 设置默认值 | ||
| isAllowed('name', { exclude: ['password'] }, true); // true | ||
| ``` |
+5
-0
@@ -13,4 +13,6 @@ [**@isdk/proxy**](README.md) | ||
| - [BodyFilterConfig](interfaces/BodyFilterConfig.md) | ||
| - [CacheEntry](interfaces/CacheEntry.md) | ||
| - [CacheMetadata](interfaces/CacheMetadata.md) | ||
| - [CacheRule](interfaces/CacheRule.md) | ||
| - [FetchWithCacheContext](interfaces/FetchWithCacheContext.md) | ||
@@ -30,2 +32,5 @@ - [FetchWithCacheOptions](interfaces/FetchWithCacheOptions.md) | ||
| - [generateCacheKey](functions/generateCacheKey.md) | ||
| - [getSiteConfig](functions/getSiteConfig.md) | ||
| - [isAllowed](functions/isAllowed.md) | ||
| - [isGlob](functions/isGlob.md) | ||
| - [isMatch](functions/isMatch.md) |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [types.ts:55](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L55) | ||
| Defined in: [types.ts:137](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L137) | ||
@@ -24,3 +24,3 @@ 完整的缓存条目 | ||
| Defined in: [types.ts:57](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L57) | ||
| Defined in: [types.ts:139](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L139) | ||
@@ -35,3 +35,3 @@ 响应体数据:小文件为 Buffer,大文件为可读流 | ||
| Defined in: [types.ts:39](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L39) | ||
| Defined in: [types.ts:121](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L121) | ||
@@ -50,3 +50,3 @@ 响应头对象 | ||
| Defined in: [types.ts:45](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L45) | ||
| Defined in: [types.ts:127](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L127) | ||
@@ -65,3 +65,3 @@ 原始请求方法 | ||
| Defined in: [types.ts:41](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L41) | ||
| Defined in: [types.ts:123](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L123) | ||
@@ -80,3 +80,3 @@ http-cache-semantics 策略对象,包含 TTL 和缓存指令 | ||
| Defined in: [types.ts:49](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L49) | ||
| Defined in: [types.ts:131](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L131) | ||
@@ -95,3 +95,3 @@ Body 的字节长度,用于精确区分“空响应”与“未入内存的大响应” | ||
| Defined in: [types.ts:37](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L37) | ||
| Defined in: [types.ts:119](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L119) | ||
@@ -110,3 +110,3 @@ HTTP 状态码 | ||
| Defined in: [types.ts:47](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L47) | ||
| Defined in: [types.ts:129](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L129) | ||
@@ -125,3 +125,3 @@ 缓存写入时的时间戳 | ||
| Defined in: [types.ts:43](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L43) | ||
| Defined in: [types.ts:125](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L125) | ||
@@ -128,0 +128,0 @@ 原始请求 URL |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [types.ts:35](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L35) | ||
| Defined in: [types.ts:117](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L117) | ||
@@ -27,3 +27,3 @@ 缓存元数据 | ||
| Defined in: [types.ts:39](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L39) | ||
| Defined in: [types.ts:121](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L121) | ||
@@ -38,3 +38,3 @@ 响应头对象 | ||
| Defined in: [types.ts:45](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L45) | ||
| Defined in: [types.ts:127](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L127) | ||
@@ -49,3 +49,3 @@ 原始请求方法 | ||
| Defined in: [types.ts:41](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L41) | ||
| Defined in: [types.ts:123](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L123) | ||
@@ -60,3 +60,3 @@ http-cache-semantics 策略对象,包含 TTL 和缓存指令 | ||
| Defined in: [types.ts:49](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L49) | ||
| Defined in: [types.ts:131](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L131) | ||
@@ -71,3 +71,3 @@ Body 的字节长度,用于精确区分“空响应”与“未入内存的大响应” | ||
| Defined in: [types.ts:37](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L37) | ||
| Defined in: [types.ts:119](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L119) | ||
@@ -82,3 +82,3 @@ HTTP 状态码 | ||
| Defined in: [types.ts:47](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L47) | ||
| Defined in: [types.ts:129](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L129) | ||
@@ -93,4 +93,4 @@ 缓存写入时的时间戳 | ||
| Defined in: [types.ts:43](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L43) | ||
| Defined in: [types.ts:125](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L125) | ||
| 原始请求 URL |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [core/fetchWithCache.ts:31](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L31) | ||
| Defined in: [core/fetchWithCache.ts:32](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L32) | ||
@@ -24,3 +24,3 @@ 内部流水线上下文,合并了入参和计算出的关键状态 | ||
| Defined in: [core/fetchWithCache.ts:35](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L35) | ||
| Defined in: [core/fetchWithCache.ts:36](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L36) | ||
@@ -41,3 +41,3 @@ 并发写入任务追踪器 | ||
| Defined in: [core/fetchWithCache.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L17) | ||
| Defined in: [core/fetchWithCache.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L18) | ||
@@ -56,3 +56,3 @@ 是否启用后台异步更新 (SWR) | ||
| Defined in: [core/fetchWithCache.ts:13](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L13) | ||
| Defined in: [core/fetchWithCache.ts:14](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L14) | ||
@@ -71,3 +71,3 @@ 混合缓存实例 | ||
| Defined in: [core/fetchWithCache.ts:34](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L34) | ||
| Defined in: [core/fetchWithCache.ts:35](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L35) | ||
@@ -80,3 +80,3 @@ *** | ||
| Defined in: [core/fetchWithCache.ts:15](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L15) | ||
| Defined in: [core/fetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L16) | ||
@@ -95,3 +95,3 @@ 站点级缓存配置 | ||
| Defined in: [core/fetchWithCache.ts:33](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L33) | ||
| Defined in: [core/fetchWithCache.ts:34](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L34) | ||
@@ -112,10 +112,25 @@ #### Parameters | ||
| > `optional` **generateKey**: (`req`, `config`) => `string` | ||
| > `optional` **generateKey**: (`req`, `config`) => `Promise`\<`string`\> | ||
| Defined in: [core/fetchWithCache.ts:21](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L21) | ||
| Defined in: [core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L22) | ||
| 自定义缓存键生成函数 | ||
| 根据 Request 和配置生成唯一的缓存键 | ||
| 根据 Request 对象和站点配置生成唯一的缓存指纹 (异步) | ||
| 该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。 | ||
| 它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。 | ||
| ### 生成指纹包含的要素: | ||
| 1. **Method**: 请求方法(统一转为大写)。 | ||
| 2. **Host & Path**: 请求的域名和路径。 | ||
| 3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。 | ||
| 4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。 | ||
| 5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。 | ||
| 6. **Request Body**: | ||
| - 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。 | ||
| - **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。 | ||
| - **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。 | ||
| - **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。 | ||
| #### Parameters | ||
@@ -127,2 +142,4 @@ | ||
| 原始 Web 标准 Request 对象。 | ||
| ##### config | ||
@@ -132,6 +149,19 @@ | ||
| 站点级缓存配置,决定了哪些字段参与指纹计算。 | ||
| #### Returns | ||
| `string` | ||
| `Promise`\<`string`\> | ||
| 返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。 | ||
| #### Example | ||
| ```typescript | ||
| const cacheKey = await generateCacheKey(request, { | ||
| query: { exclude: ['timestamp'] }, | ||
| body: { include: ['id', 'action'] } | ||
| }); | ||
| ``` | ||
| #### Inherited from | ||
@@ -147,3 +177,3 @@ | ||
| Defined in: [core/fetchWithCache.ts:19](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L19) | ||
| Defined in: [core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L20) | ||
@@ -172,2 +202,2 @@ 后台更新 Promise 触发时的回调 | ||
| Defined in: [core/fetchWithCache.ts:32](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L32) | ||
| Defined in: [core/fetchWithCache.ts:33](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L33) |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [core/fetchWithCache.ts:11](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L11) | ||
| Defined in: [core/fetchWithCache.ts:12](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L12) | ||
@@ -24,3 +24,3 @@ fetchWithCache 选项 | ||
| Defined in: [core/fetchWithCache.ts:27](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L27) | ||
| Defined in: [core/fetchWithCache.ts:28](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L28) | ||
@@ -37,3 +37,3 @@ 并发写入任务追踪器 | ||
| Defined in: [core/fetchWithCache.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L17) | ||
| Defined in: [core/fetchWithCache.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L18) | ||
@@ -48,3 +48,3 @@ 是否启用后台异步更新 (SWR) | ||
| Defined in: [core/fetchWithCache.ts:13](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L13) | ||
| Defined in: [core/fetchWithCache.ts:14](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L14) | ||
@@ -59,3 +59,3 @@ 混合缓存实例 | ||
| Defined in: [core/fetchWithCache.ts:15](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L15) | ||
| Defined in: [core/fetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L16) | ||
@@ -68,10 +68,25 @@ 站点级缓存配置 | ||
| > `optional` **generateKey**: (`req`, `config`) => `string` | ||
| > `optional` **generateKey**: (`req`, `config`) => `Promise`\<`string`\> | ||
| Defined in: [core/fetchWithCache.ts:21](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L21) | ||
| Defined in: [core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L22) | ||
| 自定义缓存键生成函数 | ||
| 根据 Request 和配置生成唯一的缓存键 | ||
| 根据 Request 对象和站点配置生成唯一的缓存指纹 (异步) | ||
| 该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。 | ||
| 它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。 | ||
| ### 生成指纹包含的要素: | ||
| 1. **Method**: 请求方法(统一转为大写)。 | ||
| 2. **Host & Path**: 请求的域名和路径。 | ||
| 3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。 | ||
| 4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。 | ||
| 5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。 | ||
| 6. **Request Body**: | ||
| - 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。 | ||
| - **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。 | ||
| - **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。 | ||
| - **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。 | ||
| #### Parameters | ||
@@ -83,2 +98,4 @@ | ||
| 原始 Web 标准 Request 对象。 | ||
| ##### config | ||
@@ -88,6 +105,19 @@ | ||
| 站点级缓存配置,决定了哪些字段参与指纹计算。 | ||
| #### Returns | ||
| `string` | ||
| `Promise`\<`string`\> | ||
| 返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。 | ||
| #### Example | ||
| ```typescript | ||
| const cacheKey = await generateCacheKey(request, { | ||
| query: { exclude: ['timestamp'] }, | ||
| body: { include: ['id', 'action'] } | ||
| }); | ||
| ``` | ||
| *** | ||
@@ -99,3 +129,3 @@ | ||
| Defined in: [core/fetchWithCache.ts:19](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L19) | ||
| Defined in: [core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L20) | ||
@@ -102,0 +132,0 @@ 后台更新 Promise 触发时的回调 |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [types.ts:6](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L6) | ||
| Defined in: [types.ts:6](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L6) | ||
@@ -16,2 +16,6 @@ 缓存键过滤配置 | ||
| ## Extended by | ||
| - [`BodyFilterConfig`](BodyFilterConfig.md) | ||
| ## Properties | ||
@@ -21,7 +25,7 @@ | ||
| > `optional` **exclude**: `string`[] | ||
| > `optional` **exclude**: (`string` \| `RegExp`)[] | ||
| Defined in: [types.ts:10](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L10) | ||
| Defined in: [types.ts:10](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L10) | ||
| 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段 | ||
| 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。 | ||
@@ -32,6 +36,6 @@ *** | ||
| > `optional` **include**: `string`[] | ||
| > `optional` **include**: (`string` \| `RegExp`)[] | ||
| Defined in: [types.ts:8](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L8) | ||
| Defined in: [types.ts:8](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L8) | ||
| 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算 | ||
| 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。 |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [types.ts:63](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L63) | ||
| Defined in: [types.ts:145](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L145) | ||
@@ -20,3 +20,3 @@ 代理拦截器全局配置 | ||
| Defined in: [types.ts:65](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L65) | ||
| Defined in: [types.ts:147](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L147) | ||
@@ -31,3 +31,3 @@ 默认缓存配置,当请求的域名未在 sites 中匹配时使用 | ||
| Defined in: [types.ts:67](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L67) | ||
| Defined in: [types.ts:149](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L149) | ||
@@ -42,4 +42,4 @@ 针对特定域名的精细化缓存配置 | ||
| Defined in: [types.ts:69](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L69) | ||
| Defined in: [types.ts:151](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L151) | ||
| 磁盘缓存(cacache)的物理存储路径,可选,默认为系统临时目录 |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [types.ts:16](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L16) | ||
| Defined in: [types.ts:81](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L81) | ||
@@ -16,2 +16,25 @@ 站点级缓存配置 | ||
| ### body? | ||
| > `optional` **body**: [`BodyFilterConfig`](BodyFilterConfig.md) | ||
| Defined in: [types.ts:104](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L104) | ||
| 请求体过滤配置 (仅限 JSON 类型)。 | ||
| 当方法为 POST/PUT/PATCH 且为 JSON 格式时,用于从 Body 中提取特定字段参与指纹计算。 | ||
| *** | ||
| ### cacheRules? | ||
| > `optional` **cacheRules**: [`CacheRule`](CacheRule.md)[] | ||
| Defined in: [types.ts:93](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L93) | ||
| 精细化缓存规则列表。 | ||
| 如果配置了此项,请求必须匹配其中至少一条规则才会被允许进入缓存流程。 | ||
| 适用于只希望缓存特定 API 接口的场景。 | ||
| *** | ||
| ### cookies? | ||
@@ -21,5 +44,5 @@ | ||
| Defined in: [types.ts:22](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L22) | ||
| Defined in: [types.ts:99](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L99) | ||
| Cookie 过滤配置 | ||
| Cookie 过滤配置:决定哪些 Cookie 字段参与缓存指纹计算 | ||
@@ -32,5 +55,5 @@ *** | ||
| Defined in: [types.ts:26](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L26) | ||
| Defined in: [types.ts:108](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L108) | ||
| 是否强制缓存一切响应(无视 no-store 等不缓存指令),用于极端的离线可用容错场景 | ||
| 强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。 | ||
@@ -43,8 +66,20 @@ *** | ||
| Defined in: [types.ts:20](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L20) | ||
| Defined in: [types.ts:97](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L97) | ||
| 请求头过滤配置 | ||
| 请求头过滤配置:决定哪些 Header 参与缓存指纹计算 | ||
| *** | ||
| ### methods? | ||
| > `optional` **methods**: `string`[] | ||
| Defined in: [types.ts:87](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L87) | ||
| 允许缓存的 HTTP 方法列表。 | ||
| 默认值: ['GET', 'HEAD']。 | ||
| 若要缓存 POST/PUT,必须在此显式添加,并确保后端响应满足缓存条件(或开启 `forceCache`)。 | ||
| *** | ||
| ### query? | ||
@@ -54,5 +89,5 @@ | ||
| Defined in: [types.ts:18](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L18) | ||
| Defined in: [types.ts:95](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L95) | ||
| Query 参数过滤配置 | ||
| Query 参数过滤配置:决定哪些查询参数参与缓存指纹 (Cache Key) 的计算 | ||
@@ -65,4 +100,4 @@ *** | ||
| Defined in: [types.ts:24](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L24) | ||
| Defined in: [types.ts:106](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L106) | ||
| 当后端请求失败且存在旧缓存时,是否强制返回旧缓存(容错机制) | ||
| 容错机制:当后端请求失败(网络错误或 5xx)且存在旧缓存时,是否强制返回旧缓存 |
@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md) | ||
| Defined in: [core/SmartCache.ts:10](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L10) | ||
| Defined in: [core/SmartCache.ts:10](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L10) | ||
@@ -20,3 +20,3 @@ SmartCache 选项 | ||
| Defined in: [core/SmartCache.ts:14](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L14) | ||
| Defined in: [core/SmartCache.ts:14](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L14) | ||
@@ -27,10 +27,36 @@ 内存缓存阈值(字节)。响应体大小超过此值时,Body 将只存入磁盘,而 Meta 仍保留在内存。默认 1MB。 | ||
| ### maxTotalMemorySize? | ||
| > `optional` **maxTotalMemorySize**: `number` | ||
| Defined in: [core/SmartCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L16) | ||
| 内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。 | ||
| *** | ||
| ### memoryOptions? | ||
| > `optional` **memoryOptions**: `Partial`\<`KeyvCacheableMemoryOptions`\> | ||
| > `optional` **memoryOptions**: `object` | ||
| Defined in: [core/SmartCache.ts:16](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L16) | ||
| Defined in: [core/SmartCache.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L18) | ||
| 透传给 L1 (Memory) 的高级配置 | ||
| 透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options) | ||
| #### Index Signature | ||
| \[`key`: `string`\]: `any` | ||
| #### capacity? | ||
| > `optional` **capacity**: `number` | ||
| #### cleanInterval? | ||
| > `optional` **cleanInterval**: `number` | ||
| #### expires? | ||
| > `optional` **expires**: `number` | ||
| *** | ||
@@ -42,4 +68,4 @@ | ||
| Defined in: [core/SmartCache.ts:12](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L12) | ||
| Defined in: [core/SmartCache.ts:12](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L12) | ||
| 磁盘缓存的物理路径。如果不提供,将默认使用系统临时目录。 |
+191
-7
@@ -22,2 +22,4 @@ **@isdk/proxy** | ||
| - **🚀 Hybrid Multi-tier Cache**: Extreme speed with L1 (LRU Memory) and persistence with L2 (Content Addressable Disk via `cacache`). | ||
| - **📥 HTTP POST & Method Support**: Full support for caching POST, PUT, and other methods with intelligent request body fingerprinting. | ||
| - **🎯 Precision Filtering**: Fine-grained `cacheRules` to intercept specific paths or query parameters. | ||
| - **🌊 Streaming Native**: Fully stream-based internal pipeline natively prevents Out-Of-Memory (OOM) issues when proxying large files. | ||
@@ -41,2 +43,4 @@ - **🧠 Intelligent Meta-Residency**: Metadata (Headers, Status, Policy) stays in memory regardless of body size, ensuring nanosecond cache policy evaluations. | ||
| ### Basic Usage (GET) | ||
| ```typescript | ||
@@ -51,3 +55,3 @@ import { SmartCache, createCachedFetch } from '@isdk/proxy'; | ||
| // 2. Create a pre-configured cached fetcher (automatically tracks concurrent requests) | ||
| // 2. Create a pre-configured cached fetcher | ||
| const myFetch = createCachedFetch({ | ||
@@ -57,3 +61,2 @@ cache, | ||
| staleIfError: true, | ||
| forceCache: false // Set to true to cache everything (ignore no-store) for offline-first apps | ||
| }, | ||
@@ -63,10 +66,79 @@ backgroundUpdate: true // Enable SWR | ||
| // 3. Use it anywhere in your app! | ||
| const request = new Request('https://api.example.com/data'); | ||
| const response = await myFetch(request, (req) => fetch(req)); | ||
| // 3. Use it! | ||
| const response = await myFetch(new Request('https://api.example.com/data'), (req) => fetch(req)); | ||
| console.log(response.headers.get('x-proxy-cache')); | ||
| ``` | ||
| console.log(response.headers.get('x-proxy-cache')); // "MISS", "HIT", "STALE", or "STALE_IF_ERROR" | ||
| const data = await response.json(); | ||
| ### Advanced Usage: Caching POST Requests | ||
| You can cache POST/PUT requests by enabling methods and defining body filters to ignore dynamic fields (like timestamps) in the request body. | ||
| ```typescript | ||
| const myPostFetch = createCachedFetch({ | ||
| cache, | ||
| config: { | ||
| methods: ['GET', 'POST'], // Enable POST caching | ||
| body: { | ||
| exclude: ['timestamp', 'nonce'] // Ignore these fields when generating cache keys | ||
| }, | ||
| cacheRules: [ | ||
| { method: 'POST', path: '/api/v1/query' } // Only cache specific POST endpoints | ||
| ], | ||
| forceCache: true // Often needed for POST if backend doesn't send Cache-Control | ||
| } | ||
| }); | ||
| ``` | ||
| ## Configuration: `SiteCacheConfig` | ||
| | Field | Type | Description | | ||
| | :--- | :--- | :--- | | ||
| | `methods` | `string[]` | List of allowed HTTP methods. Default: `['GET', 'HEAD']`. | | ||
| | `cacheRules` | `CacheRule[]` | Fine-grained rules. If set, a request must match at least one rule to be cached. | | ||
| | `query` | `KeyFilterConfig` | Filters for URL search parameters (`include`/`exclude`). | | ||
| | `headers` | `KeyFilterConfig` | Filters for request headers. | | ||
| | `cookies` | `KeyFilterConfig` | Filters for cookies. | | ||
| | `body` | `KeyFilterConfig` | Filters for JSON request body fields. | | ||
| | `staleIfError`| `boolean` | Serve stale cache on network failure. | | ||
| | `forceCache` | `boolean` | Ignore `no-store` and force caching (useful for offline support). | | ||
| ### `CacheRule` Object | ||
| - `method`: HTTP method to match. | ||
| - `path`: URL pathname matching (supports **RegExp**, **Glob**, **Array**, or **prefix match**). | ||
| - `query`: Key-value pairs. Values can be `string` (exact/Glob match), `true` (must exist), `false` (must not exist), or `RegExp`. | ||
| - `body`: Body content matching (supports **RegExp**, **Glob**, or **Array**). | ||
| ### Pattern Matching | ||
| `@isdk/proxy` provides powerful pattern matching for all configurable fields: | ||
| | Pattern Type | Example | Description | | ||
| | :--- | :--- | :--- | | ||
| | **RegExp** | `/api/v[12]/.*/i` | JavaScript RegExp (in JSON, use string like `"/api/v[12]/.*/i"`) | | ||
| | **Glob** | `/**/*.json` | File path style wildcard matching | | ||
| | **Negation** | `['!/api/private/**', '/api/**']` | Exclude patterns (prefixed with `!`, checked first) | | ||
| | **Array** | `['/api/v1/*', '/api/v2/*']` | Multiple patterns (OR logic, negative takes precedence) | | ||
| | **Boolean** | `true` / `false` | For query params: must/must not exist | | ||
| **Example with advanced pattern matching:** | ||
| ```typescript | ||
| const myFetch = createCachedFetch({ | ||
| cache, | ||
| config: { | ||
| cacheRules: [ | ||
| { | ||
| path: ['/api/v1/items/*', '!/api/v1/items/private/*'], // v1 items, exclude private | ||
| query: { | ||
| format: '/^(json|xml)$/', // Regex for format param | ||
| 'page*': true // Glob: any param starting with 'page' must exist | ||
| }, | ||
| body: /\"action\"\s*:\s*\"query\"/ // Regex body match | ||
| } | ||
| ] | ||
| } | ||
| }); | ||
| ``` | ||
| ## Adapters | ||
@@ -76,2 +148,4 @@ | ||
| - **HTTP Caching Proxy Server (Node.js)**: See [@isdk/proxy-server](https://www.npmjs.com/package/@isdk/proxy-server) (separate package) for running a standalone HTTP forward proxy. | ||
| - **Crawlee Adapter**: See [@isdk/proxy-crawlee](https://www.npmjs.com/package/@isdk/proxy-crawlee) (separate package) for integrating with Crawlee web scraping lifecycle. | ||
| - **MSW Adapter**: See `@isdk/proxy-msw` (separate package) to use this caching engine as an MSW interceptor. | ||
@@ -129,5 +203,115 @@ - **Axios Adapter**: Easily implemented by converting Axios config to Web `Request`. | ||
| ### Utility Functions | ||
| Exported from `@isdk/proxy` for advanced usage: | ||
| #### `isMatch(pattern, value, usePrefix?)` | ||
| Universal pattern matching function. Supports RegExp, Glob, array patterns (with negation), and string prefix/exact matching. | ||
| - **`pattern`**: `string | RegExp | (string | RegExp)[]` | ||
| - **`value`**: The string to test against | ||
| - **`usePrefix`**: For plain strings, use prefix match instead of exact match (default: `false`) | ||
| - **Returns**: `boolean` | ||
| ```typescript | ||
| import { isMatch } from '@isdk/proxy'; | ||
| isMatch('/api/v[12]/.*', '/api/v1/users'); // RegExp | ||
| isMatch('/api/**/*.json', '/api/v1/data.json'); // Glob | ||
| isMatch(['!/private/**', '/api/**'], '/api/data'); // Negation | ||
| ``` | ||
| #### `isGlob(pattern)` | ||
| Check if a pattern is Glob syntax. | ||
| - **`pattern`**: `string` | ||
| - **Returns**: `boolean` | ||
| #### `getSiteConfig(urlString, proxyConfig)` | ||
| Get the site-specific cache configuration for a given URL. | ||
| - **`urlString`**: Full URL to match | ||
| - **`proxyConfig`**: `ProxyConfig` object with `sites` and `default` config | ||
| - **Returns**: `SiteCacheConfig` | ||
| ```typescript | ||
| import { getSiteConfig } from '@isdk/proxy'; | ||
| const config = getSiteConfig('https://api.example.com/data', { | ||
| default: { methods: ['GET'] }, | ||
| sites: { | ||
| 'api.example.com': { methods: ['GET', 'POST'], forceCache: true }, | ||
| '/internal/': { staleIfError: true } // prefix match | ||
| } | ||
| }); | ||
| ``` | ||
| #### `isAllowed(key, config, defaultAllowed?)` | ||
| Check if a key is allowed to participate in cache key fingerprinting. | ||
| - **`key`**: The key name to check | ||
| - **`config`**: `KeyFilterConfig` with `include` (whitelist) or `exclude` (blacklist) | ||
| - **`defaultAllowed`**: Optional. Default value when no config or no match | ||
| - **Returns**: `boolean | undefined` | ||
| **Priority Logic**: | ||
| 1. `exclude` hit → returns `false` (highest priority) | ||
| 2. `include` exists and hits → returns `true` | ||
| 3. `include` exists but no hit → returns `false` | ||
| 4. No config → uses `defaultAllowed` (returns `undefined` if not provided) | ||
| ```typescript | ||
| import { isAllowed } from '@isdk/proxy'; | ||
| // No config | ||
| isAllowed('key'); // undefined (falsy) | ||
| // Whitelist | ||
| isAllowed('id', { include: ['id', 'name'] }); // true | ||
| isAllowed('email', { include: ['id', 'name'] }); // false | ||
| // Blacklist | ||
| isAllowed('password', { exclude: ['password'] }); // false | ||
| isAllowed('name', { exclude: ['password'] }); // undefined (falsy) | ||
| // Need defaultAllowed to set default | ||
| isAllowed('name', { exclude: ['password'] }, true); // true | ||
| ``` | ||
| #### `extractData(source, config, defaultAllowed?)` | ||
| Extract and normalize data from a source object based on filter config. Used for generating cache fingerprints. | ||
| - **`source`**: Original data object (Query, Headers, Cookies, etc.) | ||
| - **`config`**: `KeyFilterConfig` object | ||
| - **`defaultAllowed`**: Optional. Whether to allow extraction when no config or no match (default `false`) | ||
| - **Returns**: `Record<string, string[]>` normalized data with lowercase keys and sorted array values | ||
| ```typescript | ||
| import { extractData } from '@isdk/proxy'; | ||
| const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' }; | ||
| // No extraction by default | ||
| extractData(headers); // {} | ||
| // Extract all keys | ||
| extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] } | ||
| // Whitelist | ||
| extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] } | ||
| // Blacklist | ||
| extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] } | ||
| ``` | ||
| ### Cache Status Headers | ||
| Every response processed by `@isdk/proxy` will include an `x-proxy-cache` header indicating its lifecycle: | ||
| - `HIT`: Served entirely from L1 or L2 cache. | ||
@@ -134,0 +318,0 @@ - `MISS`: Bypassed cache and fetched from the origin server. |
+4
-2
| { | ||
| "name": "@isdk/proxy", | ||
| "version": "0.1.1", | ||
| "version": "0.1.2", | ||
| "description": "A framework-agnostic, high-performance hybrid caching middleware with SWR, request collapsing, and stale-if-error support.", | ||
@@ -22,3 +22,2 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@cacheable/memory": "^2.0.8", | ||
| "@isdk/common-error": "^0.2.0", | ||
@@ -30,2 +29,4 @@ "cacache": "^20.0.4", | ||
| "nanoid": "^5.1.11", | ||
| "picomatch": "^4.0.4", | ||
| "secondary-cache": "^2.1.0", | ||
| "util-ex": "^2.5.2" | ||
@@ -45,2 +46,3 @@ }, | ||
| "@types/node": "^20.0.0", | ||
| "@types/picomatch": "^4.0.3", | ||
| "@typescript-eslint/eslint-plugin": "~8.41.0", | ||
@@ -47,0 +49,0 @@ "eslint": "^9.34.0", |
+191
-9
@@ -16,2 +16,4 @@ # @isdk/proxy | ||
| - **🚀 混合多级缓存**: L1 (LRU 内存) 提供极速响应,L2 (内容寻址磁盘 `cacache`) 提供持久化存储。 | ||
| - **📥 HTTP POST & 多方法支持**: 完整支持 POST、PUT 等非 GET 方法的缓存,内置智能请求体指纹计算机制。 | ||
| - **🎯 精细化规则拦截**: 支持通过 `cacheRules` 对特定路径或 Query 参数进行外科手术式的精确缓存控制。 | ||
| - **🌊 原生流式分发**: 内部完全基于 Stream 管道化构建,在代理大文件时天然防 OOM 内存溢出。 | ||
@@ -35,2 +37,4 @@ - **🧠 智能元数据驻留**: 无论文件多大,元数据 (Headers, Status, Policy) 始终驻留在内存中,确保纳秒级的缓存策略判定。 | ||
| ### 基础用法 (GET 请求) | ||
| ```typescript | ||
@@ -45,3 +49,3 @@ import { SmartCache, createCachedFetch } from '@isdk/proxy'; | ||
| // 2. 创建一个预配置的缓存 Fetcher (内部会自动防缓存击穿) | ||
| // 2. 创建一个预配置的缓存 Fetcher | ||
| const myFetch = createCachedFetch({ | ||
@@ -51,3 +55,2 @@ cache, | ||
| staleIfError: true, | ||
| forceCache: false // 设置为 true 可无视 no-store 强制缓存一切,适用于离线应用 | ||
| }, | ||
@@ -57,10 +60,79 @@ backgroundUpdate: true // 开启 SWR (过期后后台静默更新) | ||
| // 3. 在应用的任何地方愉快地使用它! | ||
| const request = new Request('https://api.example.com/data'); | ||
| const response = await myFetch(request, (req) => fetch(req)); // 传入任何返回 Promise<Response> 的获取函数 | ||
| // 3. 愉快地使用它! | ||
| const response = await myFetch(new Request('https://api.example.com/data'), (req) => fetch(req)); | ||
| console.log(response.headers.get('x-proxy-cache')); | ||
| ``` | ||
| console.log(response.headers.get('x-proxy-cache')); // 输出: "MISS", "HIT", "STALE" 或 "STALE_IF_ERROR" | ||
| const data = await response.json(); | ||
| ### 进阶用法:缓存 POST 请求 | ||
| 你可以通过配置 `methods` 开启 POST/PUT 缓存,并使用 `body` 过滤器排除请求体中的动态字段(如时间戳、随机数),从而确保缓存键的稳定性。 | ||
| ```typescript | ||
| const myPostFetch = createCachedFetch({ | ||
| cache, | ||
| config: { | ||
| methods: ['GET', 'POST'], // 允许缓存 POST | ||
| body: { | ||
| exclude: ['timestamp', 'nonce'] // 生成缓存键时忽略这些动态字段 | ||
| }, | ||
| cacheRules: [ | ||
| { method: 'POST', path: '/api/v1/query' } // 仅对特定的 POST 接口生效 | ||
| ], | ||
| forceCache: true // 对于 POST 请求,后端通常不发 Cache-Control,建议开启强制缓存 | ||
| } | ||
| }); | ||
| ``` | ||
| ## 配置详解:`SiteCacheConfig` | ||
| | 配置项 | 类型 | 说明 | | ||
| | :--- | :--- | :--- | | ||
| | `methods` | `string[]` | 允许缓存的 HTTP 方法列表。默认仅为 `['GET', 'HEAD']`。 | | ||
| | `cacheRules` | `CacheRule[]` | 精细化拦截规则。如果配置,请求必须匹配其中至少一条规则才会被缓存。 | | ||
| | `query` | `KeyFilterConfig` | URL 查询参数过滤(`include` 白名单 / `exclude` 黑名单)。 | | ||
| | `headers` | `KeyFilterConfig` | 请求头过滤。 | | ||
| | `cookies` | `KeyFilterConfig` | Cookie 字段过滤。 | | ||
| | `body` | `KeyFilterConfig` | **仅限 JSON** 的请求体字段过滤。 | | ||
| | `staleIfError`| `boolean` | 网络请求失败时,是否强制返回本地过期的旧缓存。 | | ||
| | `forceCache` | `boolean` | 是否无视源站指令强制执行缓存,常用于离线应用。 | | ||
| ### `CacheRule` 规则对象 | ||
| - `method`: 匹配的 HTTP 方法。 | ||
| - `path`: 路径匹配(支持**正则表达式**、**Glob 通配符**、**数组格式**或**前缀匹配**)。 | ||
| - `query`: 键值对匹配。值可以是 `string`(全等/Glob匹配)、`true`(参数必须存在)、`false`(参数必须不存在)、或 `RegExp`(正则匹配)。 | ||
| - `body`: Body 内容匹配(支持**正则表达式**、**Glob 通配符**或**数组格式**)。 | ||
| ### 模式匹配说明 | ||
| `@isdk/proxy` 为所有可配置字段提供强大的模式匹配能力: | ||
| | 模式类型 | 示例 | 说明 | | ||
| | :--- | :--- | :--- | | ||
| | **正则表达式** | `/api/v[12]/.*/i` | JavaScript 正则(JSON 中用字符串表示,如 `"/api/v[12]/.*/i"`) | | ||
| | **Glob 通配符** | `/**/*.json` | 文件路径风格通配符匹配 | | ||
| | **否定模式** | `['!/api/private/**', '/api/**']` | 排除匹配(以 `!` 开头) | | ||
| | **数组格式** | `['/api/v1/*', '/api/v2/*']` | 多模式组合(OR 逻辑,负向优先) | | ||
| | **布尔值** | `true` / `false` | 用于 query 参数:必须存在/不存在 | | ||
| **高级模式匹配示例:** | ||
| ```typescript | ||
| const myFetch = createCachedFetch({ | ||
| cache, | ||
| config: { | ||
| cacheRules: [ | ||
| { | ||
| path: ['/api/v1/items/*', '!/api/v1/items/private/*'], // v1 items,排除 private | ||
| query: { | ||
| format: '/^(json|xml)$/', // 正则匹配 format 参数 | ||
| 'page*': true // Glob:任何以 page 开头的参数必须存在 | ||
| }, | ||
| body: /\"action\"\s*:\s*\"query\"/ // 正则匹配 Body 内容 | ||
| } | ||
| ] | ||
| } | ||
| }); | ||
| ``` | ||
| ## 适配器 (Adapters) | ||
@@ -70,5 +142,6 @@ | ||
| - **MSW 适配器**: 参见 `@isdk/proxy-msw` (独立包),将此缓存引擎作为 MSW 拦截器使用。 | ||
| - **HTTP 代理服务器 (Node.js)**: 参见 [@isdk/proxy-server](https://www.npmjs.com/package/@isdk/proxy-server)(独立包),用于启动独立的 HTTP 缓存代理服务器。 | ||
| - **Crawlee 适配器**: 参见 [@isdk/proxy-crawlee](https://www.npmjs.com/package/@isdk/proxy-crawlee)(独立包),用于集成到 Crawlee 网页爬虫生命周期中。 | ||
| - **MSW 适配器**: 参见 `@isdk/proxy-msw`(独立包),将此缓存引擎作为 MSW 拦截器使用。 | ||
| - **Axios 适配器**: 可以通过将 Axios 配置转换为 Web 标准 `Request` 轻松实现。 | ||
| - **Crawlee 适配器**: 能够集成到爬虫生命周期中,减少重复抓取。 | ||
@@ -133,2 +206,111 @@ ## 架构设计详解 | ||
| ### 工具函数 | ||
| 导出以下工具函数供高级用法: | ||
| #### `isMatch(pattern, value, usePrefix?)` | ||
| 通用模式匹配函数。支持正则表达式、Glob、数组模式(含否定)和字符串前缀/精确匹配。 | ||
| - **`pattern`**: `string | RegExp | (string | RegExp)[]` | ||
| - **`value`**: 要测试的字符串 | ||
| - **`usePrefix`**: 对于普通字符串,是否使用前缀匹配而非精确匹配(默认:`false`) | ||
| - **返回值**: `boolean` | ||
| ```typescript | ||
| import { isMatch } from '@isdk/proxy'; | ||
| isMatch('/api/v[12]/.*', '/api/v1/users'); // 正则表达式 | ||
| isMatch('/api/**/*.json', '/api/v1/data.json'); // Glob 通配符 | ||
| isMatch(['!/private/**', '/api/**'], '/api/data'); // 否定模式 | ||
| ``` | ||
| #### `isGlob(pattern)` | ||
| 判断字符串是否为 Glob 语法。 | ||
| - **`pattern`**: `string` | ||
| - **返回值**: `boolean` | ||
| #### `getSiteConfig(urlString, proxyConfig)` | ||
| 根据 URL 获取对应的站点级缓存配置。 | ||
| - **`urlString`**: 完整的请求 URL | ||
| - **`proxyConfig`**: 包含 `sites` 和 `default` 配置的 `ProxyConfig` 对象 | ||
| - **返回值**: `SiteCacheConfig` | ||
| ```typescript | ||
| import { getSiteConfig } from '@isdk/proxy'; | ||
| const config = getSiteConfig('https://api.example.com/data', { | ||
| default: { methods: ['GET'] }, | ||
| sites: { | ||
| 'api.example.com': { methods: ['GET', 'POST'], forceCache: true }, | ||
| '/internal/': { staleIfError: true } // 前缀匹配 | ||
| } | ||
| }); | ||
| ``` | ||
| #### `isAllowed(key, config, defaultAllowed?)` | ||
| 判断指定的键是否允许参与缓存指纹计算。 | ||
| - **`key`**: 要检查的键名 | ||
| - **`config`**: `KeyFilterConfig` 对象,支持 `include`(白名单)或 `exclude`(黑名单) | ||
| - **`defaultAllowed`**: 可选参数。当没有配置或配置未命中时使用的默认值 | ||
| - **返回值**: `boolean | undefined` | ||
| **优先级逻辑**: | ||
| 1. `exclude` 命中 → 直接返回 `false`(优先级最高) | ||
| 2. `include` 存在且命中 → 返回 `true` | ||
| 3. `include` 存在但不命中 → 返回 `false` | ||
| 4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`) | ||
| ```typescript | ||
| import { isAllowed } from '@isdk/proxy'; | ||
| // 无配置 | ||
| isAllowed('key'); // undefined (falsy) | ||
| // 白名单 | ||
| isAllowed('id', { include: ['id', 'name'] }); // true | ||
| isAllowed('email', { include: ['id', 'name'] }); // false | ||
| // 黑名单 | ||
| isAllowed('password', { exclude: ['password'] }); // false | ||
| isAllowed('name', { exclude: ['password'] }); // undefined (falsy) | ||
| // 需要 defaultAllowed 来设置默认值 | ||
| isAllowed('name', { exclude: ['password'] }, true); // true | ||
| ``` | ||
| #### `extractData(source, config, defaultAllowed?)` | ||
| 从源对象中根据过滤配置提取数据并标准化。用于生成缓存指纹。 | ||
| - **`source`**: 原始数据对象(Query、Headers、Cookies 等) | ||
| - **`config`**: `KeyFilterConfig` 对象 | ||
| - **`defaultAllowed`**: 可选参数。当没有配置或配置未命中时,是否允许提取(默认 `false`) | ||
| - **返回值**: `Record<string, string[]>` 标准化后的数据,键为小写,值为排序后的数组 | ||
| ```typescript | ||
| import { extractData } from '@isdk/proxy'; | ||
| const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' }; | ||
| // 默认不提取任何键 | ||
| extractData(headers); // {} | ||
| // 提取所有键 | ||
| extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] } | ||
| // 白名单 | ||
| extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] } | ||
| // 黑名单 | ||
| extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] } | ||
| ``` | ||
| ### 缓存状态标头 (Cache Status Headers) | ||
@@ -135,0 +317,0 @@ |
+191
-7
@@ -18,2 +18,4 @@ # @isdk/proxy | ||
| - **🚀 Hybrid Multi-tier Cache**: Extreme speed with L1 (LRU Memory) and persistence with L2 (Content Addressable Disk via `cacache`). | ||
| - **📥 HTTP POST & Method Support**: Full support for caching POST, PUT, and other methods with intelligent request body fingerprinting. | ||
| - **🎯 Precision Filtering**: Fine-grained `cacheRules` to intercept specific paths or query parameters. | ||
| - **🌊 Streaming Native**: Fully stream-based internal pipeline natively prevents Out-Of-Memory (OOM) issues when proxying large files. | ||
@@ -37,2 +39,4 @@ - **🧠 Intelligent Meta-Residency**: Metadata (Headers, Status, Policy) stays in memory regardless of body size, ensuring nanosecond cache policy evaluations. | ||
| ### Basic Usage (GET) | ||
| ```typescript | ||
@@ -47,3 +51,3 @@ import { SmartCache, createCachedFetch } from '@isdk/proxy'; | ||
| // 2. Create a pre-configured cached fetcher (automatically tracks concurrent requests) | ||
| // 2. Create a pre-configured cached fetcher | ||
| const myFetch = createCachedFetch({ | ||
@@ -53,3 +57,2 @@ cache, | ||
| staleIfError: true, | ||
| forceCache: false // Set to true to cache everything (ignore no-store) for offline-first apps | ||
| }, | ||
@@ -59,10 +62,79 @@ backgroundUpdate: true // Enable SWR | ||
| // 3. Use it anywhere in your app! | ||
| const request = new Request('https://api.example.com/data'); | ||
| const response = await myFetch(request, (req) => fetch(req)); | ||
| // 3. Use it! | ||
| const response = await myFetch(new Request('https://api.example.com/data'), (req) => fetch(req)); | ||
| console.log(response.headers.get('x-proxy-cache')); | ||
| ``` | ||
| console.log(response.headers.get('x-proxy-cache')); // "MISS", "HIT", "STALE", or "STALE_IF_ERROR" | ||
| const data = await response.json(); | ||
| ### Advanced Usage: Caching POST Requests | ||
| You can cache POST/PUT requests by enabling methods and defining body filters to ignore dynamic fields (like timestamps) in the request body. | ||
| ```typescript | ||
| const myPostFetch = createCachedFetch({ | ||
| cache, | ||
| config: { | ||
| methods: ['GET', 'POST'], // Enable POST caching | ||
| body: { | ||
| exclude: ['timestamp', 'nonce'] // Ignore these fields when generating cache keys | ||
| }, | ||
| cacheRules: [ | ||
| { method: 'POST', path: '/api/v1/query' } // Only cache specific POST endpoints | ||
| ], | ||
| forceCache: true // Often needed for POST if backend doesn't send Cache-Control | ||
| } | ||
| }); | ||
| ``` | ||
| ## Configuration: `SiteCacheConfig` | ||
| | Field | Type | Description | | ||
| | :--- | :--- | :--- | | ||
| | `methods` | `string[]` | List of allowed HTTP methods. Default: `['GET', 'HEAD']`. | | ||
| | `cacheRules` | `CacheRule[]` | Fine-grained rules. If set, a request must match at least one rule to be cached. | | ||
| | `query` | `KeyFilterConfig` | Filters for URL search parameters (`include`/`exclude`). | | ||
| | `headers` | `KeyFilterConfig` | Filters for request headers. | | ||
| | `cookies` | `KeyFilterConfig` | Filters for cookies. | | ||
| | `body` | `KeyFilterConfig` | Filters for JSON request body fields. | | ||
| | `staleIfError`| `boolean` | Serve stale cache on network failure. | | ||
| | `forceCache` | `boolean` | Ignore `no-store` and force caching (useful for offline support). | | ||
| ### `CacheRule` Object | ||
| - `method`: HTTP method to match. | ||
| - `path`: URL pathname matching (supports **RegExp**, **Glob**, **Array**, or **prefix match**). | ||
| - `query`: Key-value pairs. Values can be `string` (exact/Glob match), `true` (must exist), `false` (must not exist), or `RegExp`. | ||
| - `body`: Body content matching (supports **RegExp**, **Glob**, or **Array**). | ||
| ### Pattern Matching | ||
| `@isdk/proxy` provides powerful pattern matching for all configurable fields: | ||
| | Pattern Type | Example | Description | | ||
| | :--- | :--- | :--- | | ||
| | **RegExp** | `/api/v[12]/.*/i` | JavaScript RegExp (in JSON, use string like `"/api/v[12]/.*/i"`) | | ||
| | **Glob** | `/**/*.json` | File path style wildcard matching | | ||
| | **Negation** | `['!/api/private/**', '/api/**']` | Exclude patterns (prefixed with `!`, checked first) | | ||
| | **Array** | `['/api/v1/*', '/api/v2/*']` | Multiple patterns (OR logic, negative takes precedence) | | ||
| | **Boolean** | `true` / `false` | For query params: must/must not exist | | ||
| **Example with advanced pattern matching:** | ||
| ```typescript | ||
| const myFetch = createCachedFetch({ | ||
| cache, | ||
| config: { | ||
| cacheRules: [ | ||
| { | ||
| path: ['/api/v1/items/*', '!/api/v1/items/private/*'], // v1 items, exclude private | ||
| query: { | ||
| format: '/^(json|xml)$/', // Regex for format param | ||
| 'page*': true // Glob: any param starting with 'page' must exist | ||
| }, | ||
| body: /\"action\"\s*:\s*\"query\"/ // Regex body match | ||
| } | ||
| ] | ||
| } | ||
| }); | ||
| ``` | ||
| ## Adapters | ||
@@ -72,2 +144,4 @@ | ||
| - **HTTP Caching Proxy Server (Node.js)**: See [@isdk/proxy-server](https://www.npmjs.com/package/@isdk/proxy-server) (separate package) for running a standalone HTTP forward proxy. | ||
| - **Crawlee Adapter**: See [@isdk/proxy-crawlee](https://www.npmjs.com/package/@isdk/proxy-crawlee) (separate package) for integrating with Crawlee web scraping lifecycle. | ||
| - **MSW Adapter**: See `@isdk/proxy-msw` (separate package) to use this caching engine as an MSW interceptor. | ||
@@ -125,5 +199,115 @@ - **Axios Adapter**: Easily implemented by converting Axios config to Web `Request`. | ||
| ### Utility Functions | ||
| Exported from `@isdk/proxy` for advanced usage: | ||
| #### `isMatch(pattern, value, usePrefix?)` | ||
| Universal pattern matching function. Supports RegExp, Glob, array patterns (with negation), and string prefix/exact matching. | ||
| - **`pattern`**: `string | RegExp | (string | RegExp)[]` | ||
| - **`value`**: The string to test against | ||
| - **`usePrefix`**: For plain strings, use prefix match instead of exact match (default: `false`) | ||
| - **Returns**: `boolean` | ||
| ```typescript | ||
| import { isMatch } from '@isdk/proxy'; | ||
| isMatch('/api/v[12]/.*', '/api/v1/users'); // RegExp | ||
| isMatch('/api/**/*.json', '/api/v1/data.json'); // Glob | ||
| isMatch(['!/private/**', '/api/**'], '/api/data'); // Negation | ||
| ``` | ||
| #### `isGlob(pattern)` | ||
| Check if a pattern is Glob syntax. | ||
| - **`pattern`**: `string` | ||
| - **Returns**: `boolean` | ||
| #### `getSiteConfig(urlString, proxyConfig)` | ||
| Get the site-specific cache configuration for a given URL. | ||
| - **`urlString`**: Full URL to match | ||
| - **`proxyConfig`**: `ProxyConfig` object with `sites` and `default` config | ||
| - **Returns**: `SiteCacheConfig` | ||
| ```typescript | ||
| import { getSiteConfig } from '@isdk/proxy'; | ||
| const config = getSiteConfig('https://api.example.com/data', { | ||
| default: { methods: ['GET'] }, | ||
| sites: { | ||
| 'api.example.com': { methods: ['GET', 'POST'], forceCache: true }, | ||
| '/internal/': { staleIfError: true } // prefix match | ||
| } | ||
| }); | ||
| ``` | ||
| #### `isAllowed(key, config, defaultAllowed?)` | ||
| Check if a key is allowed to participate in cache key fingerprinting. | ||
| - **`key`**: The key name to check | ||
| - **`config`**: `KeyFilterConfig` with `include` (whitelist) or `exclude` (blacklist) | ||
| - **`defaultAllowed`**: Optional. Default value when no config or no match | ||
| - **Returns**: `boolean | undefined` | ||
| **Priority Logic**: | ||
| 1. `exclude` hit → returns `false` (highest priority) | ||
| 2. `include` exists and hits → returns `true` | ||
| 3. `include` exists but no hit → returns `false` | ||
| 4. No config → uses `defaultAllowed` (returns `undefined` if not provided) | ||
| ```typescript | ||
| import { isAllowed } from '@isdk/proxy'; | ||
| // No config | ||
| isAllowed('key'); // undefined (falsy) | ||
| // Whitelist | ||
| isAllowed('id', { include: ['id', 'name'] }); // true | ||
| isAllowed('email', { include: ['id', 'name'] }); // false | ||
| // Blacklist | ||
| isAllowed('password', { exclude: ['password'] }); // false | ||
| isAllowed('name', { exclude: ['password'] }); // undefined (falsy) | ||
| // Need defaultAllowed to set default | ||
| isAllowed('name', { exclude: ['password'] }, true); // true | ||
| ``` | ||
| #### `extractData(source, config, defaultAllowed?)` | ||
| Extract and normalize data from a source object based on filter config. Used for generating cache fingerprints. | ||
| - **`source`**: Original data object (Query, Headers, Cookies, etc.) | ||
| - **`config`**: `KeyFilterConfig` object | ||
| - **`defaultAllowed`**: Optional. Whether to allow extraction when no config or no match (default `false`) | ||
| - **Returns**: `Record<string, string[]>` normalized data with lowercase keys and sorted array values | ||
| ```typescript | ||
| import { extractData } from '@isdk/proxy'; | ||
| const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' }; | ||
| // No extraction by default | ||
| extractData(headers); // {} | ||
| // Extract all keys | ||
| extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] } | ||
| // Whitelist | ||
| extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] } | ||
| // Blacklist | ||
| extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] } | ||
| ``` | ||
| ### Cache Status Headers | ||
| Every response processed by `@isdk/proxy` will include an `x-proxy-cache` header indicating its lifecycle: | ||
| - `HIT`: Served entirely from L1 or L2 cache. | ||
@@ -130,0 +314,0 @@ - `MISS`: Bypassed cache and fetched from the origin server. |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
149050
83.3%29
20.83%446
117.56%314
141.54%9
12.5%29
3.57%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed