taro-uno-ui 0.9.0 → 1.0.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.
Files changed (312) hide show
  1. package/README.md +21 -0
  2. package/dist/js/{index-DffLRSro.js → index-CDFsvu80.js} +15369 -10741
  3. package/dist/js/index-CDFsvu80.js.map +1 -0
  4. package/dist/js/index-DFdcksbe.js.map +1 -1
  5. package/dist/js/index-DXRIkWX1.js.map +1 -1
  6. package/dist/js/{index-6NJ3A1Dn.js → index-JffnTUrv.js} +15430 -10801
  7. package/dist/js/index-JffnTUrv.js.map +1 -0
  8. package/dist/utils/http/request.d.ts +280 -0
  9. package/package.json +14 -10
  10. package/src/components/basic/Button/Button.tsx +53 -13
  11. package/src/components/basic/Button/Button.types.ts +45 -9
  12. package/src/components/basic/Divider/Divider.tsx +60 -29
  13. package/src/components/basic/Icon/Icon.data.ts +474 -0
  14. package/src/components/basic/Icon/Icon.test.tsx +2 -2
  15. package/src/components/basic/Icon/Icon.tsx +48 -35
  16. package/src/components/basic/Icon/IconManager.ts +229 -0
  17. package/src/components/basic/Text/Text.styles.ts +3 -3
  18. package/src/components/basic/Text/Text.types.ts +14 -4
  19. package/src/components/basic/Typography/Typography.styles.ts +10 -9
  20. package/src/components/basic/Typography/Typography.tsx +15 -13
  21. package/src/components/basic/Typography/Typography.types.ts +41 -41
  22. package/src/components/basic/Typography/index.tsx +1 -1
  23. package/src/components/basic/Video/Video.styles.ts +777 -0
  24. package/src/components/basic/Video/Video.test.tsx +490 -0
  25. package/src/components/basic/Video/Video.tsx +1468 -0
  26. package/src/components/basic/Video/Video.types.ts +500 -0
  27. package/src/components/basic/Video/index.tsx +26 -0
  28. package/src/components/basic/index.tsx +13 -15
  29. package/src/components/common/ErrorBoundary.tsx +1 -1
  30. package/src/components/common/LazyComponent.tsx +9 -8
  31. package/src/components/common/SecurityProvider.tsx +2 -14
  32. package/src/components/common/ThemeProvider.tsx +43 -56
  33. package/src/components/common/VirtualList.tsx +187 -205
  34. package/src/components/common/index.tsx +25 -0
  35. package/src/components/display/Avatar/Avatar.styles.ts +1 -1
  36. package/src/components/display/Avatar/Avatar.tsx +6 -19
  37. package/src/components/display/Avatar/Avatar.types.ts +1 -1
  38. package/src/components/display/Avatar/index.ts +1 -1
  39. package/src/components/display/Badge/Badge.tsx +3 -16
  40. package/src/components/display/Badge/Badge.types.ts +1 -1
  41. package/src/components/display/Badge/index.ts +1 -1
  42. package/src/components/display/Calendar/Calendar.styles.ts +36 -36
  43. package/src/components/display/Calendar/Calendar.test.tsx +27 -15
  44. package/src/components/display/Calendar/Calendar.tsx +56 -35
  45. package/src/components/display/Calendar/Calendar.types.ts +1 -1
  46. package/src/components/display/Calendar/index.ts +1 -1
  47. package/src/components/display/Card/Card.styles.ts +2 -2
  48. package/src/components/display/Card/Card.test.tsx +6 -4
  49. package/src/components/display/Card/Card.tsx +1 -1
  50. package/src/components/display/Card/Card.types.ts +4 -4
  51. package/src/components/display/Card/index.ts +1 -1
  52. package/src/components/display/Carousel/Carousel.styles.ts +31 -31
  53. package/src/components/display/Carousel/Carousel.tsx +34 -39
  54. package/src/components/display/Carousel/Carousel.types.ts +1 -1
  55. package/src/components/display/Carousel/index.ts +1 -1
  56. package/src/components/display/List/List.styles.ts +3 -3
  57. package/src/components/display/List/List.tsx +0 -1
  58. package/src/components/display/List/index.ts +1 -1
  59. package/src/components/display/Rate/Rate.styles.ts +5 -17
  60. package/src/components/display/Rate/Rate.tsx +6 -14
  61. package/src/components/display/Rate/Rate.types.ts +4 -3
  62. package/src/components/display/Rate/index.ts +3 -11
  63. package/src/components/display/Table/Table.test.tsx +2 -0
  64. package/src/components/display/Table/Table.tsx +3 -7
  65. package/src/components/display/Table/Table.types.ts +3 -2
  66. package/src/components/display/Tag/Tag.styles.ts +31 -31
  67. package/src/components/display/Tag/Tag.tsx +9 -26
  68. package/src/components/display/Tag/Tag.types.ts +1 -1
  69. package/src/components/display/Tag/index.ts +1 -1
  70. package/src/components/display/Timeline/Timeline.styles.ts +32 -32
  71. package/src/components/display/Timeline/Timeline.tsx +23 -42
  72. package/src/components/display/Timeline/Timeline.types.ts +1 -1
  73. package/src/components/display/Timeline/index.ts +1 -1
  74. package/src/components/display/index.tsx +33 -29
  75. package/src/components/feedback/Loading/Loading.tsx +6 -1
  76. package/src/components/feedback/Loading/index.ts +2 -5
  77. package/src/components/feedback/Message/Message.styles.ts +3 -3
  78. package/src/components/feedback/Message/index.ts +2 -5
  79. package/src/components/feedback/Modal/Modal.styles.ts +1 -1
  80. package/src/components/feedback/Modal/Modal.tsx +9 -31
  81. package/src/components/feedback/Modal/Modal.types.ts +12 -2
  82. package/src/components/feedback/Notification/Notification.styles.ts +49 -39
  83. package/src/components/feedback/Notification/Notification.test.tsx +1 -1
  84. package/src/components/feedback/Notification/Notification.tsx +97 -120
  85. package/src/components/feedback/Notification/Notification.types.ts +11 -8
  86. package/src/components/feedback/Notification/NotificationManager.tsx +135 -106
  87. package/src/components/feedback/Notification/index.ts +10 -3
  88. package/src/components/feedback/Notification/index.tsx +16 -26
  89. package/src/components/feedback/Progress/Progress.styles.ts +23 -14
  90. package/src/components/feedback/Progress/Progress.tsx +93 -113
  91. package/src/components/feedback/Progress/Progress.types.ts +1 -1
  92. package/src/components/feedback/Progress/index.ts +1 -1
  93. package/src/components/feedback/Progress/utils/animation.ts +12 -23
  94. package/src/components/feedback/Progress/utils/index.ts +2 -2
  95. package/src/components/feedback/Progress/utils/progress-calculator.ts +14 -32
  96. package/src/components/feedback/Result/Result.styles.ts +29 -29
  97. package/src/components/feedback/Result/Result.tsx +8 -20
  98. package/src/components/feedback/Result/Result.types.ts +7 -7
  99. package/src/components/feedback/Result/index.tsx +1 -1
  100. package/src/components/feedback/Toast/Toast.styles.ts +1 -1
  101. package/src/components/feedback/Toast/Toast.tsx +25 -13
  102. package/src/components/feedback/Tooltip/Tooltip.examples.tsx +21 -44
  103. package/src/components/feedback/Tooltip/Tooltip.styles.ts +16 -22
  104. package/src/components/feedback/Tooltip/Tooltip.test.tsx +1 -1
  105. package/src/components/feedback/Tooltip/Tooltip.tsx +65 -46
  106. package/src/components/feedback/Tooltip/Tooltip.types.ts +14 -20
  107. package/src/components/feedback/Tooltip/index.ts +1 -1
  108. package/src/components/feedback/Tooltip/index.tsx +12 -24
  109. package/src/components/feedback/index.tsx +54 -42
  110. package/src/components/form/Cascader/Cascader.styles.ts +2 -2
  111. package/src/components/form/Cascader/Cascader.tsx +84 -88
  112. package/src/components/form/Cascader/Cascader.types.ts +49 -50
  113. package/src/components/form/Cascader/hooks/useCascaderFieldNames.ts +11 -8
  114. package/src/components/form/Cascader/hooks/useCascaderOptions.ts +73 -55
  115. package/src/components/form/Cascader/hooks/useCascaderState.ts +31 -25
  116. package/src/components/form/Cascader/index.ts +1 -1
  117. package/src/components/form/Cascader/utils/formatDisplayValue.ts +4 -4
  118. package/src/components/form/Checkbox/Checkbox.styles.ts +83 -84
  119. package/src/components/form/Checkbox/Checkbox.tsx +2 -9
  120. package/src/components/form/Checkbox/CheckboxGroup.tsx +7 -7
  121. package/src/components/form/DatePicker/DatePicker.test.tsx +1 -1
  122. package/src/components/form/DatePicker/DatePicker.tsx +91 -75
  123. package/src/components/form/DatePicker/DatePicker.types.ts +4 -1
  124. package/src/components/form/Form/Form.tsx +66 -504
  125. package/src/components/form/Form/Form.types.ts +16 -1
  126. package/src/components/form/Form/useFormLogic.ts +497 -0
  127. package/src/components/form/Input/Input.styles.ts +8 -1
  128. package/src/components/form/Input/Input.tsx +55 -291
  129. package/src/components/form/Input/Input.types.ts +13 -1
  130. package/src/components/form/Input/useInputLogic.test.ts +82 -0
  131. package/src/components/form/Input/useInputLogic.ts +260 -0
  132. package/src/components/form/InputNumber/InputNumber.styles.ts +76 -25
  133. package/src/components/form/InputNumber/InputNumber.tsx +53 -21
  134. package/src/components/form/InputNumber/InputNumber.types.ts +21 -3
  135. package/src/components/form/InputNumber/components/InputNumberClearButton.tsx +3 -11
  136. package/src/components/form/InputNumber/components/InputNumberControls.tsx +3 -12
  137. package/src/components/form/InputNumber/hooks/index.ts +1 -1
  138. package/src/components/form/InputNumber/hooks/useInputNumberState.ts +7 -9
  139. package/src/components/form/InputNumber/hooks/useInputNumberValidation.ts +18 -17
  140. package/src/components/form/InputNumber/index.ts +7 -7
  141. package/src/components/form/Radio/Radio.styles.ts +1 -8
  142. package/src/components/form/Radio/Radio.tsx +3 -9
  143. package/src/components/form/Radio/Radio.types.ts +5 -1
  144. package/src/components/form/Select/Select.styles.ts +5 -1
  145. package/src/components/form/Select/Select.tsx +15 -15
  146. package/src/components/form/Select/Select.types.ts +2 -1
  147. package/src/components/form/Slider/Slider.styles.ts +13 -13
  148. package/src/components/form/Slider/Slider.tsx +19 -33
  149. package/src/components/form/Slider/Slider.types.ts +14 -12
  150. package/src/components/form/Slider/index.tsx +2 -9
  151. package/src/components/form/Switch/Switch.styles.ts +1 -7
  152. package/src/components/form/Switch/Switch.tsx +7 -13
  153. package/src/components/form/Textarea/Textarea.styles.ts +4 -4
  154. package/src/components/form/Textarea/Textarea.tsx +7 -1
  155. package/src/components/form/Textarea/Textarea.types.ts +4 -1
  156. package/src/components/form/TimePicker/TimePicker.styles.ts +8 -12
  157. package/src/components/form/TimePicker/TimePicker.tsx +122 -100
  158. package/src/components/form/TimePicker/TimePicker.types.ts +2 -2
  159. package/src/components/form/TimePicker/index.ts +1 -1
  160. package/src/components/form/Transfer/Transfer.styles.ts +3 -15
  161. package/src/components/form/Transfer/Transfer.tsx +146 -134
  162. package/src/components/form/Transfer/Transfer.types.ts +34 -26
  163. package/src/components/form/Transfer/components/TransferItem.tsx +55 -62
  164. package/src/components/form/Transfer/components/TransferList.tsx +212 -199
  165. package/src/components/form/Transfer/components/TransferOperations.tsx +52 -55
  166. package/src/components/form/Transfer/components/TransferPagination.tsx +115 -111
  167. package/src/components/form/Transfer/components/TransferSearch.tsx +52 -55
  168. package/src/components/form/Transfer/hooks/useTransferData.ts +91 -81
  169. package/src/components/form/Transfer/hooks/useTransferState.ts +22 -16
  170. package/src/components/form/Transfer/index.ts +2 -8
  171. package/src/components/form/Upload/Upload.styles.ts +21 -21
  172. package/src/components/form/Upload/Upload.tsx +189 -142
  173. package/src/components/form/Upload/Upload.types.ts +31 -31
  174. package/src/components/form/Upload/index.tsx +1 -1
  175. package/src/components/form/index.tsx +60 -29
  176. package/src/components/index.tsx +0 -1
  177. package/src/components/layout/Affix/Affix.styles.ts +16 -11
  178. package/src/components/layout/Affix/Affix.tsx +67 -75
  179. package/src/components/layout/Affix/Affix.types.ts +18 -18
  180. package/src/components/layout/Affix/index.tsx +1 -1
  181. package/src/components/layout/Col/Col.styles.ts +17 -17
  182. package/src/components/layout/Col/Col.test.tsx +7 -5
  183. package/src/components/layout/Col/Col.tsx +3 -21
  184. package/src/components/layout/Col/Col.types.ts +1 -1
  185. package/src/components/layout/Container/Container.styles.ts +3 -1
  186. package/src/components/layout/Container/Container.tsx +2 -11
  187. package/src/components/layout/Grid/Grid.tsx +3 -53
  188. package/src/components/layout/Layout/Content.tsx +24 -32
  189. package/src/components/layout/Layout/Footer.tsx +24 -32
  190. package/src/components/layout/Layout/Header.tsx +24 -32
  191. package/src/components/layout/Layout/Layout.styles.ts +17 -17
  192. package/src/components/layout/Layout/Layout.tsx +14 -25
  193. package/src/components/layout/Layout/Layout.types.ts +29 -29
  194. package/src/components/layout/Layout/Sider.tsx +44 -56
  195. package/src/components/layout/Layout/index.tsx +16 -2
  196. package/src/components/layout/Row/Row.tsx +15 -43
  197. package/src/components/layout/Space/Space.tsx +3 -11
  198. package/src/components/layout/Space/Space.types.ts +1 -1
  199. package/src/components/layout/index.tsx +29 -19
  200. package/src/components/navigation/Menu/Menu.constants.ts +69 -0
  201. package/src/components/navigation/Menu/Menu.stories.tsx +107 -0
  202. package/src/components/navigation/Menu/Menu.styles.ts +25 -37
  203. package/src/components/navigation/Menu/Menu.tsx +8 -11
  204. package/src/components/navigation/Menu/Menu.types.ts +2 -2
  205. package/src/components/navigation/Menu/Menu.utils.ts +17 -17
  206. package/src/components/navigation/Menu/MenuItem.tsx +9 -11
  207. package/src/components/navigation/Menu/SubMenu.tsx +8 -6
  208. package/src/components/navigation/Menu/index.tsx +4 -69
  209. package/src/components/navigation/NavBar/NavBar.styles.ts +1 -1
  210. package/src/components/navigation/NavBar/NavBar.tsx +7 -10
  211. package/src/components/navigation/NavBar/NavBar.types.ts +3 -3
  212. package/src/components/navigation/NavBar/index.tsx +1 -1
  213. package/src/components/navigation/Pagination/Pagination.test.tsx +2 -3
  214. package/src/components/navigation/Pagination/Pagination.tsx +3 -3
  215. package/src/components/navigation/Pagination/Pagination.types.ts +3 -2
  216. package/src/components/navigation/Pagination/index.ts +9 -3
  217. package/src/components/navigation/Steps/Step.tsx +24 -44
  218. package/src/components/navigation/Steps/Steps.styles.ts +28 -13
  219. package/src/components/navigation/Steps/Steps.test.tsx +2 -0
  220. package/src/components/navigation/Steps/Steps.tsx +88 -89
  221. package/src/components/navigation/Steps/Steps.types.ts +30 -30
  222. package/src/components/navigation/Steps/index.tsx +1 -1
  223. package/src/components/navigation/Tabs/Tabs.test.tsx +3 -2
  224. package/src/components/navigation/Tabs/Tabs.types.ts +4 -3
  225. package/src/components/navigation/index.tsx +21 -16
  226. package/src/constants/index.ts +1 -1
  227. package/src/hooks/index.ts +52 -102
  228. package/src/hooks/types.ts +4 -5
  229. package/src/hooks/useAsync.ts +46 -47
  230. package/src/hooks/useClickOutside.ts +52 -0
  231. package/src/hooks/useCounter.ts +87 -0
  232. package/src/hooks/useDebounce.ts +150 -0
  233. package/src/hooks/useDeepCompareEffect.ts +88 -0
  234. package/src/hooks/useEventListener.ts +77 -0
  235. package/src/hooks/useMediaQuery.ts +75 -0
  236. package/src/hooks/useMutation.ts +233 -0
  237. package/src/hooks/usePerformance.ts +1 -64
  238. package/src/hooks/usePlatform.ts +3 -1
  239. package/src/hooks/usePrevious.ts +25 -0
  240. package/src/hooks/useRequest.ts +12 -7
  241. package/src/hooks/useStateManagement.ts +1 -1
  242. package/src/hooks/useStorage.ts +169 -0
  243. package/src/hooks/useStyle.ts +8 -2
  244. package/src/hooks/useToggle.ts +54 -0
  245. package/src/index.ts +34 -9
  246. package/src/theme/ThemeProvider.tsx +3 -7
  247. package/src/theme/ThemeProvider.types.ts +1 -1
  248. package/src/theme/defaults.ts +1 -1
  249. package/src/theme/design-system.ts +2 -2
  250. package/src/theme/design-tokens.ts +85 -99
  251. package/src/theme/generated/dark-theme.scss +1 -1
  252. package/src/theme/generated/tokens.scss +82 -18
  253. package/src/theme/index.ts +8 -29
  254. package/src/theme/responsive.tsx +36 -34
  255. package/src/theme/styles.ts +1 -1
  256. package/src/theme/useThemeUtils.ts +43 -43
  257. package/src/theme/utils.ts +32 -32
  258. package/src/theme/variables.ts +70 -51
  259. package/src/types/accessibility.ts +36 -37
  260. package/src/types/button.ts +25 -27
  261. package/src/types/component-props.ts +6 -1
  262. package/src/types/glob.d.ts +4 -0
  263. package/src/types/index.ts +2 -2
  264. package/src/types/standardized-components.ts +9 -3
  265. package/src/types/utils.ts +13 -23
  266. package/src/utils/__tests__/responsiveUtils.test.ts +5 -4
  267. package/src/utils/abort-controller.ts +48 -0
  268. package/src/utils/cache.ts +2 -6
  269. package/src/utils/createNamespace.ts +4 -4
  270. package/src/utils/environment.ts +26 -6
  271. package/src/utils/error-handler.ts +2 -2
  272. package/src/utils/errorLogger.ts +16 -20
  273. package/src/utils/formatUtils.ts +38 -70
  274. package/src/utils/http/error-codes.ts +314 -0
  275. package/src/utils/http/http-client.test.ts +63 -0
  276. package/src/utils/{network → http}/http-client.ts +45 -35
  277. package/src/utils/http/request-cache.ts +127 -0
  278. package/src/utils/http/request.ts +954 -0
  279. package/src/utils/http/taro-adapter.test.ts +74 -0
  280. package/src/utils/http/taro-adapter.ts +24 -0
  281. package/src/utils/http/types.ts +414 -0
  282. package/src/utils/http/web-adapter.ts +33 -0
  283. package/src/utils/index.ts +5 -8
  284. package/src/utils/inputValidator.ts +17 -14
  285. package/src/utils/performance/performance.ts +60 -71
  286. package/src/utils/responsiveUtils.ts +7 -16
  287. package/src/utils/rtl-support.ts +29 -19
  288. package/src/utils/security/api-security.ts +47 -39
  289. package/src/utils/securityHeaders.ts +61 -67
  290. package/src/utils/typeHelpers.ts +10 -10
  291. package/src/utils/types/dataProcessing.ts +93 -92
  292. package/src/utils/types/typeHelpers.ts +31 -21
  293. package/src/utils/xssProtection.ts +96 -48
  294. package/dist/js/index-6NJ3A1Dn.js.map +0 -1
  295. package/dist/js/index-DffLRSro.js.map +0 -1
  296. package/src/components/form/Input/Input.enhanced.tsx +0 -732
  297. package/src/components/navigation/Menu/__tests__/Menu.test.tsx +0 -687
  298. package/src/components/navigation/Tree/Tree.styles.ts +0 -553
  299. package/src/components/navigation/Tree/Tree.test.basic.tsx +0 -7
  300. package/src/components/navigation/Tree/Tree.test.functional.tsx +0 -496
  301. package/src/components/navigation/Tree/Tree.test.import.check.tsx +0 -6
  302. package/src/components/navigation/Tree/Tree.test.import.tsx +0 -6
  303. package/src/components/navigation/Tree/Tree.test.minimal.tsx +0 -5
  304. package/src/components/navigation/Tree/Tree.test.simple.tsx +0 -30
  305. package/src/components/navigation/Tree/Tree.test.tsx +0 -908
  306. package/src/components/navigation/Tree/Tree.test.working.tsx +0 -673
  307. package/src/components/navigation/Tree/Tree.tsx +0 -600
  308. package/src/components/navigation/Tree/Tree.types.ts +0 -909
  309. package/src/components/navigation/Tree/Tree.utils.ts +0 -452
  310. package/src/components/navigation/Tree/index.ts +0 -33
  311. package/src/components/navigation/Tree/index.tsx +0 -23
  312. package/src/utils/network/http-client.test.ts +0 -18
