@dsh-plus/web-cache-headers 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agguy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/lib/index.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/config.d.ts
4
+ declare const Config: z<Schemastery.ObjectS<{
5
+ enabled: z<boolean, boolean>;
6
+ }>, Schemastery.ObjectT<{
7
+ enabled: z<boolean, boolean>;
8
+ }>>;
9
+ type WebCacheHeadersConfig = Schemastery.TypeT<typeof Config>;
10
+ //#endregion
11
+ //#region src/ns.d.ts
12
+ /**
13
+ * settings 命名空间字面量(纯常量,零依赖)。
14
+ * 服务端以字面量注册 settings section(用户层配置落 $DSH_HOME/settings.yaml,
15
+ * 热生效);与行级 cordis Config 共用同一份 schemastery schema(config.ts)。
16
+ * @module @dsh-plus/web-cache-headers/ns
17
+ */
18
+ declare const SETTINGS_NS = "dsh-plus-web-cache-headers";
19
+ //#endregion
20
+ //#region src/index.d.ts
21
+ declare const name = "dsh-plus-web-cache-headers";
22
+ /**
23
+ * 装配补丁并接线配置源:settings 用户层($DSH_HOME/settings.yaml,热生效)
24
+ * 优先,缺席/detach 回落 cordis 行级 config。enabled 翻转即时装/卸补丁,
25
+ * 无需 /reload;插件 dispose(profile 卸载 / live reload)保证卸下。
26
+ * @param ctx - 宿主上下文(settings 可选)。
27
+ * @param config - 行级 config(settings 缺席时的默认值来源)。
28
+ */
29
+ declare function apply(ctx: Context, config: WebCacheHeadersConfig): void;
30
+ //#endregion
31
+ export { Config, SETTINGS_NS, type WebCacheHeadersConfig, apply, name };
package/lib/index.js ADDED
@@ -0,0 +1,143 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import http from "node:http";
3
+ //#region src/config.ts
4
+ /**
5
+ * 插件配置(schemastery):可调参数一律外部化,不在逻辑里硬编码。
6
+ *
7
+ * 与仓库既有约定一致:Config 由 cordis 行级 config 解析,部署方可在
8
+ * profile 的 cordis.patch.yml 覆盖;同时经 settings namespace 暴露到
9
+ * $DSH_HOME/settings.yaml 用户层,修改热生效(无需 /reload)。
10
+ * @module @dsh-plus/web-cache-headers/config
11
+ */
12
+ const Config = z.object({ enabled: z.boolean().description("总开关(false = 卸下补丁,响应头恢复 dsh 原生行为);development 环境恒禁用以保护 dev/HMR").default(true) });
13
+ //#endregion
14
+ //#region src/ns.ts
15
+ /**
16
+ * settings 命名空间字面量(纯常量,零依赖)。
17
+ * 服务端以字面量注册 settings section(用户层配置落 $DSH_HOME/settings.yaml,
18
+ * 热生效);与行级 cordis Config 共用同一份 schemastery schema(config.ts)。
19
+ * @module @dsh-plus/web-cache-headers/ns
20
+ */
21
+ const SETTINGS_NS = "dsh-plus-web-cache-headers";
22
+ //#endregion
23
+ //#region src/decision.ts
24
+ /**
25
+ * 缓存头决策(纯函数,零 IO):给定请求方法、URL、响应状态与「响应是否已带
26
+ * cache-control」,决定是否补 immutable 长缓存头。
27
+ *
28
+ * 命中条件(全部满足,缺一即不干预):
29
+ * - 方法为 GET/HEAD(静态资源读取语义;写操作永不缓存);
30
+ * - 响应状态恰为 200(错误/重定向/未修改一律不加,避免缓存污染);
31
+ * - 路径以 `/assets/` 前缀(带段边界:`/assets` 根与 `/assetsX` 均不命中);
32
+ * - 调用方(含 core 未来版本自带的策略)未设置过 cache-control —— 纯增量,
33
+ * 已有策略永远优先。
34
+ *
35
+ * 为什么只认 `/assets/`:dsh 前端 dist(Vite 产物)全部文件内容哈希命名
36
+ * (JS/CSS/字体/语言包已逐一核实),URL 即内容地址,immutable 安全;
37
+ * `/plugins/*` 由 core 自带 `public, max-age=31536000, immutable`
38
+ * (dsh-client-modules 的 IMMUTABLE_CACHE),不在此重复;index.html /
39
+ * favicon / manifest 保持 dsh 原生行为(不缓存),确保插件清单与 rev 每次最新。
40
+ * @module @dsh-plus/web-cache-headers/decision
41
+ */
42
+ /** 与 dsh-client-modules 的 IMMUTABLE_CACHE 保持逐字节一致。 */
43
+ const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
44
+ /** dsh 前端 dist 挂载点(段边界即末尾斜杠:排除 `/assetsX` 与 `/assets` 根)。 */
45
+ const HASHED_ASSETS_PREFIX = "/assets/";
46
+ /** 取 origin-form URL 的路径部分(剥查询串;HTTP 请求行不含 hash)。 */
47
+ function pathnameOf(url) {
48
+ const queryAt = url.indexOf("?");
49
+ return queryAt === -1 ? url : url.slice(0, queryAt);
50
+ }
51
+ /**
52
+ * 决策一个响应是否补 immutable 缓存头。
53
+ * @param method - 请求方法(HTTP 方法大写敏感,按请求行原样传入)。
54
+ * @param url - 请求 URL(origin-form,可带查询串)。
55
+ * @param statusCode - 待写入的响应状态码。
56
+ * @param hasCacheControl - 响应是否已带 cache-control(setHeader 或 writeHead 头对象任一)。
57
+ * @returns 应写入的缓存头值;不干预时返回 undefined。
58
+ */
59
+ function decideCacheControl(method, url, statusCode, hasCacheControl) {
60
+ if (hasCacheControl) return void 0;
61
+ if (method !== "GET" && method !== "HEAD") return void 0;
62
+ if (statusCode !== 200) return void 0;
63
+ if (!pathnameOf(url).startsWith(HASHED_ASSETS_PREFIX)) return void 0;
64
+ return IMMUTABLE_CACHE_CONTROL;
65
+ }
66
+ //#endregion
67
+ //#region src/patch.ts
68
+ let originalWriteHead;
69
+ let installs = 0;
70
+ /** 取 writeHead 可变参末位的头对象(不存在则 undefined;数组形态不支持也返回 undefined)。 */
71
+ function headersObjectOf(args) {
72
+ if (args.length < 2) return void 0;
73
+ const last = args[args.length - 1];
74
+ if (last !== null && typeof last === "object" && !Array.isArray(last)) return last;
75
+ }
76
+ /** 头对象内是否已声明 cache-control(HTTP 头名大小写不敏感)。 */
77
+ function objectHasCacheControl(headers) {
78
+ if (headers === void 0) return false;
79
+ return Object.keys(headers).some((name) => name.toLowerCase() === "cache-control");
80
+ }
81
+ /** 安装补丁(幂等,引用计数叠加)。 */
82
+ function installImmutableAssetsPatch() {
83
+ installs += 1;
84
+ if (originalWriteHead !== void 0) return;
85
+ originalWriteHead = http.ServerResponse.prototype.writeHead;
86
+ const original = originalWriteHead;
87
+ const patched = function(...args) {
88
+ try {
89
+ const statusCode = typeof args[0] === "number" ? args[0] : 0;
90
+ const header = decideCacheControl(this.req?.method ?? "", this.req?.url ?? "", statusCode, this.getHeader("cache-control") !== void 0 || objectHasCacheControl(headersObjectOf(args)));
91
+ if (header !== void 0) this.setHeader("cache-control", header);
92
+ } catch {}
93
+ return original.apply(this, args);
94
+ };
95
+ http.ServerResponse.prototype.writeHead = patched;
96
+ }
97
+ /** 卸下补丁(引用计数归零才还原原型;超额调用安全)。 */
98
+ function uninstallImmutableAssetsPatch() {
99
+ if (installs > 0) installs -= 1;
100
+ if (installs > 0 || originalWriteHead === void 0) return;
101
+ http.ServerResponse.prototype.writeHead = originalWriteHead;
102
+ originalWriteHead = void 0;
103
+ }
104
+ //#endregion
105
+ //#region src/index.ts
106
+ const name = "dsh-plus-web-cache-headers";
107
+ /**
108
+ * 装配补丁并接线配置源:settings 用户层($DSH_HOME/settings.yaml,热生效)
109
+ * 优先,缺席/detach 回落 cordis 行级 config。enabled 翻转即时装/卸补丁,
110
+ * 无需 /reload;插件 dispose(profile 卸载 / live reload)保证卸下。
111
+ * @param ctx - 宿主上下文(settings 可选)。
112
+ * @param config - 行级 config(settings 缺席时的默认值来源)。
113
+ */
114
+ function apply(ctx, config) {
115
+ const logger = ctx.logger("web-cache-headers");
116
+ if (process.env.NODE_ENV === "development") {
117
+ logger.info("development 环境:自动禁用(保护 dev/HMR 同 URL 热更新)");
118
+ return;
119
+ }
120
+ let installed = false;
121
+ const sync = (enabled) => {
122
+ if (enabled === installed) return;
123
+ if (enabled) installImmutableAssetsPatch();
124
+ else uninstallImmutableAssetsPatch();
125
+ installed = enabled;
126
+ logger.info(enabled ? "已装配 /assets immutable 缓存头补丁" : "已卸下缓存头补丁(恢复 dsh 原生行为)");
127
+ };
128
+ let current = () => config;
129
+ ctx.inject(["settings"], (settingsCtx) => {
130
+ settingsCtx.settings.installSection(ctx, SETTINGS_NS, Config, config, {
131
+ setSource: (source) => {
132
+ current = source;
133
+ },
134
+ onChange: () => {
135
+ sync(current().enabled);
136
+ }
137
+ });
138
+ });
139
+ sync(current().enabled);
140
+ ctx.effect(() => () => sync(false), "web-cache-headers: dispose 卸下补丁");
141
+ }
142
+ //#endregion
143
+ export { Config, SETTINGS_NS, apply, name };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@dsh-plus/web-cache-headers",
3
+ "version": "0.1.0",
4
+ "description": "dsh-plus service plugin: GUI 静态资源长缓存——为内容哈希的 /assets/* 响应补 `Cache-Control: immutable`(/plugins 官方已自带),消除每次刷新重复传输 ~1.4MB JS/CSS;进程级 writeHead 增量拦截,零路由注册、零 dsh 服务耦合,已有缓存头优先保证未来共存",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./src/*": "./src/*",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "src"
19
+ ],
20
+ "peerDependencies": {
21
+ "@deepseek-ai/cordis": "^4.0.2",
22
+ "@deepseek-ai/dsh-settings": "^0.1.6-alpha.2",
23
+ "@deepseek-ai/schemastery": "^3.18.2"
24
+ },
25
+ "devDependencies": {
26
+ "@deepseek-ai/cordis": "4.0.2",
27
+ "@deepseek-ai/dsh-settings": "0.1.6-alpha.2",
28
+ "@deepseek-ai/schemastery": "3.18.2"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/A-G-guy/dsh-plus.git",
37
+ "directory": "packages/web-cache-headers"
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown src/index.ts -d lib --format esm --dts --no-fixed-extension"
41
+ }
42
+ }
package/src/config.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 插件配置(schemastery):可调参数一律外部化,不在逻辑里硬编码。
3
+ *
4
+ * 与仓库既有约定一致:Config 由 cordis 行级 config 解析,部署方可在
5
+ * profile 的 cordis.patch.yml 覆盖;同时经 settings namespace 暴露到
6
+ * $DSH_HOME/settings.yaml 用户层,修改热生效(无需 /reload)。
7
+ * @module @dsh-plus/web-cache-headers/config
8
+ */
9
+ import z from '@deepseek-ai/schemastery'
10
+
11
+ export const Config = z.object({
12
+ enabled: z
13
+ .boolean()
14
+ .description(
15
+ '总开关(false = 卸下补丁,响应头恢复 dsh 原生行为);development 环境恒禁用以保护 dev/HMR',
16
+ )
17
+ .default(true),
18
+ })
19
+
20
+ export type WebCacheHeadersConfig = Schemastery.TypeT<typeof Config>
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 缓存头决策(纯函数,零 IO):给定请求方法、URL、响应状态与「响应是否已带
3
+ * cache-control」,决定是否补 immutable 长缓存头。
4
+ *
5
+ * 命中条件(全部满足,缺一即不干预):
6
+ * - 方法为 GET/HEAD(静态资源读取语义;写操作永不缓存);
7
+ * - 响应状态恰为 200(错误/重定向/未修改一律不加,避免缓存污染);
8
+ * - 路径以 `/assets/` 前缀(带段边界:`/assets` 根与 `/assetsX` 均不命中);
9
+ * - 调用方(含 core 未来版本自带的策略)未设置过 cache-control —— 纯增量,
10
+ * 已有策略永远优先。
11
+ *
12
+ * 为什么只认 `/assets/`:dsh 前端 dist(Vite 产物)全部文件内容哈希命名
13
+ * (JS/CSS/字体/语言包已逐一核实),URL 即内容地址,immutable 安全;
14
+ * `/plugins/*` 由 core 自带 `public, max-age=31536000, immutable`
15
+ * (dsh-client-modules 的 IMMUTABLE_CACHE),不在此重复;index.html /
16
+ * favicon / manifest 保持 dsh 原生行为(不缓存),确保插件清单与 rev 每次最新。
17
+ * @module @dsh-plus/web-cache-headers/decision
18
+ */
19
+
20
+ /** 与 dsh-client-modules 的 IMMUTABLE_CACHE 保持逐字节一致。 */
21
+ export const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable'
22
+
23
+ /** dsh 前端 dist 挂载点(段边界即末尾斜杠:排除 `/assetsX` 与 `/assets` 根)。 */
24
+ const HASHED_ASSETS_PREFIX = '/assets/'
25
+
26
+ /** 取 origin-form URL 的路径部分(剥查询串;HTTP 请求行不含 hash)。 */
27
+ export function pathnameOf(url: string): string {
28
+ const queryAt = url.indexOf('?')
29
+ return queryAt === -1 ? url : url.slice(0, queryAt)
30
+ }
31
+
32
+ /**
33
+ * 决策一个响应是否补 immutable 缓存头。
34
+ * @param method - 请求方法(HTTP 方法大写敏感,按请求行原样传入)。
35
+ * @param url - 请求 URL(origin-form,可带查询串)。
36
+ * @param statusCode - 待写入的响应状态码。
37
+ * @param hasCacheControl - 响应是否已带 cache-control(setHeader 或 writeHead 头对象任一)。
38
+ * @returns 应写入的缓存头值;不干预时返回 undefined。
39
+ */
40
+ export function decideCacheControl(
41
+ method: string,
42
+ url: string,
43
+ statusCode: number,
44
+ hasCacheControl: boolean,
45
+ ): string | undefined {
46
+ if (hasCacheControl) return undefined
47
+ if (method !== 'GET' && method !== 'HEAD') return undefined
48
+ if (statusCode !== 200) return undefined
49
+ if (!pathnameOf(url).startsWith(HASHED_ASSETS_PREFIX)) return undefined
50
+ return IMMUTABLE_CACHE_CONTROL
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * dsh 插件:GUI 静态资源长缓存(web-cache-headers)。
3
+ *
4
+ * 问题:dsh 前端 dist(`dsh-host-frontend-static` 经 fallback 服务)的响应只有
5
+ * content-type,无 Cache-Control/ETag——浏览器每次刷新都全量重下 ~1.4MB
6
+ * (gzip 后约 500KB)JS/CSS。而 dist 文件全部内容哈希命名,本可安全 immutable。
7
+ * `/plugins/*` 官方已自带 immutable 缓存头(dsh-client-modules),不在范围。
8
+ *
9
+ * 本插件只做一件事:进程级拦截 ServerResponse.writeHead,对内容寻址的
10
+ * `/assets/*` 200 响应补 `Cache-Control: public, max-age=31536000, immutable`。
11
+ * 纯增量、已有缓存头优先——dsh 升级自带策略时自动共存;唯一的关闭路径是
12
+ * settings 的 enabled=false(热生效),立刻恢复 dsh 原生行为。
13
+ *
14
+ * 明确不做:
15
+ * - 不注册任何路由(避免与 core 未来路由撞车);
16
+ * - 不缓存 index.html / favicon / manifest(保持每次最新,插件 rev 清单不走缓存);
17
+ * - 不触碰 `/plugins`(官方已 immutable)与任何插件 API 路由;
18
+ * - development 环境自动禁用,保护 dev/HMR 的 same-URL 热更新语义。
19
+ *
20
+ * 结构守卫(fail-safe):inject 为空,不依赖任何 dsh 服务;宿主形态任何变更
21
+ * 都不影响本插件,反之本插件异常也绝不拖垮 boot(加头逻辑被隔离在 try 中)。
22
+ * @module @dsh-plus/web-cache-headers
23
+ */
24
+ import type { Context } from '@deepseek-ai/cordis'
25
+ import type {} from '@deepseek-ai/dsh-settings'
26
+
27
+ import { Config, type WebCacheHeadersConfig } from './config.ts'
28
+ import { SETTINGS_NS } from './ns.ts'
29
+ import { installImmutableAssetsPatch, uninstallImmutableAssetsPatch } from './patch.ts'
30
+
31
+ export const name = 'dsh-plus-web-cache-headers'
32
+
33
+ /** 无硬 inject:见模块头「结构守卫」。 */
34
+
35
+ export type { WebCacheHeadersConfig }
36
+ export { Config, SETTINGS_NS }
37
+
38
+ /**
39
+ * 装配补丁并接线配置源:settings 用户层($DSH_HOME/settings.yaml,热生效)
40
+ * 优先,缺席/detach 回落 cordis 行级 config。enabled 翻转即时装/卸补丁,
41
+ * 无需 /reload;插件 dispose(profile 卸载 / live reload)保证卸下。
42
+ * @param ctx - 宿主上下文(settings 可选)。
43
+ * @param config - 行级 config(settings 缺席时的默认值来源)。
44
+ */
45
+ export function apply(ctx: Context, config: WebCacheHeadersConfig): void {
46
+ const logger = ctx.logger('web-cache-headers')
47
+ if (process.env.NODE_ENV === 'development') {
48
+ logger.info('development 环境:自动禁用(保护 dev/HMR 同 URL 热更新)')
49
+ return
50
+ }
51
+
52
+ let installed = false
53
+ const sync = (enabled: boolean): void => {
54
+ if (enabled === installed) return
55
+ if (enabled) installImmutableAssetsPatch()
56
+ else uninstallImmutableAssetsPatch()
57
+ installed = enabled
58
+ logger.info(
59
+ enabled ? '已装配 /assets immutable 缓存头补丁' : '已卸下缓存头补丁(恢复 dsh 原生行为)',
60
+ )
61
+ }
62
+
63
+ let current: () => WebCacheHeadersConfig = () => config
64
+ ctx.inject(['settings'], (settingsCtx) => {
65
+ settingsCtx.settings.installSection(ctx, SETTINGS_NS, Config, config, {
66
+ setSource: (source) => {
67
+ current = source
68
+ },
69
+ onChange: () => {
70
+ sync(current().enabled)
71
+ },
72
+ })
73
+ })
74
+
75
+ sync(current().enabled)
76
+ ctx.effect(() => () => sync(false), 'web-cache-headers: dispose 卸下补丁')
77
+ }
package/src/ns.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * settings 命名空间字面量(纯常量,零依赖)。
3
+ * 服务端以字面量注册 settings section(用户层配置落 $DSH_HOME/settings.yaml,
4
+ * 热生效);与行级 cordis Config 共用同一份 schemastery schema(config.ts)。
5
+ * @module @dsh-plus/web-cache-headers/ns
6
+ */
7
+ export const SETTINGS_NS = 'dsh-plus-web-cache-headers'
package/src/patch.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 进程级 `http.ServerResponse.prototype.writeHead` 增量补丁。
3
+ *
4
+ * 选型理由(对比"注册 /assets 前缀路由自行服务文件"):
5
+ * - 零路由注册 —— core 未来若注册同名路由不会撞车 throw;
6
+ * - 零文件服务/MIME/穿越防护/dist 路径知识 —— core 服务文件的任何改动无需同步;
7
+ * - 唯一耦合点是 `/assets/` 路径约定与 core 经 writeHead 写响应(node:http
8
+ * 全代码库统一用法),是跟随 dsh 更新维护成本最低的切面。
9
+ *
10
+ * 正确性约束:
11
+ * - 纯增量:仅当决策命中且响应未带 cache-control 时 setHeader 一个头,
12
+ * 绝不改写既有头、绝不改状态码/响应体;
13
+ * - writeHead 全重载形态兼容(statusCode | +headers | +statusMessage+headers),
14
+ * 头对象中的 cache-control(大小写不敏感)同样被尊重;
15
+ * - 决策异常被吞掉并原样委托 —— 补丁处于所有响应的关键路径,绝不能让
16
+ * 加头逻辑影响响应写入;
17
+ * - 模块级引用计数:多实例(多 profile / live reload 先装后卸)可叠加安装,
18
+ * 计数归零才还原原型。
19
+ * @module @dsh-plus/web-cache-headers/patch
20
+ */
21
+ import type { ServerResponse } from 'node:http'
22
+ import http from 'node:http'
23
+
24
+ import { decideCacheControl } from './decision.ts'
25
+
26
+ type WriteHeadFn = ServerResponse['writeHead']
27
+
28
+ let originalWriteHead: WriteHeadFn | undefined
29
+ let installs = 0
30
+
31
+ /** 取 writeHead 可变参末位的头对象(不存在则 undefined;数组形态不支持也返回 undefined)。 */
32
+ function headersObjectOf(args: unknown[]): Record<string, unknown> | undefined {
33
+ if (args.length < 2) return undefined
34
+ const last = args[args.length - 1]
35
+ if (last !== null && typeof last === 'object' && !Array.isArray(last)) {
36
+ return last as Record<string, unknown>
37
+ }
38
+ return undefined
39
+ }
40
+
41
+ /** 头对象内是否已声明 cache-control(HTTP 头名大小写不敏感)。 */
42
+ function objectHasCacheControl(headers: Record<string, unknown> | undefined): boolean {
43
+ if (headers === undefined) return false
44
+ return Object.keys(headers).some((name) => name.toLowerCase() === 'cache-control')
45
+ }
46
+
47
+ /** 安装补丁(幂等,引用计数叠加)。 */
48
+ export function installImmutableAssetsPatch(): void {
49
+ installs += 1
50
+ if (originalWriteHead !== undefined) return
51
+ originalWriteHead = http.ServerResponse.prototype.writeHead
52
+ const original = originalWriteHead
53
+
54
+ const patched = function (this: ServerResponse, ...args: unknown[]) {
55
+ try {
56
+ const statusCode = typeof args[0] === 'number' ? args[0] : 0
57
+ const method = this.req?.method ?? ''
58
+ const url = this.req?.url ?? ''
59
+ const hasCacheControl =
60
+ this.getHeader('cache-control') !== undefined ||
61
+ objectHasCacheControl(headersObjectOf(args))
62
+ const header = decideCacheControl(method, url, statusCode, hasCacheControl)
63
+ if (header !== undefined) this.setHeader('cache-control', header)
64
+ } catch {
65
+ // 决策失败绝不阻断响应写入(补丁位于所有 HTTP 响应的关键路径)。
66
+ }
67
+ return (original as unknown as (...inner: unknown[]) => unknown).apply(this, args)
68
+ }
69
+
70
+ http.ServerResponse.prototype.writeHead = patched as unknown as WriteHeadFn
71
+ }
72
+
73
+ /** 卸下补丁(引用计数归零才还原原型;超额调用安全)。 */
74
+ export function uninstallImmutableAssetsPatch(): void {
75
+ if (installs > 0) installs -= 1
76
+ if (installs > 0 || originalWriteHead === undefined) return
77
+ http.ServerResponse.prototype.writeHead = originalWriteHead
78
+ originalWriteHead = undefined
79
+ }