@heyhru/web-util-http 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ import type { HttpClientConfig, MessageFns, RequestOptions } from "./types";
2
+ /**
3
+ * HTTP 客户端类
4
+ * 提供基于 Protobuf 的 HTTP 请求功能
5
+ */
6
+ export declare class HttpClient {
7
+ private config;
8
+ private httpService;
9
+ private logger;
10
+ private monitor?;
11
+ private memoryCache;
12
+ private defaultTtl;
13
+ constructor(config: HttpClientConfig);
14
+ /**
15
+ * 设置请求拦截器
16
+ */
17
+ private setupRequestInterceptor;
18
+ /**
19
+ * 设置响应拦截器
20
+ */
21
+ private setupResponseInterceptor;
22
+ /**
23
+ * 构建客户端消息
24
+ */
25
+ private clientMessage;
26
+ /**
27
+ * 从类型字符串中提取名称(点号后的部分)
28
+ */
29
+ private getTypeAfterDot;
30
+ /**
31
+ * 发送 POST 请求(基础方法)
32
+ */
33
+ requestPost<T, U>(requestType: MessageFns<T>, requestData: T, responseType: MessageFns<U>, requestName: string, options?: RequestOptions): Promise<U>;
34
+ /**
35
+ * 发送 POST 请求(自动提取请求名称)
36
+ */
37
+ requestPost2<T, U>(requestType: MessageFns<T>, requestData: T, responseType: MessageFns<U>, options?: RequestOptions): Promise<U>;
38
+ /**
39
+ * 设置缓存
40
+ */
41
+ setCache<T>(key: string, data: T, ttl?: number): void;
42
+ /**
43
+ * 获取缓存
44
+ */
45
+ getCache<T>(key: string): T | null;
46
+ /**
47
+ * 清除缓存
48
+ */
49
+ clearCache(key?: string): void;
50
+ /**
51
+ * 构建缓存 key
52
+ */
53
+ private buildCacheKey;
54
+ /**
55
+ * 发送 POST 请求(带缓存)
56
+ */
57
+ requestPostCache<T, U>(requestType: MessageFns<T>, requestData: T, responseType: MessageFns<U>, options?: RequestOptions): Promise<U>;
58
+ /**
59
+ * 更新配置
60
+ */
61
+ updateConfig(config: Partial<HttpClientConfig>): void;
62
+ /**
63
+ * 获取当前配置
64
+ */
65
+ getConfig(): Readonly<HttpClientConfig>;
66
+ }
67
+ //# sourceMappingURL=HttpClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpClient.d.ts","sourceRoot":"","sources":["../src/HttpClient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,cAAc,EAA8C,MAAM,SAAS,CAAC;AAoBxH;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,OAAO,CAAC,CAAiB;IACjC,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,UAAU,CAAiB;gBAEvB,MAAM,EAAE,gBAAgB;IA8BpC;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAyB/B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAoFhC;;OAEG;IACH,OAAO,CAAC,aAAa;IAWrB;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;OAEG;IACG,WAAW,CAAC,CAAC,EAAE,CAAC,EACpB,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,CAAC,EACd,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC,EAC3B,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,CAAC,CAAC;IAsFb;;OAEG;IACG,YAAY,CAAC,CAAC,EAAE,CAAC,EACrB,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,CAAC,EACd,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC,EAC3B,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,CAAC,CAAC;IAKb;;OAEG;IACH,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,SAAkB,GAAG,IAAI;IAI9D;;OAEG;IACH,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI;IAUlC;;OAEG;IACH,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAQ9B;;OAEG;IACH,OAAO,CAAC,aAAa;IAmBrB;;OAEG;IACG,gBAAgB,CAAC,CAAC,EAAE,CAAC,EACzB,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,CAAC,EACd,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC,EAC3B,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,CAAC,CAAC;IAeb;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI;IAIrD;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,gBAAgB,CAAC;CAGxC"}
@@ -0,0 +1,3 @@
1
+ export { HttpClient } from "./HttpClient";
2
+ export type { HttpClientConfig, MessageFns, RequestOptions, UserInfo, MonitorAdapter, LoggerAdapter, CacheRecord, } from "./types";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EACV,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,QAAQ,EACR,cAAc,EACd,aAAa,EACb,WAAW,GACZ,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ HttpClient: () => HttpClient
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/HttpClient.ts
38
+ var import_axios = __toESM(require("axios"));
39
+ var DefaultLogger = class {
40
+ log(...args) {
41
+ console.log(...args);
42
+ }
43
+ error(...args) {
44
+ console.error(...args);
45
+ }
46
+ warn(...args) {
47
+ console.warn(...args);
48
+ }
49
+ info(...args) {
50
+ console.info(...args);
51
+ }
52
+ };
53
+ var HttpClient = class {
54
+ // 5 分钟
55
+ constructor(config) {
56
+ this.memoryCache = /* @__PURE__ */ new Map();
57
+ this.defaultTtl = 5 * 60 * 1e3;
58
+ this.config = {
59
+ timeout: 45e3,
60
+ slowApiThreshold: 5e3,
61
+ businessErrorCodesToReport: [2, 4, 99, 120],
62
+ athenaAllowedRequests: [],
63
+ noLoginPaths: [],
64
+ ...config
65
+ };
66
+ this.logger = config.loggerAdapter || new DefaultLogger();
67
+ this.monitor = config.monitorAdapter;
68
+ this.httpService = import_axios.default.create({
69
+ timeout: this.config.timeout,
70
+ method: "post",
71
+ headers: {
72
+ "X-Requested-With": "XMLHttpRequest",
73
+ "Content-Type": "application/octet-stream"
74
+ },
75
+ responseType: "arraybuffer"
76
+ });
77
+ this.setupRequestInterceptor();
78
+ this.setupResponseInterceptor();
79
+ }
80
+ /**
81
+ * 设置请求拦截器
82
+ */
83
+ setupRequestInterceptor() {
84
+ this.httpService.interceptors.request.use(
85
+ (config) => {
86
+ config.headers.Authorization = `Bearer ${this.config.getToken()}`;
87
+ config.headers.params = JSON.stringify({
88
+ os: this.config.getOs(),
89
+ app_name: this.config.getAppName(),
90
+ locale: this.config.getLocale(),
91
+ app_version: this.config.getAppVersion()
92
+ });
93
+ config.metadata = {
94
+ ...config.metadata || {},
95
+ requestTimestamp: Date.now(),
96
+ requestTime: (/* @__PURE__ */ new Date()).toISOString()
97
+ };
98
+ return config;
99
+ },
100
+ (error) => Promise.reject(error)
101
+ );
102
+ }
103
+ /**
104
+ * 设置响应拦截器
105
+ */
106
+ setupResponseInterceptor() {
107
+ this.httpService.interceptors.response.use(
108
+ (response) => {
109
+ const metadata = response.config?.metadata || {};
110
+ const requestTimestamp = metadata.requestTimestamp || Date.now();
111
+ const duration = Date.now() - requestTimestamp;
112
+ if (this.monitor && duration > (this.config.slowApiThreshold || 5e3)) {
113
+ const userInfo = this.monitor.getUserInfo();
114
+ this.monitor.reportSlowApi({
115
+ requestName: metadata.requestName || "unknown",
116
+ protoId: metadata.protoId || "unknown",
117
+ requestTime: metadata.requestTime || (/* @__PURE__ */ new Date()).toISOString(),
118
+ requestTimestamp,
119
+ duration,
120
+ requestParams: metadata.requestParams || "unavailable",
121
+ userId: userInfo.userId,
122
+ username: userInfo.username
123
+ });
124
+ }
125
+ return response;
126
+ },
127
+ async (error) => {
128
+ const requestUrl = error.config?.url || "unknown";
129
+ const requestMethod = error.config?.method || "unknown";
130
+ const statusCode = error.response?.status || 0;
131
+ const metadata = error.config?.metadata || {};
132
+ const requestTimestamp = metadata.requestTimestamp || Date.now();
133
+ const duration = Date.now() - requestTimestamp;
134
+ let responseData = "unavailable";
135
+ try {
136
+ if (error.response?.data) {
137
+ if (error.response.data instanceof ArrayBuffer) {
138
+ const decoder = new TextDecoder();
139
+ responseData = decoder.decode(error.response.data).substring(0, 500);
140
+ } else {
141
+ responseData = JSON.stringify(error.response.data).substring(0, 500);
142
+ }
143
+ }
144
+ } catch (e) {
145
+ responseData = "parse_failed";
146
+ }
147
+ if (this.monitor && statusCode !== 401) {
148
+ const userInfo = this.monitor.getUserInfo();
149
+ this.monitor.reportApiError({
150
+ error,
151
+ requestUrl,
152
+ requestMethod,
153
+ statusCode,
154
+ requestName: metadata.requestName || "unknown",
155
+ protoId: metadata.protoId || "unknown",
156
+ requestTime: metadata.requestTime || (/* @__PURE__ */ new Date()).toISOString(),
157
+ requestTimestamp,
158
+ duration,
159
+ requestParams: metadata.requestParams || "unavailable",
160
+ responseData,
161
+ userId: userInfo.userId,
162
+ username: userInfo.username
163
+ });
164
+ }
165
+ if (statusCode === 401) {
166
+ const pathname = typeof window !== "undefined" ? window.location.pathname || "" : "";
167
+ const noLoginPaths = this.config.noLoginPaths || [];
168
+ if (noLoginPaths.includes(pathname)) {
169
+ return Promise.reject(error);
170
+ }
171
+ if (this.config.on401Error) {
172
+ await this.config.on401Error();
173
+ }
174
+ }
175
+ return Promise.reject(error);
176
+ }
177
+ );
178
+ }
179
+ /**
180
+ * 构建客户端消息
181
+ */
182
+ clientMessage(protoId, msgBuffer) {
183
+ const len = 4 + msgBuffer.length;
184
+ const buffer = new ArrayBuffer(len);
185
+ const view = new DataView(buffer);
186
+ view.setInt32(0, protoId, false);
187
+ msgBuffer.forEach((byte, index) => {
188
+ view.setUint8(4 + index, byte);
189
+ });
190
+ return buffer;
191
+ }
192
+ /**
193
+ * 从类型字符串中提取名称(点号后的部分)
194
+ */
195
+ getTypeAfterDot(type) {
196
+ const dotIndex = type.lastIndexOf(".");
197
+ return dotIndex === -1 ? type : type.substring(dotIndex + 1);
198
+ }
199
+ /**
200
+ * 发送 POST 请求(基础方法)
201
+ */
202
+ async requestPost(requestType, requestData, responseType, requestName, options) {
203
+ const protoId = this.config.protoMap[requestName];
204
+ if (!protoId) {
205
+ throw new Error(`Proto ID not found for ${requestName}`);
206
+ }
207
+ this.logger.log("protoId", protoId);
208
+ const isAthena = this.config.isAthenaEnv?.() || false;
209
+ const athenaAllowed = this.config.athenaAllowedRequests || [];
210
+ if (isAthena && !athenaAllowed.includes(requestName)) {
211
+ return {};
212
+ }
213
+ try {
214
+ const requestMessage = requestType.create(requestData);
215
+ const encodedMessage = requestType.encode(requestMessage).finish();
216
+ const reqMessage = this.clientMessage(protoId, encodedMessage);
217
+ let requestParamsStr = "unavailable";
218
+ try {
219
+ requestParamsStr = JSON.stringify(requestData).substring(0, 1e3);
220
+ } catch (e) {
221
+ requestParamsStr = "serialize_failed";
222
+ }
223
+ const response = await this.httpService.post(this.config.apiHost, reqMessage, {
224
+ signal: options?.signal,
225
+ timeout: options?.timeout ?? this.config.timeout,
226
+ metadata: {
227
+ protoId,
228
+ requestName,
229
+ requestParams: requestParamsStr
230
+ }
231
+ });
232
+ const buf = response.data;
233
+ const realProto = buf.slice(4);
234
+ const uint8Array = new Uint8Array(realProto);
235
+ const responseProto = responseType.decode(uint8Array);
236
+ this.logger.log("HttpClient", requestName, requestData, responseProto);
237
+ if (responseProto.code !== void 0) {
238
+ const businessCode = responseProto.code;
239
+ const errorCodesToReport = this.config.businessErrorCodesToReport || [];
240
+ if (businessCode !== 1 && businessCode !== 0) {
241
+ if (this.monitor && errorCodesToReport.includes(businessCode)) {
242
+ const errorMessage = responseProto.message || responseProto.msg || "unknown";
243
+ let responseStr = "unavailable";
244
+ try {
245
+ responseStr = JSON.stringify(responseProto).substring(0, 1e3);
246
+ } catch (e) {
247
+ responseStr = "serialize_failed";
248
+ }
249
+ const userInfo = this.monitor.getUserInfo();
250
+ this.monitor.reportBusinessError({
251
+ businessCode,
252
+ requestName,
253
+ protoId,
254
+ requestParams: requestParamsStr,
255
+ errorMessage,
256
+ responseData: responseStr,
257
+ userId: userInfo.userId,
258
+ username: userInfo.username
259
+ });
260
+ }
261
+ }
262
+ }
263
+ return responseProto;
264
+ } catch (e) {
265
+ this.logger.error("HttpClient", `${requestName} request error:`, e);
266
+ throw e;
267
+ }
268
+ }
269
+ /**
270
+ * 发送 POST 请求(自动提取请求名称)
271
+ */
272
+ async requestPost2(requestType, requestData, responseType, options) {
273
+ const requestName = this.getTypeAfterDot(requestType.$type);
274
+ return this.requestPost(requestType, requestData, responseType, requestName, options);
275
+ }
276
+ /**
277
+ * 设置缓存
278
+ */
279
+ setCache(key, data, ttl = this.defaultTtl) {
280
+ this.memoryCache.set(key, { data, expiry: Date.now() + ttl });
281
+ }
282
+ /**
283
+ * 获取缓存
284
+ */
285
+ getCache(key) {
286
+ const record = this.memoryCache.get(key);
287
+ if (!record) return null;
288
+ if (Date.now() > record.expiry) {
289
+ this.memoryCache.delete(key);
290
+ return null;
291
+ }
292
+ return record.data;
293
+ }
294
+ /**
295
+ * 清除缓存
296
+ */
297
+ clearCache(key) {
298
+ if (key) {
299
+ this.memoryCache.delete(key);
300
+ } else {
301
+ this.memoryCache.clear();
302
+ }
303
+ }
304
+ /**
305
+ * 构建缓存 key
306
+ */
307
+ buildCacheKey(requestType, requestData) {
308
+ const typeName = this.getTypeAfterDot(requestType.$type);
309
+ try {
310
+ const message = requestType.create(requestData);
311
+ const bytes = requestType.encode(message).finish();
312
+ const payloadKey = Array.from(bytes).join(",");
313
+ return `${typeName}:${payloadKey}`;
314
+ } catch (e) {
315
+ const stableStringify = (value) => {
316
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
317
+ if (Array.isArray(value)) return `[${value.map((v) => stableStringify(v)).join(",")}]`;
318
+ const keys = Object.keys(value).sort();
319
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
320
+ };
321
+ return `${typeName}:${stableStringify(requestData)}`;
322
+ }
323
+ }
324
+ /**
325
+ * 发送 POST 请求(带缓存)
326
+ */
327
+ async requestPostCache(requestType, requestData, responseType, options) {
328
+ const cacheKey = this.buildCacheKey(requestType, requestData);
329
+ const cached = this.getCache(cacheKey);
330
+ if (cached) {
331
+ this.logger.log("HttpClient", `requestPostCache hit: ${cacheKey}`);
332
+ return cached;
333
+ }
334
+ const result = await this.requestPost2(requestType, requestData, responseType, options);
335
+ this.setCache(cacheKey, result, this.defaultTtl);
336
+ this.logger.log("HttpClient", `requestPostCache set: ${cacheKey}`);
337
+ return result;
338
+ }
339
+ /**
340
+ * 更新配置
341
+ */
342
+ updateConfig(config) {
343
+ this.config = { ...this.config, ...config };
344
+ }
345
+ /**
346
+ * 获取当前配置
347
+ */
348
+ getConfig() {
349
+ return { ...this.config };
350
+ }
351
+ };
352
+ // Annotate the CommonJS export names for ESM import in node:
353
+ 0 && (module.exports = {
354
+ HttpClient
355
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,318 @@
1
+ // src/HttpClient.ts
2
+ import axios from "axios";
3
+ var DefaultLogger = class {
4
+ log(...args) {
5
+ console.log(...args);
6
+ }
7
+ error(...args) {
8
+ console.error(...args);
9
+ }
10
+ warn(...args) {
11
+ console.warn(...args);
12
+ }
13
+ info(...args) {
14
+ console.info(...args);
15
+ }
16
+ };
17
+ var HttpClient = class {
18
+ // 5 分钟
19
+ constructor(config) {
20
+ this.memoryCache = /* @__PURE__ */ new Map();
21
+ this.defaultTtl = 5 * 60 * 1e3;
22
+ this.config = {
23
+ timeout: 45e3,
24
+ slowApiThreshold: 5e3,
25
+ businessErrorCodesToReport: [2, 4, 99, 120],
26
+ athenaAllowedRequests: [],
27
+ noLoginPaths: [],
28
+ ...config
29
+ };
30
+ this.logger = config.loggerAdapter || new DefaultLogger();
31
+ this.monitor = config.monitorAdapter;
32
+ this.httpService = axios.create({
33
+ timeout: this.config.timeout,
34
+ method: "post",
35
+ headers: {
36
+ "X-Requested-With": "XMLHttpRequest",
37
+ "Content-Type": "application/octet-stream"
38
+ },
39
+ responseType: "arraybuffer"
40
+ });
41
+ this.setupRequestInterceptor();
42
+ this.setupResponseInterceptor();
43
+ }
44
+ /**
45
+ * 设置请求拦截器
46
+ */
47
+ setupRequestInterceptor() {
48
+ this.httpService.interceptors.request.use(
49
+ (config) => {
50
+ config.headers.Authorization = `Bearer ${this.config.getToken()}`;
51
+ config.headers.params = JSON.stringify({
52
+ os: this.config.getOs(),
53
+ app_name: this.config.getAppName(),
54
+ locale: this.config.getLocale(),
55
+ app_version: this.config.getAppVersion()
56
+ });
57
+ config.metadata = {
58
+ ...config.metadata || {},
59
+ requestTimestamp: Date.now(),
60
+ requestTime: (/* @__PURE__ */ new Date()).toISOString()
61
+ };
62
+ return config;
63
+ },
64
+ (error) => Promise.reject(error)
65
+ );
66
+ }
67
+ /**
68
+ * 设置响应拦截器
69
+ */
70
+ setupResponseInterceptor() {
71
+ this.httpService.interceptors.response.use(
72
+ (response) => {
73
+ const metadata = response.config?.metadata || {};
74
+ const requestTimestamp = metadata.requestTimestamp || Date.now();
75
+ const duration = Date.now() - requestTimestamp;
76
+ if (this.monitor && duration > (this.config.slowApiThreshold || 5e3)) {
77
+ const userInfo = this.monitor.getUserInfo();
78
+ this.monitor.reportSlowApi({
79
+ requestName: metadata.requestName || "unknown",
80
+ protoId: metadata.protoId || "unknown",
81
+ requestTime: metadata.requestTime || (/* @__PURE__ */ new Date()).toISOString(),
82
+ requestTimestamp,
83
+ duration,
84
+ requestParams: metadata.requestParams || "unavailable",
85
+ userId: userInfo.userId,
86
+ username: userInfo.username
87
+ });
88
+ }
89
+ return response;
90
+ },
91
+ async (error) => {
92
+ const requestUrl = error.config?.url || "unknown";
93
+ const requestMethod = error.config?.method || "unknown";
94
+ const statusCode = error.response?.status || 0;
95
+ const metadata = error.config?.metadata || {};
96
+ const requestTimestamp = metadata.requestTimestamp || Date.now();
97
+ const duration = Date.now() - requestTimestamp;
98
+ let responseData = "unavailable";
99
+ try {
100
+ if (error.response?.data) {
101
+ if (error.response.data instanceof ArrayBuffer) {
102
+ const decoder = new TextDecoder();
103
+ responseData = decoder.decode(error.response.data).substring(0, 500);
104
+ } else {
105
+ responseData = JSON.stringify(error.response.data).substring(0, 500);
106
+ }
107
+ }
108
+ } catch (e) {
109
+ responseData = "parse_failed";
110
+ }
111
+ if (this.monitor && statusCode !== 401) {
112
+ const userInfo = this.monitor.getUserInfo();
113
+ this.monitor.reportApiError({
114
+ error,
115
+ requestUrl,
116
+ requestMethod,
117
+ statusCode,
118
+ requestName: metadata.requestName || "unknown",
119
+ protoId: metadata.protoId || "unknown",
120
+ requestTime: metadata.requestTime || (/* @__PURE__ */ new Date()).toISOString(),
121
+ requestTimestamp,
122
+ duration,
123
+ requestParams: metadata.requestParams || "unavailable",
124
+ responseData,
125
+ userId: userInfo.userId,
126
+ username: userInfo.username
127
+ });
128
+ }
129
+ if (statusCode === 401) {
130
+ const pathname = typeof window !== "undefined" ? window.location.pathname || "" : "";
131
+ const noLoginPaths = this.config.noLoginPaths || [];
132
+ if (noLoginPaths.includes(pathname)) {
133
+ return Promise.reject(error);
134
+ }
135
+ if (this.config.on401Error) {
136
+ await this.config.on401Error();
137
+ }
138
+ }
139
+ return Promise.reject(error);
140
+ }
141
+ );
142
+ }
143
+ /**
144
+ * 构建客户端消息
145
+ */
146
+ clientMessage(protoId, msgBuffer) {
147
+ const len = 4 + msgBuffer.length;
148
+ const buffer = new ArrayBuffer(len);
149
+ const view = new DataView(buffer);
150
+ view.setInt32(0, protoId, false);
151
+ msgBuffer.forEach((byte, index) => {
152
+ view.setUint8(4 + index, byte);
153
+ });
154
+ return buffer;
155
+ }
156
+ /**
157
+ * 从类型字符串中提取名称(点号后的部分)
158
+ */
159
+ getTypeAfterDot(type) {
160
+ const dotIndex = type.lastIndexOf(".");
161
+ return dotIndex === -1 ? type : type.substring(dotIndex + 1);
162
+ }
163
+ /**
164
+ * 发送 POST 请求(基础方法)
165
+ */
166
+ async requestPost(requestType, requestData, responseType, requestName, options) {
167
+ const protoId = this.config.protoMap[requestName];
168
+ if (!protoId) {
169
+ throw new Error(`Proto ID not found for ${requestName}`);
170
+ }
171
+ this.logger.log("protoId", protoId);
172
+ const isAthena = this.config.isAthenaEnv?.() || false;
173
+ const athenaAllowed = this.config.athenaAllowedRequests || [];
174
+ if (isAthena && !athenaAllowed.includes(requestName)) {
175
+ return {};
176
+ }
177
+ try {
178
+ const requestMessage = requestType.create(requestData);
179
+ const encodedMessage = requestType.encode(requestMessage).finish();
180
+ const reqMessage = this.clientMessage(protoId, encodedMessage);
181
+ let requestParamsStr = "unavailable";
182
+ try {
183
+ requestParamsStr = JSON.stringify(requestData).substring(0, 1e3);
184
+ } catch (e) {
185
+ requestParamsStr = "serialize_failed";
186
+ }
187
+ const response = await this.httpService.post(this.config.apiHost, reqMessage, {
188
+ signal: options?.signal,
189
+ timeout: options?.timeout ?? this.config.timeout,
190
+ metadata: {
191
+ protoId,
192
+ requestName,
193
+ requestParams: requestParamsStr
194
+ }
195
+ });
196
+ const buf = response.data;
197
+ const realProto = buf.slice(4);
198
+ const uint8Array = new Uint8Array(realProto);
199
+ const responseProto = responseType.decode(uint8Array);
200
+ this.logger.log("HttpClient", requestName, requestData, responseProto);
201
+ if (responseProto.code !== void 0) {
202
+ const businessCode = responseProto.code;
203
+ const errorCodesToReport = this.config.businessErrorCodesToReport || [];
204
+ if (businessCode !== 1 && businessCode !== 0) {
205
+ if (this.monitor && errorCodesToReport.includes(businessCode)) {
206
+ const errorMessage = responseProto.message || responseProto.msg || "unknown";
207
+ let responseStr = "unavailable";
208
+ try {
209
+ responseStr = JSON.stringify(responseProto).substring(0, 1e3);
210
+ } catch (e) {
211
+ responseStr = "serialize_failed";
212
+ }
213
+ const userInfo = this.monitor.getUserInfo();
214
+ this.monitor.reportBusinessError({
215
+ businessCode,
216
+ requestName,
217
+ protoId,
218
+ requestParams: requestParamsStr,
219
+ errorMessage,
220
+ responseData: responseStr,
221
+ userId: userInfo.userId,
222
+ username: userInfo.username
223
+ });
224
+ }
225
+ }
226
+ }
227
+ return responseProto;
228
+ } catch (e) {
229
+ this.logger.error("HttpClient", `${requestName} request error:`, e);
230
+ throw e;
231
+ }
232
+ }
233
+ /**
234
+ * 发送 POST 请求(自动提取请求名称)
235
+ */
236
+ async requestPost2(requestType, requestData, responseType, options) {
237
+ const requestName = this.getTypeAfterDot(requestType.$type);
238
+ return this.requestPost(requestType, requestData, responseType, requestName, options);
239
+ }
240
+ /**
241
+ * 设置缓存
242
+ */
243
+ setCache(key, data, ttl = this.defaultTtl) {
244
+ this.memoryCache.set(key, { data, expiry: Date.now() + ttl });
245
+ }
246
+ /**
247
+ * 获取缓存
248
+ */
249
+ getCache(key) {
250
+ const record = this.memoryCache.get(key);
251
+ if (!record) return null;
252
+ if (Date.now() > record.expiry) {
253
+ this.memoryCache.delete(key);
254
+ return null;
255
+ }
256
+ return record.data;
257
+ }
258
+ /**
259
+ * 清除缓存
260
+ */
261
+ clearCache(key) {
262
+ if (key) {
263
+ this.memoryCache.delete(key);
264
+ } else {
265
+ this.memoryCache.clear();
266
+ }
267
+ }
268
+ /**
269
+ * 构建缓存 key
270
+ */
271
+ buildCacheKey(requestType, requestData) {
272
+ const typeName = this.getTypeAfterDot(requestType.$type);
273
+ try {
274
+ const message = requestType.create(requestData);
275
+ const bytes = requestType.encode(message).finish();
276
+ const payloadKey = Array.from(bytes).join(",");
277
+ return `${typeName}:${payloadKey}`;
278
+ } catch (e) {
279
+ const stableStringify = (value) => {
280
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
281
+ if (Array.isArray(value)) return `[${value.map((v) => stableStringify(v)).join(",")}]`;
282
+ const keys = Object.keys(value).sort();
283
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
284
+ };
285
+ return `${typeName}:${stableStringify(requestData)}`;
286
+ }
287
+ }
288
+ /**
289
+ * 发送 POST 请求(带缓存)
290
+ */
291
+ async requestPostCache(requestType, requestData, responseType, options) {
292
+ const cacheKey = this.buildCacheKey(requestType, requestData);
293
+ const cached = this.getCache(cacheKey);
294
+ if (cached) {
295
+ this.logger.log("HttpClient", `requestPostCache hit: ${cacheKey}`);
296
+ return cached;
297
+ }
298
+ const result = await this.requestPost2(requestType, requestData, responseType, options);
299
+ this.setCache(cacheKey, result, this.defaultTtl);
300
+ this.logger.log("HttpClient", `requestPostCache set: ${cacheKey}`);
301
+ return result;
302
+ }
303
+ /**
304
+ * 更新配置
305
+ */
306
+ updateConfig(config) {
307
+ this.config = { ...this.config, ...config };
308
+ }
309
+ /**
310
+ * 获取当前配置
311
+ */
312
+ getConfig() {
313
+ return { ...this.config };
314
+ }
315
+ };
316
+ export {
317
+ HttpClient
318
+ };
@@ -0,0 +1,138 @@
1
+ import { AxiosError } from "axios";
2
+ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
3
+ import { DeepPartial, Exact } from "@heyhru/business-pwa-proto/baseType";
4
+ /**
5
+ * MessageFns 接口定义(与 protobuf 生成的类型兼容)
6
+ */
7
+ export interface MessageFns<T, Type extends string = string> {
8
+ $type: string;
9
+ encode(message: T, writer?: BinaryWriter): BinaryWriter;
10
+ decode(input: BinaryReader | Uint8Array, length?: number): T;
11
+ fromJSON(object: any): T;
12
+ toJSON(message: T): unknown;
13
+ create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
14
+ fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
15
+ }
16
+ /**
17
+ * HTTP 客户端配置
18
+ */
19
+ export interface HttpClientConfig {
20
+ /** API 主机地址 */
21
+ apiHost: string;
22
+ /** 请求超时时间(毫秒),默认 45000 */
23
+ timeout?: number;
24
+ /** 获取授权 token */
25
+ getToken: () => string;
26
+ /** 获取操作系统信息 */
27
+ getOs: () => string;
28
+ /** 获取应用名称 */
29
+ getAppName: () => string;
30
+ /** 获取语言设置 */
31
+ getLocale: () => string;
32
+ /** 获取应用版本 */
33
+ getAppVersion: () => string;
34
+ /** Proto ID 映射表 */
35
+ protoMap: Record<string, number>;
36
+ /** 可选的监控适配器 */
37
+ monitorAdapter?: MonitorAdapter;
38
+ /** 可选的日志适配器 */
39
+ loggerAdapter?: LoggerAdapter;
40
+ /** 可选的环境检查函数 */
41
+ isAthenaEnv?: () => boolean;
42
+ /** 雅典娜环境允许的请求列表 */
43
+ athenaAllowedRequests?: string[];
44
+ /** 需要上报的业务错误码白名单 */
45
+ businessErrorCodesToReport?: number[];
46
+ /** 401 错误时的处理回调 */
47
+ on401Error?: () => void | Promise<void>;
48
+ /** 不需要登录的路径列表 */
49
+ noLoginPaths?: string[];
50
+ /** 慢接口阈值(毫秒),默认 5000 */
51
+ slowApiThreshold?: number;
52
+ }
53
+ /**
54
+ * 用户信息接口
55
+ */
56
+ export interface UserInfo {
57
+ userId: string | number;
58
+ username?: string;
59
+ }
60
+ /**
61
+ * 监控适配器接口(用于错误上报,如 Sentry)
62
+ */
63
+ export interface MonitorAdapter {
64
+ /**
65
+ * 获取用户信息
66
+ */
67
+ getUserInfo(): UserInfo;
68
+ /**
69
+ * 上报慢接口
70
+ */
71
+ reportSlowApi(params: {
72
+ requestName: string;
73
+ protoId: number | string;
74
+ requestTime: string;
75
+ requestTimestamp: number;
76
+ duration: number;
77
+ requestParams: string;
78
+ userId: string | number;
79
+ username?: string;
80
+ }): void;
81
+ /**
82
+ * 上报 API 错误
83
+ */
84
+ reportApiError(params: {
85
+ error: AxiosError;
86
+ requestUrl: string;
87
+ requestMethod: string;
88
+ statusCode: number;
89
+ requestName: string;
90
+ protoId: number | string;
91
+ requestTime: string;
92
+ requestTimestamp: number;
93
+ duration: number;
94
+ requestParams: string;
95
+ responseData: string;
96
+ userId: string | number;
97
+ username?: string;
98
+ }): void;
99
+ /**
100
+ * 上报业务错误
101
+ */
102
+ reportBusinessError(params: {
103
+ businessCode: number;
104
+ requestName: string;
105
+ protoId: number | string;
106
+ requestParams: string;
107
+ errorMessage: string;
108
+ responseData: string;
109
+ userId: string | number;
110
+ username?: string;
111
+ }): void;
112
+ }
113
+ /**
114
+ * 日志适配器接口
115
+ */
116
+ export interface LoggerAdapter {
117
+ log(...args: any[]): void;
118
+ error(...args: any[]): void;
119
+ warn(...args: any[]): void;
120
+ info(...args: any[]): void;
121
+ }
122
+ /**
123
+ * 请求选项
124
+ */
125
+ export interface RequestOptions {
126
+ /** 取消信号 */
127
+ signal?: AbortSignal;
128
+ /** 超时时间(毫秒) */
129
+ timeout?: number;
130
+ }
131
+ /**
132
+ * 缓存记录
133
+ */
134
+ export interface CacheRecord<T> {
135
+ data: T;
136
+ expiry: number;
137
+ }
138
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@heyhru/web-util-http",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.1.1",
7
+ "description": "HTTP client with protobuf encoding, caching, and monitoring adapters",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup && tsc --emitDeclarationOnly",
23
+ "dev": "tsup --watch",
24
+ "lint1": "eslint src",
25
+ "clean": "rm -rf dist"
26
+ },
27
+ "dependencies": {
28
+ "@heyhru/business-pwa-proto": "^0.1.1",
29
+ "@heyhru/web-util-ua": "^0.1.1",
30
+ "axios": "^1.15.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@bufbuild/protobuf": "^2.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@bufbuild/protobuf": "^2.2.2",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "^6.0.2"
39
+ },
40
+ "gitHead": "bea932bfa6fd932a7a27a305d74b4b01c2e4ebe3"
41
+ }