@@ -0,0 +1,314 @@
1
+ /**
2
+ * 统一错误码规范
3
+ * 提供完整的错误码定义和错误信息映射
4
+ */
5
+
6
+ /** 错误码类型 */
7
+ export enum ErrorCode {
8
+ // 网络错误
9
+ NETWORK_ERROR = 'NETWORK_ERROR',
10
+ TIMEOUT_ERROR = 'TIMEOUT_ERROR',
11
+ CANCEL_ERROR = 'CANCEL_ERROR',
12
+ INVALID_URL = 'INVALID_URL',
13
+ INSECURE_URL = 'INSECURE_URL',
14
+
15
+ // HTTP状态码错误
16
+ HTTP_400 = 'HTTP_400',
17
+ HTTP_401 = 'HTTP_401',
18
+ HTTP_403 = 'HTTP_403',
19
+ HTTP_404 = 'HTTP_404',
20
+ HTTP_405 = 'HTTP_405',
21
+ HTTP_408 = 'HTTP_408',
22
+ HTTP_409 = 'HTTP_409',
23
+ HTTP_410 = 'HTTP_410',
24
+ HTTP_422 = 'HTTP_422',
25
+ HTTP_429 = 'HTTP_429',
26
+ HTTP_500 = 'HTTP_500',
27
+ HTTP_501 = 'HTTP_501',
28
+ HTTP_502 = 'HTTP_502',
29
+ HTTP_503 = 'HTTP_503',
30
+ HTTP_504 = 'HTTP_504',
31
+ HTTP_505 = 'HTTP_505',
32
+
33
+ // 业务逻辑错误
34
+ BUSINESS_ERROR = 'BUSINESS_ERROR',
35
+ INVALID_PARAMS = 'INVALID_PARAMS',
36
+ INVALID_REQUEST = 'INVALID_REQUEST',
37
+ DATA_NOT_FOUND = 'DATA_NOT_FOUND',
38
+ DATA_CONFLICT = 'DATA_CONFLICT',
39
+ PERMISSION_DENIED = 'PERMISSION_DENIED',
40
+ USER_NOT_LOGGED_IN = 'USER_NOT_LOGGED_IN',
41
+ USER_SESSION_EXPIRED = 'USER_SESSION_EXPIRED',
42
+ USER_TOKEN_INVALID = 'USER_TOKEN_INVALID',
43
+ INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
44
+
45
+ // 系统错误
46
+ SYSTEM_ERROR = 'SYSTEM_ERROR',
47
+ SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
48
+ DATABASE_ERROR = 'DATABASE_ERROR',
49
+ CACHE_ERROR = 'CACHE_ERROR',
50
+
51
+ // 安全错误
52
+ SECURITY_ERROR = 'SECURITY_ERROR',
53
+ XSS_ATTACK = 'XSS_ATTACK',
54
+ CSRF_ATTACK = 'CSRF_ATTACK',
55
+ SQL_INJECTION = 'SQL_INJECTION',
56
+ INVALID_SIGNATURE = 'INVALID_SIGNATURE',
57
+ INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
58
+
59
+ // 其他错误
60
+ UNKNOWN_ERROR = 'UNKNOWN_ERROR',
61
+ UNEXPECTED_ERROR = 'UNEXPECTED_ERROR',
62
+ NOT_SUPPORTED = 'NOT_SUPPORTED',
63
+ FEATURE_DISABLED = 'FEATURE_DISABLED',
64
+ }
65
+
66
+ /** 错误信息映射 */
67
+ export const ERROR_MESSAGES: Record<ErrorCode, string> = {
68
+ // 网络错误
69
+ [ErrorCode.NETWORK_ERROR]: '网络连接失败,请检查您的网络设置',
70
+ [ErrorCode.TIMEOUT_ERROR]: '请求超时,请稍后重试',
71
+ [ErrorCode.CANCEL_ERROR]: '请求已取消',
72
+ [ErrorCode.INVALID_URL]: '无效的请求地址',
73
+ [ErrorCode.INSECURE_URL]: '不安全的请求地址',
74
+
75
+ // HTTP状态码错误
76
+ [ErrorCode.HTTP_400]: '请求参数错误',
77
+ [ErrorCode.HTTP_401]: '未授权访问',
78
+ [ErrorCode.HTTP_403]: '禁止访问',
79
+ [ErrorCode.HTTP_404]: '请求的资源不存在',
80
+ [ErrorCode.HTTP_405]: '不支持的请求方法',
81
+ [ErrorCode.HTTP_408]: '请求超时',
82
+ [ErrorCode.HTTP_409]: '请求冲突',
83
+ [ErrorCode.HTTP_410]: '请求的资源已永久删除',
84
+ [ErrorCode.HTTP_422]: '请求参数验证失败',
85
+ [ErrorCode.HTTP_429]: '请求过于频繁,请稍后重试',
86
+ [ErrorCode.HTTP_500]: '服务器内部错误',
87
+ [ErrorCode.HTTP_501]: '服务器不支持该请求方法',
88
+ [ErrorCode.HTTP_502]: '网关错误',
89
+ [ErrorCode.HTTP_503]: '服务暂时不可用',
90
+ [ErrorCode.HTTP_504]: '网关超时',
91
+ [ErrorCode.HTTP_505]: 'HTTP版本不支持',
92
+
93
+ // 业务逻辑错误
94
+ [ErrorCode.BUSINESS_ERROR]: '业务逻辑错误',
95
+ [ErrorCode.INVALID_PARAMS]: '无效的请求参数',
96
+ [ErrorCode.INVALID_REQUEST]: '无效的请求',
97
+ [ErrorCode.DATA_NOT_FOUND]: '请求的数据不存在',
98
+ [ErrorCode.DATA_CONFLICT]: '数据冲突',
99
+ [ErrorCode.PERMISSION_DENIED]: '没有操作权限',
100
+ [ErrorCode.USER_NOT_LOGGED_IN]: '用户未登录',
101
+ [ErrorCode.USER_SESSION_EXPIRED]: '用户会话已过期',
102
+ [ErrorCode.USER_TOKEN_INVALID]: '无效的用户令牌',
103
+ [ErrorCode.INSUFFICIENT_BALANCE]: '余额不足',
104
+
105
+ // 系统错误
106
+ [ErrorCode.SYSTEM_ERROR]: '系统错误',
107
+ [ErrorCode.SERVICE_UNAVAILABLE]: '服务不可用',
108
+ [ErrorCode.DATABASE_ERROR]: '数据库错误',
109
+ [ErrorCode.CACHE_ERROR]: '缓存错误',
110
+
111
+ // 安全错误
112
+ [ErrorCode.SECURITY_ERROR]: '安全错误',
113
+ [ErrorCode.XSS_ATTACK]: 'XSS攻击检测',
114
+ [ErrorCode.CSRF_ATTACK]: 'CSRF攻击检测',
115
+ [ErrorCode.SQL_INJECTION]: 'SQL注入攻击检测',
116
+ [ErrorCode.INVALID_SIGNATURE]: '无效的签名',
117
+ [ErrorCode.INVALID_CREDENTIALS]: '无效的凭证',
118
+
119
+ // 其他错误
120
+ [ErrorCode.UNKNOWN_ERROR]: '未知错误',
121
+ [ErrorCode.UNEXPECTED_ERROR]: '意外错误',
122
+ [ErrorCode.NOT_SUPPORTED]: '不支持的功能',
123
+ [ErrorCode.FEATURE_DISABLED]: '功能已禁用',
124
+ };
125
+
126
+ /** 错误严重程度 */
127
+ export enum ErrorSeverity {
128
+ LOW = 'LOW',
129
+ MEDIUM = 'MEDIUM',
130
+ HIGH = 'HIGH',
131
+ CRITICAL = 'CRITICAL',
132
+ }
133
+
134
+ /** 错误严重程度映射 */
135
+ export const ERROR_SEVERITY: Record<ErrorCode, ErrorSeverity> = {
136
+ // 网络错误
137
+ [ErrorCode.NETWORK_ERROR]: ErrorSeverity.MEDIUM,
138
+ [ErrorCode.TIMEOUT_ERROR]: ErrorSeverity.MEDIUM,
139
+ [ErrorCode.CANCEL_ERROR]: ErrorSeverity.LOW,
140
+ [ErrorCode.INVALID_URL]: ErrorSeverity.HIGH,
141
+ [ErrorCode.INSECURE_URL]: ErrorSeverity.CRITICAL,
142
+
143
+ // HTTP状态码错误
144
+ [ErrorCode.HTTP_400]: ErrorSeverity.MEDIUM,
145
+ [ErrorCode.HTTP_401]: ErrorSeverity.HIGH,
146
+ [ErrorCode.HTTP_403]: ErrorSeverity.HIGH,
147
+ [ErrorCode.HTTP_404]: ErrorSeverity.MEDIUM,
148
+ [ErrorCode.HTTP_405]: ErrorSeverity.HIGH,
149
+ [ErrorCode.HTTP_408]: ErrorSeverity.MEDIUM,
150
+ [ErrorCode.HTTP_409]: ErrorSeverity.MEDIUM,
151
+ [ErrorCode.HTTP_410]: ErrorSeverity.MEDIUM,
152
+ [ErrorCode.HTTP_422]: ErrorSeverity.MEDIUM,
153
+ [ErrorCode.HTTP_429]: ErrorSeverity.MEDIUM,
154
+ [ErrorCode.HTTP_500]: ErrorSeverity.CRITICAL,
155
+ [ErrorCode.HTTP_501]: ErrorSeverity.CRITICAL,
156
+ [ErrorCode.HTTP_502]: ErrorSeverity.CRITICAL,
157
+ [ErrorCode.HTTP_503]: ErrorSeverity.CRITICAL,
158
+ [ErrorCode.HTTP_504]: ErrorSeverity.CRITICAL,
159
+ [ErrorCode.HTTP_505]: ErrorSeverity.CRITICAL,
160
+
161
+ // 业务逻辑错误
162
+ [ErrorCode.BUSINESS_ERROR]: ErrorSeverity.MEDIUM,
163
+ [ErrorCode.INVALID_PARAMS]: ErrorSeverity.MEDIUM,
164
+ [ErrorCode.INVALID_REQUEST]: ErrorSeverity.MEDIUM,
165
+ [ErrorCode.DATA_NOT_FOUND]: ErrorSeverity.MEDIUM,
166
+ [ErrorCode.DATA_CONFLICT]: ErrorSeverity.MEDIUM,
167
+ [ErrorCode.PERMISSION_DENIED]: ErrorSeverity.HIGH,
168
+ [ErrorCode.USER_NOT_LOGGED_IN]: ErrorSeverity.HIGH,
169
+ [ErrorCode.USER_SESSION_EXPIRED]: ErrorSeverity.HIGH,
170
+ [ErrorCode.USER_TOKEN_INVALID]: ErrorSeverity.HIGH,
171
+ [ErrorCode.INSUFFICIENT_BALANCE]: ErrorSeverity.MEDIUM,
172
+
173
+ // 系统错误
174
+ [ErrorCode.SYSTEM_ERROR]: ErrorSeverity.CRITICAL,
175
+ [ErrorCode.SERVICE_UNAVAILABLE]: ErrorSeverity.CRITICAL,
176
+ [ErrorCode.DATABASE_ERROR]: ErrorSeverity.CRITICAL,
177
+ [ErrorCode.CACHE_ERROR]: ErrorSeverity.HIGH,
178
+
179
+ // 安全错误
180
+ [ErrorCode.SECURITY_ERROR]: ErrorSeverity.CRITICAL,
181
+ [ErrorCode.XSS_ATTACK]: ErrorSeverity.CRITICAL,
182
+ [ErrorCode.CSRF_ATTACK]: ErrorSeverity.CRITICAL,
183
+ [ErrorCode.SQL_INJECTION]: ErrorSeverity.CRITICAL,
184
+ [ErrorCode.INVALID_SIGNATURE]: ErrorSeverity.HIGH,
185
+ [ErrorCode.INVALID_CREDENTIALS]: ErrorSeverity.HIGH,
186
+
187
+ // 其他错误
188
+ [ErrorCode.UNKNOWN_ERROR]: ErrorSeverity.MEDIUM,
189
+ [ErrorCode.UNEXPECTED_ERROR]: ErrorSeverity.CRITICAL,
190
+ [ErrorCode.NOT_SUPPORTED]: ErrorSeverity.LOW,
191
+ [ErrorCode.FEATURE_DISABLED]: ErrorSeverity.LOW,
192
+ };
193
+
194
+ /** 错误码工具函数 */
195
+ export const ErrorCodeUtils = {
196
+ /**
197
+ * 根据HTTP状态码获取对应的错误码
198
+ */
199
+ fromHttpStatus(statusCode: number): ErrorCode {
200
+ switch (statusCode) {
201
+ case 400:
202
+ return ErrorCode.HTTP_400;
203
+ case 401:
204
+ return ErrorCode.HTTP_401;
205
+ case 403:
206
+ return ErrorCode.HTTP_403;
207
+ case 404:
208
+ return ErrorCode.HTTP_404;
209
+ case 405:
210
+ return ErrorCode.HTTP_405;
211
+ case 408:
212
+ return ErrorCode.HTTP_408;
213
+ case 409:
214
+ return ErrorCode.HTTP_409;
215
+ case 410:
216
+ return ErrorCode.HTTP_410;
217
+ case 422:
218
+ return ErrorCode.HTTP_422;
219
+ case 429:
220
+ return ErrorCode.HTTP_429;
221
+ case 500:
222
+ return ErrorCode.HTTP_500;
223
+ case 501:
224
+ return ErrorCode.HTTP_501;
225
+ case 502:
226
+ return ErrorCode.HTTP_502;
227
+ case 503:
228
+ return ErrorCode.HTTP_503;
229
+ case 504:
230
+ return ErrorCode.HTTP_504;
231
+ case 505:
232
+ return ErrorCode.HTTP_505;
233
+ default:
234
+ return ErrorCode.UNKNOWN_ERROR;
235
+ }
236
+ },
237
+
238
+ /**
239
+ * 获取错误信息
240
+ */
241
+ getErrorMessage(code: ErrorCode, customMessage?: string): string {
242
+ return customMessage || ERROR_MESSAGES[code] || ERROR_MESSAGES[ErrorCode.UNKNOWN_ERROR];
243
+ },
244
+
245
+ /**
246
+ * 获取错误严重程度
247
+ */
248
+ getSeverity(code: ErrorCode): ErrorSeverity {
249
+ return ERROR_SEVERITY[code] || ErrorSeverity.MEDIUM;
250
+ },
251
+
252
+ /**
253
+ * 检查错误是否为网络错误
254
+ */
255
+ isNetworkError(code: ErrorCode): boolean {
256
+ return [
257
+ ErrorCode.NETWORK_ERROR,
258
+ ErrorCode.TIMEOUT_ERROR,
259
+ ErrorCode.CANCEL_ERROR,
260
+ ErrorCode.INVALID_URL,
261
+ ErrorCode.INSECURE_URL,
262
+ ].includes(code);
263
+ },
264
+
265
+ /**
266
+ * 检查错误是否为认证错误
267
+ */
268
+ isAuthError(code: ErrorCode): boolean {
269
+ return [
270
+ ErrorCode.HTTP_401,
271
+ ErrorCode.HTTP_403,
272
+ ErrorCode.PERMISSION_DENIED,
273
+ ErrorCode.USER_NOT_LOGGED_IN,
274
+ ErrorCode.USER_SESSION_EXPIRED,
275
+ ErrorCode.USER_TOKEN_INVALID,
276
+ ErrorCode.INVALID_CREDENTIALS,
277
+ ].includes(code);
278
+ },
279
+
280
+ /**
281
+ * 检查错误是否为系统错误
282
+ */
283
+ isSystemError(code: ErrorCode): boolean {
284
+ return [
285
+ ErrorCode.SYSTEM_ERROR,
286
+ ErrorCode.SERVICE_UNAVAILABLE,
287
+ ErrorCode.DATABASE_ERROR,
288
+ ErrorCode.CACHE_ERROR,
289
+ ErrorCode.UNEXPECTED_ERROR,
290
+ ].includes(code);
291
+ },
292
+
293
+ /**
294
+ * 检查错误是否为业务错误
295
+ */
296
+ isBusinessError(code: ErrorCode): boolean {
297
+ return [
298
+ ErrorCode.BUSINESS_ERROR,
299
+ ErrorCode.INVALID_PARAMS,
300
+ ErrorCode.INVALID_REQUEST,
301
+ ErrorCode.DATA_NOT_FOUND,
302
+ ErrorCode.DATA_CONFLICT,
303
+ ErrorCode.INSUFFICIENT_BALANCE,
304
+ ].includes(code);
305
+ },
306
+ };
307
+
308
+ export default {
309
+ ErrorCode,
310
+ ERROR_MESSAGES,
311
+ ErrorSeverity,
312
+ ERROR_SEVERITY,
313
+ ErrorCodeUtils,
314
+ };
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { HttpClient } from './http-client';
3
+ import { IRequestAdapter, RequestConfig, ResponseData } from './types';
4
+
5
+ // 模拟 localStorage
6
+ const mockLocalStorage = {
7
+ storage: {} as Record<string, string>,
8
+ getItem: vi.fn((key: string) => mockLocalStorage.storage[key] || null),
9
+ setItem: vi.fn((key: string, value: string) => {
10
+ mockLocalStorage.storage[key] = value;
11
+ }),
12
+ removeItem: vi.fn((key: string) => {
13
+ delete mockLocalStorage.storage[key];
14
+ }),
15
+ clear: vi.fn(() => {
16
+ mockLocalStorage.storage = {};
17
+ })
18
+ };
19
+
20
+ globalThis.localStorage = mockLocalStorage as any;
21
+
22
+ describe('HttpClient', () => {
23
+ beforeEach(() => {
24
+ // 重置模拟
25
+ vi.clearAllMocks();
26
+ mockLocalStorage.storage = {};
27
+ });
28
+
29
+ it('builds GET request and parses json', async () => {
30
+ // 模拟 fetch
31
+ const mockFetch = vi.spyOn(global, 'fetch' as any).mockResolvedValue({
32
+ ok: true,
33
+ status: 200,
34
+ headers: new Headers({ 'content-type': 'application/json' }),
35
+ json: async () => ({ ok: true }),
36
+ text: async () => ''
37
+ } as any);
38
+
39
+ // 创建简单的适配器来避免安全功能
40
+ const simpleAdapter: IRequestAdapter = {
41
+ async request<T = any>(config: RequestConfig): Promise<ResponseData<T>> {
42
+ const response = await fetch(config.url, {
43
+ method: config.method,
44
+ headers: config.headers,
45
+ body: config.method !== 'GET' ? JSON.stringify(config.data) : undefined
46
+ });
47
+
48
+ const data = await response.json();
49
+
50
+ return {
51
+ data,
52
+ statusCode: response.status,
53
+ header: Object.fromEntries(response.headers)
54
+ };
55
+ }
56
+ };
57
+
58
+ const client = new HttpClient({ baseUrl: 'https://example.com', adapter: simpleAdapter });
59
+ const res = await client.get('/api/test', { params: { q: 'x' } });
60
+ expect(res).toEqual({ ok: true });
61
+ mockFetch.mockRestore();
62
+ });
63
+ });
@@ -1,5 +1,8 @@
1
1
  import { createApiInterceptor, buildSecureHeaders, isSecureUrl, secureRetry } from '@/utils/security/api-security';
