@isdk/proxy 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.cn.md +59 -1
- package/README.md +59 -1
- package/dist/index.d.mts +113 -22
- package/dist/index.d.ts +113 -22
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/README.md +59 -1
- package/docs/classes/OfflineCacheMissError.md +426 -0
- package/docs/classes/SmartCache.md +30 -10
- package/docs/functions/createCachedFetch.md +1 -1
- package/docs/functions/createFetchWithCache.md +1 -1
- package/docs/functions/extractData.md +1 -1
- package/docs/functions/fetchWithCache.md +14 -15
- package/docs/functions/generateCacheKey.md +1 -1
- package/docs/functions/getSiteConfig.md +1 -1
- package/docs/functions/isAllowed.md +1 -1
- package/docs/functions/isCacheable.md +27 -0
- package/docs/functions/isGlob.md +1 -1
- package/docs/functions/isMatch.md +1 -1
- package/docs/functions/prefetch.md +33 -0
- package/docs/globals.md +10 -0
- package/docs/interfaces/BodyFilterConfig.md +6 -6
- package/docs/interfaces/CacheEntry.md +9 -9
- package/docs/interfaces/CacheMetadata.md +8 -8
- package/docs/interfaces/CacheRule.md +6 -6
- package/docs/interfaces/FetchWithCacheContext.md +11 -13
- package/docs/interfaces/FetchWithCacheOptions.md +7 -9
- package/docs/interfaces/KeyFilterConfig.md +3 -3
- package/docs/interfaces/PrefetchOptions.md +107 -0
- package/docs/interfaces/PrefetchRequest.md +31 -0
- package/docs/interfaces/PrefetchResult.md +47 -0
- package/docs/interfaces/ProxyConfig.md +4 -4
- package/docs/interfaces/SiteCacheConfig.md +19 -9
- package/docs/interfaces/SmartCacheOptions.md +5 -5
- package/docs/variables/OfflineCacheMissErrorCode.md +18 -0
- package/package.json +2 -2
package/README.cn.md
CHANGED
|
@@ -87,17 +87,31 @@ const myPostFetch = createCachedFetch({
|
|
|
87
87
|
| `query` | `KeyFilterConfig` | URL 查询参数过滤(`include` 白名单 / `exclude` 黑名单)。 |
|
|
88
88
|
| `headers` | `KeyFilterConfig` | 请求头过滤。 |
|
|
89
89
|
| `cookies` | `KeyFilterConfig` | Cookie 字段过滤。 |
|
|
90
|
-
| `body` | `KeyFilterConfig` |
|
|
90
|
+
| `body` | `KeyFilterConfig` | 请求体字段过滤。对于 JSON 类型支持字段级过滤;也支持通过 `extract` 正则提取关键数据。 |
|
|
91
91
|
| `staleIfError`| `boolean` | 网络请求失败时,是否强制返回本地过期的旧缓存。 |
|
|
92
92
|
| `forceCache` | `boolean` | 是否无视源站指令强制执行缓存,常用于离线应用。 |
|
|
93
|
+
| `offline` | `boolean` | 离线模式。开启后只读缓存,若无缓存则抛出 `OfflineCacheMissError`。 |
|
|
93
94
|
|
|
94
95
|
### `CacheRule` 规则对象
|
|
95
96
|
|
|
96
97
|
- `method`: 匹配的 HTTP 方法。
|
|
97
98
|
- `path`: 路径匹配(支持**正则表达式**、**Glob 通配符**、**数组格式**或**前缀匹配**)。
|
|
98
99
|
- `query`: 键值对匹配。值可以是 `string`(全等/Glob匹配)、`true`(参数必须存在)、`false`(参数必须不存在)、或 `RegExp`(正则匹配)。
|
|
100
|
+
- `bodyType`: 匹配 Body 类型,支持 `'json'`, `'text'`, `'binary'`。
|
|
99
101
|
- `body`: Body 内容匹配(支持**正则表达式**、**Glob 通配符**或**数组格式**)。
|
|
100
102
|
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### `fetchWithCache` 高级选项
|
|
106
|
+
|
|
107
|
+
除了 `SiteCacheConfig` 外,`fetchWithCache` 还支持以下控制选项:
|
|
108
|
+
|
|
109
|
+
| 选项 | 类型 | 说明 |
|
|
110
|
+
| :--- | :--- | :--- |
|
|
111
|
+
| `backgroundUpdate` | `boolean` | 是否启用后台异步更新 (SWR)。默认为 `true`。 |
|
|
112
|
+
| `onBackgroundUpdate`| `function` | 当触发后台更新时,接收该更新 Promise 的回调。可用作任务追踪。 |
|
|
113
|
+
| `generateKey` | `function` | 自定义缓存键生成函数。 |
|
|
114
|
+
|
|
101
115
|
### 模式匹配说明
|
|
102
116
|
|
|
103
117
|
`@isdk/proxy` 为所有可配置字段提供强大的模式匹配能力:
|
|
@@ -306,6 +320,50 @@ extractData(headers, { include: ['content-type'] }); // { 'content-type': ['appl
|
|
|
306
320
|
extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
|
|
307
321
|
```
|
|
308
322
|
|
|
323
|
+
### `prefetch(options)`
|
|
324
|
+
|
|
325
|
+
预缓存函数,提前将指定的 URL 列表内容存入缓存。
|
|
326
|
+
|
|
327
|
+
- **`urls`**: `PrefetchRequest[]`。每个对象包含 `url` 和可选的 `request` 配置。
|
|
328
|
+
- **`config`**: `ProxyConfig` 完整配置。
|
|
329
|
+
- **`cache`**: `SmartCache` 实例。
|
|
330
|
+
- **`concurrency`**: 并发数(默认 `3`)。
|
|
331
|
+
- **`onProgress`**: 进度回调 `(completed, total, url) => void`。
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { prefetch } from '@isdk/proxy';
|
|
335
|
+
|
|
336
|
+
const result = await prefetch({
|
|
337
|
+
urls: [
|
|
338
|
+
{ url: 'https://api.example.com/page1' },
|
|
339
|
+
{ url: 'https://api.example.com/api2', request: { method: 'POST', body: '...' } }
|
|
340
|
+
],
|
|
341
|
+
config,
|
|
342
|
+
cache,
|
|
343
|
+
onProgress: (c, t, url) => console.log(`Progress: ${c}/${t} - ${url}`)
|
|
344
|
+
});
|
|
345
|
+
console.log(`Succeeded: ${result.succeeded}, Failed: ${result.failed}`);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 错误处理:`OfflineCacheMissError`
|
|
349
|
+
|
|
350
|
+
在开启 `offline: true` 模式时,如果请求未命中缓存,将抛出此错误。
|
|
351
|
+
|
|
352
|
+
- **`name`**: `OfflineCacheMissError`
|
|
353
|
+
- **`code`**: `ERR_OFFLINE_CACHE_MISS` (可通过导入 `OfflineCacheMissErrorCode` 获得)
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { OfflineCacheMissError } from '@isdk/proxy';
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await myFetch(request);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
if (e instanceof OfflineCacheMissError) {
|
|
362
|
+
// 处理离线未命中
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
309
367
|
### 缓存状态标头 (Cache Status Headers)
|
|
310
368
|
|
|
311
369
|
由 `@isdk/proxy` 处理并返回的所有 `Response`,其 Headers 中都会注入 `x-proxy-cache` 字段以便观测生命周期,可能的值有:
|
package/README.md
CHANGED
|
@@ -89,17 +89,31 @@ const myPostFetch = createCachedFetch({
|
|
|
89
89
|
| `query` | `KeyFilterConfig` | Filters for URL search parameters (`include`/`exclude`). |
|
|
90
90
|
| `headers` | `KeyFilterConfig` | Filters for request headers. |
|
|
91
91
|
| `cookies` | `KeyFilterConfig` | Filters for cookies. |
|
|
92
|
-
| `body` | `KeyFilterConfig` | Filters for
|
|
92
|
+
| `body` | `KeyFilterConfig` | Filters for request body fields. For JSON, supports field-level filtering; also supports extracting key data via `extract` regex. |
|
|
93
93
|
| `staleIfError`| `boolean` | Serve stale cache on network failure. |
|
|
94
94
|
| `forceCache` | `boolean` | Ignore `no-store` and force caching (useful for offline support). |
|
|
95
|
+
| `offline` | `boolean` | Offline mode. Only reads from cache; throws `OfflineCacheMissError` if no cache exists. |
|
|
95
96
|
|
|
96
97
|
### `CacheRule` Object
|
|
97
98
|
|
|
98
99
|
- `method`: HTTP method to match.
|
|
99
100
|
- `path`: URL pathname matching (supports **RegExp**, **Glob**, **Array**, or **prefix match**).
|
|
100
101
|
- `query`: Key-value pairs. Values can be `string` (exact/Glob match), `true` (must exist), `false` (must not exist), or `RegExp`.
|
|
102
|
+
- `bodyType`: Match body type. Supports `'json'`, `'text'`, `'binary'`.
|
|
101
103
|
- `body`: Body content matching (supports **RegExp**, **Glob**, or **Array**).
|
|
102
104
|
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `fetchWithCache` Advanced Options
|
|
108
|
+
|
|
109
|
+
In addition to `SiteCacheConfig`, `fetchWithCache` supports the following control options:
|
|
110
|
+
|
|
111
|
+
| Option | Type | Description |
|
|
112
|
+
| :--- | :--- | :--- |
|
|
113
|
+
| `backgroundUpdate` | `boolean` | Whether to enable background async update (SWR). Default is `true`. |
|
|
114
|
+
| `onBackgroundUpdate`| `function` | Callback that receives the update Promise when a background update is triggered. Useful for task tracking. |
|
|
115
|
+
| `generateKey` | `function` | Custom cache key generation function. |
|
|
116
|
+
|
|
103
117
|
### Pattern Matching
|
|
104
118
|
|
|
105
119
|
`@isdk/proxy` provides powerful pattern matching for all configurable fields:
|
|
@@ -299,6 +313,50 @@ extractData(headers, { include: ['content-type'] }); // { 'content-type': ['appl
|
|
|
299
313
|
extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
|
|
300
314
|
```
|
|
301
315
|
|
|
316
|
+
### `prefetch(options)`
|
|
317
|
+
|
|
318
|
+
Pre-cache function that fetches and stores a list of URLs into cache ahead of time.
|
|
319
|
+
|
|
320
|
+
- **`urls`**: `PrefetchRequest[]`. Each object contains `url` and optional `request` config.
|
|
321
|
+
- **`config`**: `ProxyConfig` full configuration.
|
|
322
|
+
- **`cache`**: `SmartCache` instance.
|
|
323
|
+
- **`concurrency`**: Concurrency limit (default `3`).
|
|
324
|
+
- **`onProgress`**: Progress callback `(completed, total, url) => void`.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { prefetch } from '@isdk/proxy';
|
|
328
|
+
|
|
329
|
+
const result = await prefetch({
|
|
330
|
+
urls: [
|
|
331
|
+
{ url: 'https://api.example.com/page1' },
|
|
332
|
+
{ url: 'https://api.example.com/api2', request: { method: 'POST', body: '...' } }
|
|
333
|
+
],
|
|
334
|
+
config,
|
|
335
|
+
cache,
|
|
336
|
+
onProgress: (c, t, url) => console.log(`Progress: ${c}/${t} - ${url}`)
|
|
337
|
+
});
|
|
338
|
+
console.log(`Succeeded: ${result.succeeded}, Failed: ${result.failed}`);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Error Handling: `OfflineCacheMissError`
|
|
342
|
+
|
|
343
|
+
When `offline: true` mode is enabled and a request does not hit the cache, this error is thrown.
|
|
344
|
+
|
|
345
|
+
- **`name`**: `OfflineCacheMissError`
|
|
346
|
+
- **`code`**: `ERR_OFFLINE_CACHE_MISS` (can be imported as `OfflineCacheMissErrorCode`)
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import { OfflineCacheMissError } from '@isdk/proxy';
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await myFetch(request);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
if (e instanceof OfflineCacheMissError) {
|
|
355
|
+
// Handle offline cache miss
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
302
360
|
### Cache Status Headers
|
|
303
361
|
|
|
304
362
|
Every response processed by `@isdk/proxy` will include an `x-proxy-cache` header indicating its lifecycle:
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
import { CommonError, ErrorCode } from '@isdk/common-error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 错误类型定义
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Offline 缓存未命中错误代码
|
|
9
|
+
*
|
|
10
|
+
* 当处于 offline 模式且请求的 URL 没有对应缓存时抛出。
|
|
11
|
+
* 这帮助调用者区分:
|
|
12
|
+
* - 网络请求失败(其他错误类型)
|
|
13
|
+
* - offline 模式下缓存不存在(本错误)
|
|
14
|
+
*/
|
|
15
|
+
declare const OfflineCacheMissErrorCode = ErrorCode.OfflineCacheMiss;
|
|
16
|
+
/**
|
|
17
|
+
* Offline 缓存未命中错误
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* throw new OfflineCacheMissError('http://example.com/data')
|
|
21
|
+
*
|
|
22
|
+
* @extends CommonError
|
|
23
|
+
*/
|
|
24
|
+
declare class OfflineCacheMissError extends CommonError {
|
|
25
|
+
static code: ErrorCode;
|
|
26
|
+
constructor(url: string | number, name?: string | Record<string, any>);
|
|
27
|
+
}
|
|
28
|
+
|
|
1
29
|
/**
|
|
2
30
|
* 缓存键过滤配置
|
|
3
31
|
*
|
|
@@ -103,6 +131,8 @@ interface SiteCacheConfig {
|
|
|
103
131
|
staleIfError?: boolean;
|
|
104
132
|
/** 强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。 */
|
|
105
133
|
forceCache?: boolean;
|
|
134
|
+
/** 严格离线模式:不发起任何网络请求,只读缓存。缓存未命中时抛出 OfflineCacheMissError */
|
|
135
|
+
offline?: boolean;
|
|
106
136
|
}
|
|
107
137
|
/**
|
|
108
138
|
* 缓存元数据
|
|
@@ -230,8 +260,17 @@ declare class SmartCache {
|
|
|
230
260
|
* @returns Node.js 可写流
|
|
231
261
|
*/
|
|
232
262
|
setStream(key: string, metadata: Omit<CacheMetadata, 'size'>): NodeJS.WritableStream;
|
|
233
|
-
|
|
234
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Deletes the cache entry for the specified key.
|
|
265
|
+
* @param key - The cache key to delete
|
|
266
|
+
* @param [clearPersistent=true] - Whether to also delete the entry from persistent (disk) storage. Defaults to `true`.
|
|
267
|
+
*/
|
|
268
|
+
delete(key: string, clearPersistent?: boolean): Promise<void>;
|
|
269
|
+
/**
|
|
270
|
+
* Clears the cache. By default, both the in-memory cache and the persistent disk cache are cleared.
|
|
271
|
+
* @param [clearPersistent=true] - Whether to clear the persistent (disk) cache. Defaults to `true` for backward compatibility.
|
|
272
|
+
*/
|
|
273
|
+
clear(clearPersistent?: boolean): Promise<void>;
|
|
235
274
|
}
|
|
236
275
|
|
|
237
276
|
/**
|
|
@@ -282,12 +321,10 @@ interface FetchWithCacheOptions {
|
|
|
282
321
|
generateKey?: typeof generateCacheKey;
|
|
283
322
|
/**
|
|
284
323
|
* 并发写入任务追踪器
|
|
285
|
-
* 传入一个外部维护的 Map,用于在跨请求、跨实例时防止针对同一文件的并发重复下载。
|
|
286
|
-
* Map 的 Key 是缓存 Key,Value 是一个代表写入完成的 Promise。
|
|
287
324
|
*/
|
|
288
325
|
activeCacheWrites?: Map<string, Promise<void>>;
|
|
289
326
|
}
|
|
290
|
-
/**
|
|
327
|
+
/** 内部流水线上下文 */
|
|
291
328
|
interface FetchWithCacheContext extends FetchWithCacheOptions {
|
|
292
329
|
request: Request;
|
|
293
330
|
fetcher: (req: Request) => Promise<Response>;
|
|
@@ -295,22 +332,21 @@ interface FetchWithCacheContext extends FetchWithCacheOptions {
|
|
|
295
332
|
activeCacheWrites: Map<string, Promise<void>>;
|
|
296
333
|
}
|
|
297
334
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* @param
|
|
311
|
-
* @param
|
|
312
|
-
* @
|
|
313
|
-
* @returns 带有缓存标识头和流式 Body 的 Response 对象
|
|
335
|
+
* 核心协调函数:协调请求、缓存命中、并发控制和 SWR
|
|
336
|
+
*
|
|
337
|
+
* 流程如下:
|
|
338
|
+
* 1. 初始化上下文并生成缓存键。
|
|
339
|
+
* 2. 检查离线模式:若开启则强读取,未命中直接抛错。
|
|
340
|
+
* 3. 检查请求是否符合缓存规则 (isCacheable)。
|
|
341
|
+
* 4. 尝试读取缓存并判定状态 (HIT / STALE)。
|
|
342
|
+
* 5. 处理 SWR (后台更新)。
|
|
343
|
+
* 6. 处理请求合并 (Request Coalescing),防止缓存击穿。
|
|
344
|
+
* 7. 若缓存缺失,发起网络请求并流式写入。
|
|
345
|
+
*
|
|
346
|
+
* @param request - 标准 Web Request 对象
|
|
347
|
+
* @param fetcher - 底层发起真实请求的函数
|
|
348
|
+
* @param options - 缓存协调配置项
|
|
349
|
+
* @returns 标准 Web Response 对象 (带 x-proxy-cache 标头)
|
|
314
350
|
*/
|
|
315
351
|
declare function fetchWithCache(request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions): Promise<Response>;
|
|
316
352
|
|
|
@@ -344,6 +380,61 @@ declare function createFetchWithCache(activeCacheWrites?: Map<string, Promise<vo
|
|
|
344
380
|
*/
|
|
345
381
|
declare function createCachedFetch(defaultOptions: FetchWithCacheOptions): (request: Request, fetcher: (req: Request) => Promise<Response>, overrideOptions?: Partial<FetchWithCacheOptions>) => Promise<Response>;
|
|
346
382
|
|
|
383
|
+
/**
|
|
384
|
+
* 预缓存请求选项
|
|
385
|
+
*/
|
|
386
|
+
interface PrefetchRequest {
|
|
387
|
+
/** 请求 URL */
|
|
388
|
+
url: string;
|
|
389
|
+
/** 可选的请求配置(method, headers, body 等) */
|
|
390
|
+
request?: RequestInit;
|
|
391
|
+
}
|
|
392
|
+
interface PrefetchOptions {
|
|
393
|
+
/** 要预缓存的 URL 列表及其请求选项 */
|
|
394
|
+
urls: PrefetchRequest[];
|
|
395
|
+
/** 完整的代理配置 */
|
|
396
|
+
config: ProxyConfig;
|
|
397
|
+
/** SmartCache 实例 */
|
|
398
|
+
cache: SmartCache;
|
|
399
|
+
/** 自定义 fetcher,默认使用 globalThis.fetch */
|
|
400
|
+
fetcher?: (req: Request) => Promise<Response>;
|
|
401
|
+
/** 并发数,默认 3 */
|
|
402
|
+
concurrency?: number;
|
|
403
|
+
/** 进度回调 (completed, total, url) */
|
|
404
|
+
onProgress?: (completed: number, total: number, url: string) => void;
|
|
405
|
+
/** 取消信号 */
|
|
406
|
+
signal?: AbortSignal;
|
|
407
|
+
}
|
|
408
|
+
interface PrefetchResult {
|
|
409
|
+
/** 成功数量 */
|
|
410
|
+
succeeded: number;
|
|
411
|
+
/** 失败数量 */
|
|
412
|
+
failed: number;
|
|
413
|
+
/** 失败详情 */
|
|
414
|
+
errors?: Array<{
|
|
415
|
+
url: string;
|
|
416
|
+
error: Error;
|
|
417
|
+
}>;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 预缓存函数
|
|
421
|
+
*
|
|
422
|
+
* 提前将指定的 URL 列表内容存入缓存,支持并发控制和进度回调。
|
|
423
|
+
* 复用了 `createCachedFetch` 的完整逻辑,自动支持:
|
|
424
|
+
* - GET/POST/PUT/PATCH/DELETE 等所有方法
|
|
425
|
+
* - POST body 过滤和缓存键生成
|
|
426
|
+
* - 站点级配置
|
|
427
|
+
*
|
|
428
|
+
* @param options - 预缓存选项
|
|
429
|
+
* @returns 预缓存结果,包含成功/失败数量和错误详情
|
|
430
|
+
*/
|
|
431
|
+
declare function prefetch(options: PrefetchOptions): Promise<PrefetchResult>;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 判断当前请求是否满足可缓存的基础条件
|
|
435
|
+
*/
|
|
436
|
+
declare function isCacheable(request: Request, config: SiteCacheConfig): Promise<boolean>;
|
|
437
|
+
|
|
347
438
|
/**
|
|
348
439
|
* 从源对象中根据过滤配置提取数据并标准化。
|
|
349
440
|
*
|
|
@@ -452,4 +543,4 @@ declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value:
|
|
|
452
543
|
*/
|
|
453
544
|
declare function getSiteConfig(urlString: string, proxyConfig: ProxyConfig): SiteCacheConfig;
|
|
454
545
|
|
|
455
|
-
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 };
|
|
546
|
+
export { type BodyFilterConfig, type CacheEntry, type CacheMetadata, type CacheRule, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, OfflineCacheMissError, OfflineCacheMissErrorCode, type PrefetchOptions, type PrefetchRequest, type PrefetchResult, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, getSiteConfig, isAllowed, isCacheable, isGlob, isMatch, prefetch };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
import { CommonError, ErrorCode } from '@isdk/common-error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 错误类型定义
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Offline 缓存未命中错误代码
|
|
9
|
+
*
|
|
10
|
+
* 当处于 offline 模式且请求的 URL 没有对应缓存时抛出。
|
|
11
|
+
* 这帮助调用者区分:
|
|
12
|
+
* - 网络请求失败(其他错误类型)
|
|
13
|
+
* - offline 模式下缓存不存在(本错误)
|
|
14
|
+
*/
|
|
15
|
+
declare const OfflineCacheMissErrorCode = ErrorCode.OfflineCacheMiss;
|
|
16
|
+
/**
|
|
17
|
+
* Offline 缓存未命中错误
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* throw new OfflineCacheMissError('http://example.com/data')
|
|
21
|
+
*
|
|
22
|
+
* @extends CommonError
|
|
23
|
+
*/
|
|
24
|
+
declare class OfflineCacheMissError extends CommonError {
|
|
25
|
+
static code: ErrorCode;
|
|
26
|
+
constructor(url: string | number, name?: string | Record<string, any>);
|
|
27
|
+
}
|
|
28
|
+
|
|
1
29
|
/**
|
|
2
30
|
* 缓存键过滤配置
|
|
3
31
|
*
|
|
@@ -103,6 +131,8 @@ interface SiteCacheConfig {
|
|
|
103
131
|
staleIfError?: boolean;
|
|
104
132
|
/** 强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。 */
|
|
105
133
|
forceCache?: boolean;
|
|
134
|
+
/** 严格离线模式:不发起任何网络请求,只读缓存。缓存未命中时抛出 OfflineCacheMissError */
|
|
135
|
+
offline?: boolean;
|
|
106
136
|
}
|
|
107
137
|
/**
|
|
108
138
|
* 缓存元数据
|
|
@@ -230,8 +260,17 @@ declare class SmartCache {
|
|
|
230
260
|
* @returns Node.js 可写流
|
|
231
261
|
*/
|
|
232
262
|
setStream(key: string, metadata: Omit<CacheMetadata, 'size'>): NodeJS.WritableStream;
|
|
233
|
-
|
|
234
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Deletes the cache entry for the specified key.
|
|
265
|
+
* @param key - The cache key to delete
|
|
266
|
+
* @param [clearPersistent=true] - Whether to also delete the entry from persistent (disk) storage. Defaults to `true`.
|
|
267
|
+
*/
|
|
268
|
+
delete(key: string, clearPersistent?: boolean): Promise<void>;
|
|
269
|
+
/**
|
|
270
|
+
* Clears the cache. By default, both the in-memory cache and the persistent disk cache are cleared.
|
|
271
|
+
* @param [clearPersistent=true] - Whether to clear the persistent (disk) cache. Defaults to `true` for backward compatibility.
|
|
272
|
+
*/
|
|
273
|
+
clear(clearPersistent?: boolean): Promise<void>;
|
|
235
274
|
}
|
|
236
275
|
|
|
237
276
|
/**
|
|
@@ -282,12 +321,10 @@ interface FetchWithCacheOptions {
|
|
|
282
321
|
generateKey?: typeof generateCacheKey;
|
|
283
322
|
/**
|
|
284
323
|
* 并发写入任务追踪器
|
|
285
|
-
* 传入一个外部维护的 Map,用于在跨请求、跨实例时防止针对同一文件的并发重复下载。
|
|
286
|
-
* Map 的 Key 是缓存 Key,Value 是一个代表写入完成的 Promise。
|
|
287
324
|
*/
|
|
288
325
|
activeCacheWrites?: Map<string, Promise<void>>;
|
|
289
326
|
}
|
|
290
|
-
/**
|
|
327
|
+
/** 内部流水线上下文 */
|
|
291
328
|
interface FetchWithCacheContext extends FetchWithCacheOptions {
|
|
292
329
|
request: Request;
|
|
293
330
|
fetcher: (req: Request) => Promise<Response>;
|
|
@@ -295,22 +332,21 @@ interface FetchWithCacheContext extends FetchWithCacheOptions {
|
|
|
295
332
|
activeCacheWrites: Map<string, Promise<void>>;
|
|
296
333
|
}
|
|
297
334
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* @param
|
|
311
|
-
* @param
|
|
312
|
-
* @
|
|
313
|
-
* @returns 带有缓存标识头和流式 Body 的 Response 对象
|
|
335
|
+
* 核心协调函数:协调请求、缓存命中、并发控制和 SWR
|
|
336
|
+
*
|
|
337
|
+
* 流程如下:
|
|
338
|
+
* 1. 初始化上下文并生成缓存键。
|
|
339
|
+
* 2. 检查离线模式:若开启则强读取,未命中直接抛错。
|
|
340
|
+
* 3. 检查请求是否符合缓存规则 (isCacheable)。
|
|
341
|
+
* 4. 尝试读取缓存并判定状态 (HIT / STALE)。
|
|
342
|
+
* 5. 处理 SWR (后台更新)。
|
|
343
|
+
* 6. 处理请求合并 (Request Coalescing),防止缓存击穿。
|
|
344
|
+
* 7. 若缓存缺失,发起网络请求并流式写入。
|
|
345
|
+
*
|
|
346
|
+
* @param request - 标准 Web Request 对象
|
|
347
|
+
* @param fetcher - 底层发起真实请求的函数
|
|
348
|
+
* @param options - 缓存协调配置项
|
|
349
|
+
* @returns 标准 Web Response 对象 (带 x-proxy-cache 标头)
|
|
314
350
|
*/
|
|
315
351
|
declare function fetchWithCache(request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions): Promise<Response>;
|
|
316
352
|
|
|
@@ -344,6 +380,61 @@ declare function createFetchWithCache(activeCacheWrites?: Map<string, Promise<vo
|
|
|
344
380
|
*/
|
|
345
381
|
declare function createCachedFetch(defaultOptions: FetchWithCacheOptions): (request: Request, fetcher: (req: Request) => Promise<Response>, overrideOptions?: Partial<FetchWithCacheOptions>) => Promise<Response>;
|
|
346
382
|
|
|
383
|
+
/**
|
|
384
|
+
* 预缓存请求选项
|
|
385
|
+
*/
|
|
386
|
+
interface PrefetchRequest {
|
|
387
|
+
/** 请求 URL */
|
|
388
|
+
url: string;
|
|
389
|
+
/** 可选的请求配置(method, headers, body 等) */
|
|
390
|
+
request?: RequestInit;
|
|
391
|
+
}
|
|
392
|
+
interface PrefetchOptions {
|
|
393
|
+
/** 要预缓存的 URL 列表及其请求选项 */
|
|
394
|
+
urls: PrefetchRequest[];
|
|
395
|
+
/** 完整的代理配置 */
|
|
396
|
+
config: ProxyConfig;
|
|
397
|
+
/** SmartCache 实例 */
|
|
398
|
+
cache: SmartCache;
|
|
399
|
+
/** 自定义 fetcher,默认使用 globalThis.fetch */
|
|
400
|
+
fetcher?: (req: Request) => Promise<Response>;
|
|
401
|
+
/** 并发数,默认 3 */
|
|
402
|
+
concurrency?: number;
|
|
403
|
+
/** 进度回调 (completed, total, url) */
|
|
404
|
+
onProgress?: (completed: number, total: number, url: string) => void;
|
|
405
|
+
/** 取消信号 */
|
|
406
|
+
signal?: AbortSignal;
|
|
407
|
+
}
|
|
408
|
+
interface PrefetchResult {
|
|
409
|
+
/** 成功数量 */
|
|
410
|
+
succeeded: number;
|
|
411
|
+
/** 失败数量 */
|
|
412
|
+
failed: number;
|
|
413
|
+
/** 失败详情 */
|
|
414
|
+
errors?: Array<{
|
|
415
|
+
url: string;
|
|
416
|
+
error: Error;
|
|
417
|
+
}>;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 预缓存函数
|
|
421
|
+
*
|
|
422
|
+
* 提前将指定的 URL 列表内容存入缓存,支持并发控制和进度回调。
|
|
423
|
+
* 复用了 `createCachedFetch` 的完整逻辑,自动支持:
|
|
424
|
+
* - GET/POST/PUT/PATCH/DELETE 等所有方法
|
|
425
|
+
* - POST body 过滤和缓存键生成
|
|
426
|
+
* - 站点级配置
|
|
427
|
+
*
|
|
428
|
+
* @param options - 预缓存选项
|
|
429
|
+
* @returns 预缓存结果,包含成功/失败数量和错误详情
|
|
430
|
+
*/
|
|
431
|
+
declare function prefetch(options: PrefetchOptions): Promise<PrefetchResult>;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 判断当前请求是否满足可缓存的基础条件
|
|
435
|
+
*/
|
|
436
|
+
declare function isCacheable(request: Request, config: SiteCacheConfig): Promise<boolean>;
|
|
437
|
+
|
|
347
438
|
/**
|
|
348
439
|
* 从源对象中根据过滤配置提取数据并标准化。
|
|
349
440
|
*
|
|
@@ -452,4 +543,4 @@ declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value:
|
|
|
452
543
|
*/
|
|
453
544
|
declare function getSiteConfig(urlString: string, proxyConfig: ProxyConfig): SiteCacheConfig;
|
|
454
545
|
|
|
455
|
-
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 };
|
|
546
|
+
export { type BodyFilterConfig, type CacheEntry, type CacheMetadata, type CacheRule, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, OfflineCacheMissError, OfflineCacheMissErrorCode, type PrefetchOptions, type PrefetchRequest, type PrefetchResult, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, getSiteConfig, isAllowed, isCacheable, isGlob, isMatch, prefetch };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"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
|
+
"use strict";var t,e=Object.create,r=Object.defineProperty,n=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,s=(t,e,i,s)=>{if(e&&"object"==typeof e||"function"==typeof e)for(let o of c(e))a.call(t,o)||o===i||r(t,o,{get:()=>e[o],enumerable:!(s=n(e,o))||s.enumerable});return t},o=(t,n,c)=>(c=null!=t?e(i(t)):{},s(!n&&t&&t.__esModule?c:r(c,"default",{value:t,enumerable:!0}),t)),u={};((t,e)=>{for(var n in e)r(t,n,{get:e[n],enumerable:!0})})(u,{OfflineCacheMissError:()=>l,OfflineCacheMissErrorCode:()=>h,SmartCache:()=>m,createCachedFetch:()=>U,createFetchWithCache:()=>P,extractData:()=>E,fetchWithCache:()=>H,generateCacheKey:()=>q,getSiteConfig:()=>S,isAllowed:()=>R,isCacheable:()=>k,isGlob:()=>j,isMatch:()=>T,prefetch:()=>W}),module.exports=(t=u,s(r({},"__esModule",{value:!0}),t));var f=require("@isdk/common-error"),h=f.ErrorCode.OfflineCacheMiss,l=class extends f.CommonError{static code=h;constructor(t,e){super(`Offline mode: No cached response for ${t}`,e,h),this.data={url:t}}};f.CommonError[h]=l;var y=require("secondary-cache"),w=o(require("cacache")),d=o(require("os")),p=o(require("path")),m=class{memory;storagePath;maxMemorySize;constructor(t={}){this.storagePath=t.storagePath||p.default.join(d.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 y.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 r=w.default.get.stream(this.storagePath,t);return{...e,body:r}}try{const e=await w.default.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:r}=await w.default.get(this.storagePath,t),n=r,c={...n,body:e};return this.saveToMemory(t,e,n),c}{const e=(await w.default.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,e),{...e,body:w.default.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,r){const n={...r,size:e.length};await w.default.put(this.storagePath,t,e,{metadata:n}),this.saveToMemory(t,e,n)}saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...r,body:e});else{const{...e}=r;this.memory.set(t,e)}}getStream(t){return w.default.get.stream(this.storagePath,t)}setStream(t,e){this.memory.del(t);const r=w.default.put.stream(this.storagePath,t,{metadata:e});return r.on("finish",()=>{this.memory.del(t)}),r}async delete(t,e=!0){this.memory.del(t),e&&await w.default.rm.entry(this.storagePath,t)}async clear(t=!0){this.memory.clear(),t&&await w.default.rm.all(this.storagePath)}},b=require("crypto"),x=require("util-ex"),O=o(require("picomatch")),g=require("util-ex");function j(t){return/[!*?{}[\]()]/.test(t)}function T(t,e,r=!1){if(Array.isArray(t)&&t.length){const n=[],c=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?c.push(t.slice(1)):n.push(t)}),!(c.length>0&&c.some(t=>T(t,e,r)))&&(0===n.length||n.some(t=>T(t,e,r)))}return t instanceof RegExp?t.test(e):"string"==typeof t&&((0,g.isRegExpStr)(t)?(0,g.toRegExp)(t).test(e):j(t)?(0,O.default)(t,{dot:!0})(e):r?e.startsWith(t):e===t)}function R(t,e,r){let n;return e?.include&&(n=T(e.include,t)),e?.exclude&&T(e.exclude,t)&&(n=!1),void 0===n&&(n=r),n}var E=(t,e,r)=>{const n={};return Object.keys(t).filter(t=>R(t,e,r)).sort().forEach(e=>{const r=t[e];null!=r&&(n[e.toLowerCase()]=Array.isArray(r)?[...r].sort():[r])}),n};function S(t,e){const{sites:r,default:n}=e;if(!r)return n;let c="";try{c=new URL(t).hostname}catch{}for(const[e,n]of Object.entries(r)){if(c&&e===c)return n;if(T(e,t,!0))return n;if(c&&c.endsWith(e)&&(e.startsWith(".")||"."===c.charAt(c.length-e.length-1)))return n}return n}async function q(t,e){const r=new URL(t.url),n=t.headers.get("cookie")||"",c=Object.fromEntries(n.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let i=null;const a=t.method.toUpperCase();if(["POST","PUT","PATCH"].includes(a))try{const r=t.headers.get("content-type")||"";if(r.includes("application/json")){const r=await t.clone().json();i=E(r,e.body,!0)}else if(e.body?.extract&&(r.includes("text/")||r.includes("application/xml")||r.includes("x-www-form-urlencoded"))){const r=e.body?.maxLength||1024,n=(await t.clone().text()).slice(0,r),c=e.body.extract,a="string"==typeof c&&(0,x.isRegExpStr)(c)?(0,x.toRegExp)(c):c instanceof RegExp?c:null;if(a){const t=n.match(a);if(t)if(t.length>1){const r=t.slice(1);e.body?.sort&&r.sort(),i=r.join(":")}else i=t[0]}else i=(0,b.createHash)("sha256").update(n).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(i=(0,b.createHash)("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const s={m:a,h:r.host,p:r.pathname,q:E(Object.fromEntries(r.searchParams),e.query,!0),hd:E(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:E(c,e.cookies)};return null!==i&&(s.b=i),(0,b.createHash)("sha256").update(JSON.stringify(s)).digest("hex")}var A=require("stream"),C=require("stream/promises"),v=o(require("http-cache-semantics"));async function M(t,e,r,n,c,i){if(t.method&&t.method.toUpperCase()!==e)return!1;if(t.path&&!T(t.path,r.pathname,!0))return!1;if(t.query)for(const[e,r]of Object.entries(t.query)){const t=n.get(e)||"";if("boolean"==typeof r){if(r&&!n.has(e))return!1;if(!r&&n.has(e))return!1}else if(!T(r,t))return!1}if(t.bodyType||t.body){const e=c.headers.get("content-type")||"",r=e.includes("application/json")?"json":e.includes("text/")||e.includes("application/xml")||e.includes("x-www-form-urlencoded")?"text":"binary";if(t.bodyType&&t.bodyType!==r)return!1;if(t.body){if("binary"===r)return!1;if(!i.checked){try{const t=await c.clone().text();i.text=t.slice(0,i.limit)}catch{i.text=""}i.checked=!0}if(!i.text||!T(t.body,i.text))return!1}}return!0}async function k(t,e){const r=t.method.toUpperCase();if(!(e.methods||["GET","HEAD"]).includes(r))return!1;if(e.cacheRules&&e.cacheRules.length>0){const n=new URL(t.url),c=n.searchParams,i={text:null,checked:!1,limit:e.body?.maxLength||1024};for(const a of e.cacheRules)if(await M(a,r,n,c,t,i))return!0;return!1}return!0}function I(t,e){const r=204===t.status||304===t.status||t.status<200?null:function(t){if(t instanceof Buffer)return new Uint8Array(t);if(t&&"function"==typeof t.pipe)try{const e="function"==typeof t._read&&"object"==typeof t._readableState?t:A.Readable.from(t);return A.Readable.toWeb(e)}catch(e){return t}return t}(t.body);return new Response(r,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function L(t,e){let r,n;const c=new Promise((e,c)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},n=e=>{t.activeCacheWrites.delete(t.cacheKey),c(e)}});c.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,c);try{const e=await t.fetcher(t.request.clone()),c=new v.default({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"),!c.storable()&&!t.config.forceCache)return r(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:i});const a={status:e.status,headers:Object.fromEntries(e.headers),policy:c.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),a),r(),new Response(null,{status:e.status,statusText:e.statusText,headers:i});const[s,o]=e.body.tee();return(0,C.pipeline)(A.Readable.fromWeb(o),t.cache.setStream(t.cacheKey,a)).then(r).catch(n),new Response(s,{status:e.status,statusText:e.statusText,headers:i})}catch(r){if(n(r),e&&t.config.staleIfError)return I(e,"STALE_IF_ERROR");throw r}}async function H(t,e,r){const{config:n}=r,c=await async function(t,e,r){const n=r.generateKey||q,c=await n(t,r.config);return{...r,request:t,fetcher:e,cacheKey:c,activeCacheWrites:r.activeCacheWrites||new Map}}(t,e,r),i=await c.cache.get(c.cacheKey);if(n.offline){if(i)return I(i,"OFFLINE_HIT");throw new l(t.url)}if(!await k(t,n))return e(t);if(i){const t=function(t,e){const r=v.default.fromObject(e.policy),n={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(c,i);if("HIT"===t)return I(i,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const r=L(t,e).catch(r=>(console.error(`[SWR Error] ${t.cacheKey}:`,r),I(e,"STALE_IF_ERROR")));try{t.onBackgroundUpdate?.(r)}catch(e){console.error(`[SWR Callback Error] ${t.cacheKey}:`,e)}}(c,i),I(i,"STALE")}if(c.activeCacheWrites.has(c.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const r=await t.cache.get(t.cacheKey);return r?I(r,"HIT"):null}(c);if(t)return t}return L(c,i)}function P(t){return t||(t=new Map),async function(e,r,n){return H(e,r,{...n,activeCacheWrites:t})}}function U(t){const e=P(t.activeCacheWrites);return async function(r,n,c){return e(r,n,{...t,...c})}}async function W(t){const{urls:e,config:r,cache:n,fetcher:c=t=>globalThis.fetch(t),concurrency:i=3,onProgress:a,signal:s}=t,o={succeeded:0,failed:0,errors:[]};if(0===e.length)return o;if(s?.aborted)return o;const u=new Map,f=P(u),h=[...e];let l=0;const y=Array.from({length:Math.min(i,e.length)},()=>(async()=>{for(;h.length>0&&!s?.aborted;){const t=h.shift();if(!t)break;try{const e=S(t.url,r),i=new Request(t.url,{...t.request,signal:s});if(!await k(i,e))continue;const a=await f(i,c,{cache:n,config:{...e,offline:!1},backgroundUpdate:!1});a.headers.has("x-proxy-cache")&&(await a.arrayBuffer(),o.succeeded++)}catch(e){if("AbortError"===e.name||s?.aborted)break;o.failed++,o.errors.push({url:t.url,error:e})}finally{l++,a?.(l,e.length,t.url)}}})());return await Promise.all(y),u.size>0&&await Promise.allSettled(u.values()),o}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{CommonError as t,ErrorCode as e}from"@isdk/common-error";var r=e.OfflineCacheMiss,n=class extends t{static code=r;constructor(t,e){super(`Offline mode: No cached response for ${t}`,e,r),this.data={url:t}}};t[r]=n;import{LRUCache as o}from"secondary-cache";import i from"cacache";import s from"os";import c from"path";var a=class{memory;storagePath;maxMemorySize;constructor(t={}){this.storagePath=t.storagePath||c.join(s.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 o(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 r=i.get.stream(this.storagePath,t);return{...e,body:r}}try{const e=await i.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:r}=await i.get(this.storagePath,t),n=r,o={...n,body:e};return this.saveToMemory(t,e,n),o}{const e=(await i.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,e),{...e,body:i.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,r){const n={...r,size:e.length};await i.put(this.storagePath,t,e,{metadata:n}),this.saveToMemory(t,e,n)}saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...r,body:e});else{const{...e}=r;this.memory.set(t,e)}}getStream(t){return i.get.stream(this.storagePath,t)}setStream(t,e){this.memory.del(t);const r=i.put.stream(this.storagePath,t,{metadata:e});return r.on("finish",()=>{this.memory.del(t)}),r}async delete(t,e=!0){this.memory.del(t),e&&await i.rm.entry(this.storagePath,t)}async clear(t=!0){this.memory.clear(),t&&await i.rm.all(this.storagePath)}};import{createHash as u}from"crypto";import{isRegExpStr as f,toRegExp as h}from"util-ex";import l from"picomatch";import{isRegExpStr as p,toRegExp as y}from"util-ex";function m(t){return/[!*?{}[\]()]/.test(t)}function w(t,e,r=!1){if(Array.isArray(t)&&t.length){const n=[],o=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?o.push(t.slice(1)):n.push(t)}),!(o.length>0&&o.some(t=>w(t,e,r)))&&(0===n.length||n.some(t=>w(t,e,r)))}return t instanceof RegExp?t.test(e):"string"==typeof t&&(p(t)?y(t).test(e):m(t)?l(t,{dot:!0})(e):r?e.startsWith(t):e===t)}function d(t,e,r){let n;return e?.include&&(n=w(e.include,t)),e?.exclude&&w(e.exclude,t)&&(n=!1),void 0===n&&(n=r),n}var x=(t,e,r)=>{const n={};return Object.keys(t).filter(t=>d(t,e,r)).sort().forEach(e=>{const r=t[e];null!=r&&(n[e.toLowerCase()]=Array.isArray(r)?[...r].sort():[r])}),n};function b(t,e){const{sites:r,default:n}=e;if(!r)return n;let o="";try{o=new URL(t).hostname}catch{}for(const[e,n]of Object.entries(r)){if(o&&e===o)return n;if(w(e,t,!0))return n;if(o&&o.endsWith(e)&&(e.startsWith(".")||"."===o.charAt(o.length-e.length-1)))return n}return n}async function g(t,e){const r=new URL(t.url),n=t.headers.get("cookie")||"",o=Object.fromEntries(n.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let i=null;const s=t.method.toUpperCase();if(["POST","PUT","PATCH"].includes(s))try{const r=t.headers.get("content-type")||"";if(r.includes("application/json")){const r=await t.clone().json();i=x(r,e.body,!0)}else if(e.body?.extract&&(r.includes("text/")||r.includes("application/xml")||r.includes("x-www-form-urlencoded"))){const r=e.body?.maxLength||1024,n=(await t.clone().text()).slice(0,r),o=e.body.extract,s="string"==typeof o&&f(o)?h(o):o instanceof RegExp?o:null;if(s){const t=n.match(s);if(t)if(t.length>1){const r=t.slice(1);e.body?.sort&&r.sort(),i=r.join(":")}else i=t[0]}else i=u("sha256").update(n).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(i=u("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const c={m:s,h:r.host,p:r.pathname,q:x(Object.fromEntries(r.searchParams),e.query,!0),hd:x(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:x(o,e.cookies)};return null!==i&&(c.b=i),u("sha256").update(JSON.stringify(c)).digest("hex")}import{Readable as R}from"stream";import{pipeline as T}from"stream/promises";import E from"http-cache-semantics";async function O(t,e,r,n,o,i){if(t.method&&t.method.toUpperCase()!==e)return!1;if(t.path&&!w(t.path,r.pathname,!0))return!1;if(t.query)for(const[e,r]of Object.entries(t.query)){const t=n.get(e)||"";if("boolean"==typeof r){if(r&&!n.has(e))return!1;if(!r&&n.has(e))return!1}else if(!w(r,t))return!1}if(t.bodyType||t.body){const e=o.headers.get("content-type")||"",r=e.includes("application/json")?"json":e.includes("text/")||e.includes("application/xml")||e.includes("x-www-form-urlencoded")?"text":"binary";if(t.bodyType&&t.bodyType!==r)return!1;if(t.body){if("binary"===r)return!1;if(!i.checked){try{const t=await o.clone().text();i.text=t.slice(0,i.limit)}catch{i.text=""}i.checked=!0}if(!i.text||!w(t.body,i.text))return!1}}return!0}async function S(t,e){const r=t.method.toUpperCase();if(!(e.methods||["GET","HEAD"]).includes(r))return!1;if(e.cacheRules&&e.cacheRules.length>0){const n=new URL(t.url),o=n.searchParams,i={text:null,checked:!1,limit:e.body?.maxLength||1024};for(const s of e.cacheRules)if(await O(s,r,n,o,t,i))return!0;return!1}return!0}function j(t,e){const r=204===t.status||304===t.status||t.status<200?null:function(t){if(t instanceof Buffer)return new Uint8Array(t);if(t&&"function"==typeof t.pipe)try{const e="function"==typeof t._read&&"object"==typeof t._readableState?t:R.from(t);return R.toWeb(e)}catch(e){return t}return t}(t.body);return new Response(r,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function A(t,e){let r,n;const o=new Promise((e,o)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},n=e=>{t.activeCacheWrites.delete(t.cacheKey),o(e)}});o.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,o);try{const e=await t.fetcher(t.request.clone()),o=new E({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"),!o.storable()&&!t.config.forceCache)return r(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:i});const s={status:e.status,headers:Object.fromEntries(e.headers),policy:o.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),r(),new Response(null,{status:e.status,statusText:e.statusText,headers:i});const[c,a]=e.body.tee();return T(R.fromWeb(a),t.cache.setStream(t.cacheKey,s)).then(r).catch(n),new Response(c,{status:e.status,statusText:e.statusText,headers:i})}catch(r){if(n(r),e&&t.config.staleIfError)return j(e,"STALE_IF_ERROR");throw r}}async function k(t,e,r){const{config:o}=r,i=await async function(t,e,r){const n=r.generateKey||g,o=await n(t,r.config);return{...r,request:t,fetcher:e,cacheKey:o,activeCacheWrites:r.activeCacheWrites||new Map}}(t,e,r),s=await i.cache.get(i.cacheKey);if(o.offline){if(s)return j(s,"OFFLINE_HIT");throw new n(t.url)}if(!await S(t,o))return e(t);if(s){const t=function(t,e){const r=E.fromObject(e.policy),n={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(i,s);if("HIT"===t)return j(s,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const r=A(t,e).catch(r=>(console.error(`[SWR Error] ${t.cacheKey}:`,r),j(e,"STALE_IF_ERROR")));try{t.onBackgroundUpdate?.(r)}catch(e){console.error(`[SWR Callback Error] ${t.cacheKey}:`,e)}}(i,s),j(s,"STALE")}if(i.activeCacheWrites.has(i.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const r=await t.cache.get(t.cacheKey);return r?j(r,"HIT"):null}(i);if(t)return t}return A(i,s)}function I(t){return t||(t=new Map),async function(e,r,n){return k(e,r,{...n,activeCacheWrites:t})}}function L(t){const e=I(t.activeCacheWrites);return async function(r,n,o){return e(r,n,{...t,...o})}}async function H(t){const{urls:e,config:r,cache:n,fetcher:o=t=>globalThis.fetch(t),concurrency:i=3,onProgress:s,signal:c}=t,a={succeeded:0,failed:0,errors:[]};if(0===e.length)return a;if(c?.aborted)return a;const u=new Map,f=I(u),h=[...e];let l=0;const p=Array.from({length:Math.min(i,e.length)},()=>(async()=>{for(;h.length>0&&!c?.aborted;){const t=h.shift();if(!t)break;try{const e=b(t.url,r),i=new Request(t.url,{...t.request,signal:c});if(!await S(i,e))continue;const s=await f(i,o,{cache:n,config:{...e,offline:!1},backgroundUpdate:!1});s.headers.has("x-proxy-cache")&&(await s.arrayBuffer(),a.succeeded++)}catch(e){if("AbortError"===e.name||c?.aborted)break;a.failed++,a.errors.push({url:t.url,error:e})}finally{l++,s?.(l,e.length,t.url)}}})());return await Promise.all(p),u.size>0&&await Promise.allSettled(u.values()),a}export{n as OfflineCacheMissError,r as OfflineCacheMissErrorCode,a as SmartCache,L as createCachedFetch,I as createFetchWithCache,x as extractData,k as fetchWithCache,g as generateCacheKey,b as getSiteConfig,d as isAllowed,S as isCacheable,m as isGlob,w as isMatch,H as prefetch};
|