@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.
Files changed (36) hide show
  1. package/README.cn.md +59 -1
  2. package/README.md +59 -1
  3. package/dist/index.d.mts +113 -22
  4. package/dist/index.d.ts +113 -22
  5. package/dist/index.js +1 -1
  6. package/dist/index.mjs +1 -1
  7. package/docs/README.md +59 -1
  8. package/docs/classes/OfflineCacheMissError.md +426 -0
  9. package/docs/classes/SmartCache.md +30 -10
  10. package/docs/functions/createCachedFetch.md +1 -1
  11. package/docs/functions/createFetchWithCache.md +1 -1
  12. package/docs/functions/extractData.md +1 -1
  13. package/docs/functions/fetchWithCache.md +14 -15
  14. package/docs/functions/generateCacheKey.md +1 -1
  15. package/docs/functions/getSiteConfig.md +1 -1
  16. package/docs/functions/isAllowed.md +1 -1
  17. package/docs/functions/isCacheable.md +27 -0
  18. package/docs/functions/isGlob.md +1 -1
  19. package/docs/functions/isMatch.md +1 -1
  20. package/docs/functions/prefetch.md +33 -0
  21. package/docs/globals.md +10 -0
  22. package/docs/interfaces/BodyFilterConfig.md +6 -6
  23. package/docs/interfaces/CacheEntry.md +9 -9
  24. package/docs/interfaces/CacheMetadata.md +8 -8
  25. package/docs/interfaces/CacheRule.md +6 -6
  26. package/docs/interfaces/FetchWithCacheContext.md +11 -13
  27. package/docs/interfaces/FetchWithCacheOptions.md +7 -9
  28. package/docs/interfaces/KeyFilterConfig.md +3 -3
  29. package/docs/interfaces/PrefetchOptions.md +107 -0
  30. package/docs/interfaces/PrefetchRequest.md +31 -0
  31. package/docs/interfaces/PrefetchResult.md +47 -0
  32. package/docs/interfaces/ProxyConfig.md +4 -4
  33. package/docs/interfaces/SiteCacheConfig.md +19 -9
  34. package/docs/interfaces/SmartCacheOptions.md +5 -5
  35. package/docs/variables/OfflineCacheMissErrorCode.md +18 -0
  36. 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` | **仅限 JSON** 的请求体字段过滤。 |
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 JSON request body fields. |
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
- delete(key: string): Promise<void>;
234
- clear(): Promise<void>;
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
- * 核心协调函数 (Fetcher Orchestrator)
299
- *
300
- * 实现了基于流的混合缓存代理核心逻辑,主要机制包括:
301
- * - **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。
302
- * - **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。
303
- * - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM
304
- * - **SWR (Stale-While-Revalidate)**:后台静默更新机制。
305
- * - **并发防击穿 (Request Coalescing)**:利用 `activeCacheWrites` 将并发请求合并。
306
- * - **强制离线容灾**:支持 `staleIfError` 和 `forceCache`(无视 Cache-Control 强制入库)。
307
- *
308
- * 并且会在响应头中自动注入 `x-proxy-cache` 标明缓存命中状态 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`)。
309
- *
310
- * @param request - 原始 Web 标准 Request 对象
311
- * @param fetcher - 实际执行网络请求的函数
312
- * @param options - 缓存配置选项
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
- delete(key: string): Promise<void>;
234
- clear(): Promise<void>;
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
- * 核心协调函数 (Fetcher Orchestrator)
299
- *
300
- * 实现了基于流的混合缓存代理核心逻辑,主要机制包括:
301
- * - **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。
302
- * - **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。
303
- * - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM
304
- * - **SWR (Stale-While-Revalidate)**:后台静默更新机制。
305
- * - **并发防击穿 (Request Coalescing)**:利用 `activeCacheWrites` 将并发请求合并。
306
- * - **强制离线容灾**:支持 `staleIfError` 和 `forceCache`(无视 Cache-Control 强制入库)。
307
- *
308
- * 并且会在响应头中自动注入 `x-proxy-cache` 标明缓存命中状态 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`)。
309
- *
310
- * @param request - 原始 Web 标准 Request 对象
311
- * @param fetcher - 实际执行网络请求的函数
312
- * @param options - 缓存配置选项
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{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};
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};