2
2
  import { ErrorHandlingManager } from '@/utils/error-handler';
3
+ import { IRequestAdapter, RequestConfig as AdapterRequestConfig } from './types';
4
+ import { WebAdapter } from './web-adapter';
5
+ import { TaroAdapter } from './taro-adapter';
3
6
 
4
7
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
5
8
 
@@ -17,6 +20,7 @@ export interface RequestOptions {
17
20
  export interface HttpClientConfig {
18
21
  baseUrl?: string;
19
22
  headers?: Record<string, string>;
23
+ adapter?: IRequestAdapter;
20
24
  }
21
25
 
22
26
  interface RequestConfig {
@@ -29,12 +33,12 @@ interface RequestConfig {
29
33
  interface ResponseData {
30
34
  status: number;
31
35
  ok: boolean;
32
- headers: Headers;
36
+ headers: Headers | Record<string, string>;
33
37
  data: any;
34
38
  }
35
39
 
36
- type RequestInterceptor = (config: RequestConfig) => RequestConfig;
37
- type ResponseInterceptor = (response: ResponseData) => ResponseData;
40
+ type RequestInterceptor = (_config: RequestConfig) => RequestConfig;
41
+ type ResponseInterceptor = (_response: ResponseData) => ResponseData;
38
42
 
39
43
  export class HttpClient {
40
44
  private baseUrl: string;
@@ -43,10 +47,19 @@ export class HttpClient {
43
47
  private security = createApiInterceptor();
44
48
  private requestInterceptors: RequestInterceptor[] = [];
45
49
  private responseInterceptors: ResponseInterceptor[] = [];
50
+ private adapter: IRequestAdapter;
46
51
 
47
52
  constructor(config: HttpClientConfig = {}) {
48
53
  this.baseUrl = config.baseUrl || '';
49
54
  this.defaultHeaders = config.headers || {};
55
+ this.adapter = config.adapter || this.getDefaultAdapter();
56
+ }
57
+
58
+ private getDefaultAdapter(): IRequestAdapter {
59
+ if (process.env['TARO_ENV'] === 'h5') {
60
+ return new WebAdapter();
61
+ }
62
+ return new TaroAdapter();
50
63
  }
51
64
 
52
65
  addRequestInterceptor(interceptor: RequestInterceptor) {
@@ -68,41 +81,45 @@ export class HttpClient {
68
81
  const headers = {
69
82
  ...this.defaultHeaders,
70
83
  ...(options.headers || {}),
71
- ...buildSecureHeaders(method, fullUrl, options.data)
84
+ ...buildSecureHeaders(method, fullUrl, options.data),
72
85
  };
73
86
 
74
87
  let config: RequestConfig = { method, url: fullUrl, headers, data: options.data };
75
88
  config = this.security.request.execute(config);
76
89
  for (const it of this.requestInterceptors) config = it(config);
77
90
 
78
- const doFetch = async () => {
79
- const controller = new AbortController();
80
- const timeout = options.timeout ?? 15000;
81
- const timer = setTimeout(() => controller.abort(), timeout);
82
- try {
83
- const resp = await fetch(config.url, {
84
- method: config.method,
85
- headers: config.headers,
86
- body: config.method === 'GET' ? null : JSON.stringify(config.data ?? {}),
87
- signal: controller.signal
88
- });
89
- const wrapped = { status: resp.status, ok: resp.ok, headers: resp.headers, data: await this.parseBody(resp) };
90
- let processed = this.security.response.execute(wrapped);
91
- for (const it of this.responseInterceptors) processed = it(processed);
92
- if (!resp.ok) {
93
- const err = new Error(`HTTP ${resp.status}`);
94
- this.errorManager.handleError(err);
95
- throw err;
96
- }
97
- return processed.data as T;
98
- } finally {
99
- clearTimeout(timer);
91
+ const doRequest = async () => {
92
+ const adapterConfig: AdapterRequestConfig = {
93
+ url: config.url,
94
+ method: config.method,
95
+ headers: config.headers,
96
+ data: config.data,
97
+ timeout: options.timeout,
98
+ };
99
+
100
+ const resp = await this.adapter.request(adapterConfig);
101
+
102
+ const wrapped: ResponseData = {
103
+ status: resp.statusCode,
104
+ ok: resp.statusCode >= 200 && resp.statusCode < 300,
105
+ headers: resp.header,
106
+ data: resp.data,
107
+ };
108
+
109
+ let processed = this.security.response.execute(wrapped);
110
+ for (const it of this.responseInterceptors) processed = it(processed);
111
+
112
+ if (!processed.ok) {
113
+ const err = new Error(`HTTP ${processed.status}`);
114
+ this.errorManager.handleError(err);
115
+ throw err;
100
116
  }
117
+ return processed.data as T;
101
118
  };
102
119
 
103
120
  const retries = options.retries ?? (method === 'GET' ? 3 : 0);
104
121
  const retryDelay = options.retryDelay ?? 500;
105
- return await secureRetry(doFetch, retries, retryDelay);
122
+ return await secureRetry(doRequest, retries, retryDelay);
106
123
  }
107
124
 
108
125
  get<T>(url: string, options: Omit<RequestOptions, 'method' | 'data'> = {}): Promise<T> {
@@ -132,19 +149,12 @@ export class HttpClient {
132
149
  const usp = new URLSearchParams();
133
150
  Object.entries(params).forEach(([k, v]) => {
134
151
  if (v === undefined || v === null) return;
135
- if (Array.isArray(v)) v.forEach(x => usp.append(k, String(x)));
152
+ if (Array.isArray(v)) v.forEach((x) => usp.append(k, String(x)));
136
153
  else usp.append(k, String(v));
137
154
  });
138
155
  const sep = path.includes('?') ? '&' : '?';
139
156
  return `${path}${sep}${usp.toString()}`;
140
157
  }
141
-
142
- private async parseBody(resp: Response): Promise<any> {
143
- const ct = resp.headers.get('content-type') || '';
144
- if (ct.includes('application/json')) return resp.json();
145
- const text = await resp.text();
146
- try { return JSON.parse(text); } catch { return text; }
147
- }
148
158
  }
149
159
 
150
160
  export const httpClient = new HttpClient();
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Request Cache Manager
3
+ * Provides intelligent request caching and deduplication
4
+ */
5
+
6
+ export interface CacheConfig {
7
+ /** Cache time to live in milliseconds */
8
+ ttl?: number;
9
+ /** Cache key generator function */
10
+ keyGenerator?: (url: string, params?: any) => string;
11
+ /** Whether to enable cache */
12
+ enabled?: boolean;
13
+ }
14
+
15
+ export interface CachedResponse<T> {
16
+ data: T;
17
+ timestamp: number;
18
+ ttl: number;
19
+ }
20
+
21
+ export class RequestCache {
22
+ private cache = new Map<string, CachedResponse<any>>();
23
+ private pendingRequests = new Map<string, Promise<any>>();
24
+
25
+ /**
26
+ * Generate cache key from request config
27
+ */
28
+ generateKey(url: string, params?: any): string {
29
+ const paramStr = params ? JSON.stringify(params) : '';
30
+ return `${url}:${paramStr}`;
31
+ }
32
+
33
+ /**
34
+ * Get cached response if valid
35
+ */
36
+ get<T>(key: string): T | null {
37
+ const cached = this.cache.get(key);
38
+ if (!cached) return null;
39
+
40
+ const now = Date.now();
41
+ if (now - cached.timestamp > cached.ttl) {
42
+ this.cache.delete(key);
43
+ return null;
44
+ }
45
+
46
+ return cached.data as T;
47
+ }
48
+
49
+ /**
50
+ * Set cache entry
51
+ */
52
+ set<T>(key: string, data: T, ttl: number = 5 * 60 * 1000): void {
53
+ this.cache.set(key, {
54
+ data,
55
+ timestamp: Date.now(),
56
+ ttl,
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Check if request is already in flight
62
+ */
63
+ hasPendingRequest(key: string): boolean {
64
+ return this.pendingRequests.has(key);
65
+ }
66
+
67
+ /**
68
+ * Get pending request promise
69
+ */
70
+ getPendingRequest<T>(key: string): Promise<T> | undefined {
71
+ return this.pendingRequests.get(key);
72
+ }
73
+
74
+ /**
75
+ * Set pending request
76
+ */
77
+ setPendingRequest<T>(key: string, promise: Promise<T>): void {
78
+ this.pendingRequests.set(key, promise);
79
+
80
+ // Clean up after promise settles
81
+ promise
82
+ .finally(() => {
83
+ this.pendingRequests.delete(key);
84
+ })
85
+ .catch(() => {
86
+ // Ignore errors - they'll be handled by the caller
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Clear specific cache entry
92
+ */
93
+ clear(key: string): void {
94
+ this.cache.delete(key);
95
+ }
96
+
97
+ /**
98
+ * Clear all cache
99
+ */
100
+ clearAll(): void {
101
+ this.cache.clear();
102
+ }
103
+
104
+ /**
105
+ * Clear expired entries
106
+ */
107
+ clearExpired(): void {
108
+ const now = Date.now();
109
+ for (const [key, cached] of this.cache.entries()) {
110
+ if (now - cached.timestamp > cached.ttl) {
111
+ this.cache.delete(key);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get cache statistics
118
+ */
119
+ getStats() {
120
+ return {
121
+ size: this.cache.size,
122
+ pending: this.pendingRequests.size,
123
+ };
124
+ }
125
+ }
126
+
127
+ export const globalRequestCache = new RequestCache();