@snail-js/api 0.1.28 → 1.0.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.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -605
  3. package/dist/adapter/plain.d.ts +12 -0
  4. package/dist/adapter/plain.d.ts.map +1 -0
  5. package/dist/adapter/react.d.ts +14 -0
  6. package/dist/adapter/react.d.ts.map +1 -0
  7. package/dist/adapter/registry.d.ts +16 -0
  8. package/dist/adapter/registry.d.ts.map +1 -0
  9. package/dist/adapter/vue.d.ts +15 -0
  10. package/dist/adapter/vue.d.ts.map +1 -0
  11. package/dist/chunks/custom-UchOSg_s.js +466 -0
  12. package/dist/chunks/custom-UchOSg_s.js.map +1 -0
  13. package/dist/chunks/download-lBC7oOTT.js +160 -0
  14. package/dist/chunks/download-lBC7oOTT.js.map +1 -0
  15. package/dist/chunks/object-Ct0w-h4i.js +74 -0
  16. package/dist/chunks/object-Ct0w-h4i.js.map +1 -0
  17. package/dist/chunks/plugin-D3RDffDG.js +742 -0
  18. package/dist/chunks/plugin-D3RDffDG.js.map +1 -0
  19. package/dist/chunks/react-C1em-gxy.js +62 -0
  20. package/dist/chunks/react-C1em-gxy.js.map +1 -0
  21. package/dist/chunks/request-qeRhihEF.js +40 -0
  22. package/dist/chunks/request-qeRhihEF.js.map +1 -0
  23. package/dist/chunks/response-D-ubkAFl.js +127 -0
  24. package/dist/chunks/response-D-ubkAFl.js.map +1 -0
  25. package/dist/chunks/use-download-FAjtdJ58.js +1816 -0
  26. package/dist/chunks/use-download-FAjtdJ58.js.map +1 -0
  27. package/dist/chunks/vue-Dd9EhkQ3.js +34 -0
  28. package/dist/chunks/vue-Dd9EhkQ3.js.map +1 -0
  29. package/dist/core/args.d.ts +54 -0
  30. package/dist/core/args.d.ts.map +1 -0
  31. package/dist/core/connection.d.ts +15 -0
  32. package/dist/core/connection.d.ts.map +1 -0
  33. package/dist/core/context.d.ts +124 -0
  34. package/dist/core/context.d.ts.map +1 -0
  35. package/dist/core/http-stream.d.ts +28 -0
  36. package/dist/core/http-stream.d.ts.map +1 -0
  37. package/dist/core/logger.d.ts +22 -0
  38. package/dist/core/logger.d.ts.map +1 -0
  39. package/dist/core/metadata.d.ts +92 -0
  40. package/dist/core/metadata.d.ts.map +1 -0
  41. package/dist/core/metadata.keys.d.ts +41 -0
  42. package/dist/core/metadata.keys.d.ts.map +1 -0
  43. package/dist/core/method.d.ts +177 -0
  44. package/dist/core/method.d.ts.map +1 -0
  45. package/dist/core/plugin-manager.d.ts +106 -0
  46. package/dist/core/plugin-manager.d.ts.map +1 -0
  47. package/dist/core/plugin.d.ts +113 -0
  48. package/dist/core/plugin.d.ts.map +1 -0
  49. package/dist/core/reconnect.d.ts +22 -0
  50. package/dist/core/reconnect.d.ts.map +1 -0
  51. package/dist/core/resolve-stream.d.ts +45 -0
  52. package/dist/core/resolve-stream.d.ts.map +1 -0
  53. package/dist/core/resolve.d.ts +50 -0
  54. package/dist/core/resolve.d.ts.map +1 -0
  55. package/dist/core/response.d.ts +60 -0
  56. package/dist/core/response.d.ts.map +1 -0
  57. package/dist/core/server.d.ts +132 -0
  58. package/dist/core/server.d.ts.map +1 -0
  59. package/dist/core/sse.d.ts +27 -0
  60. package/dist/core/sse.d.ts.map +1 -0
  61. package/dist/core/state-bag.d.ts +36 -0
  62. package/dist/core/state-bag.d.ts.map +1 -0
  63. package/dist/core/websocket.d.ts +29 -0
  64. package/dist/core/websocket.d.ts.map +1 -0
  65. package/dist/decorators/api.d.ts +24 -4
  66. package/dist/decorators/api.d.ts.map +1 -0
  67. package/dist/decorators/args.d.ts +77 -14
  68. package/dist/decorators/args.d.ts.map +1 -0
  69. package/dist/decorators/custom.d.ts +81 -0
  70. package/dist/decorators/custom.d.ts.map +1 -0
  71. package/dist/decorators/header.d.ts +22 -0
  72. package/dist/decorators/header.d.ts.map +1 -0
  73. package/dist/decorators/index.d.ts +17 -0
  74. package/dist/decorators/index.d.ts.map +1 -0
  75. package/dist/decorators/methods.d.ts +33 -0
  76. package/dist/decorators/methods.d.ts.map +1 -0
  77. package/dist/decorators/progress.d.ts +24 -6
  78. package/dist/decorators/progress.d.ts.map +1 -0
  79. package/dist/decorators/server.d.ts +28 -7
  80. package/dist/decorators/server.d.ts.map +1 -0
  81. package/dist/decorators/stream.d.ts +113 -0
  82. package/dist/decorators/stream.d.ts.map +1 -0
  83. package/dist/default/options.d.ts +19 -0
  84. package/dist/default/options.d.ts.map +1 -0
  85. package/dist/error/base.d.ts +20 -0
  86. package/dist/error/base.d.ts.map +1 -0
  87. package/dist/error/decorator.d.ts +14 -0
  88. package/dist/error/decorator.d.ts.map +1 -0
  89. package/dist/error/hook.d.ts +14 -0
  90. package/dist/error/hook.d.ts.map +1 -0
  91. package/dist/error/index.d.ts +8 -0
  92. package/dist/error/index.d.ts.map +1 -0
  93. package/dist/error/options.d.ts +12 -0
  94. package/dist/error/options.d.ts.map +1 -0
  95. package/dist/error/plugin.d.ts +11 -0
  96. package/dist/error/plugin.d.ts.map +1 -0
  97. package/dist/error/request.d.ts +29 -0
  98. package/dist/error/request.d.ts.map +1 -0
  99. package/dist/error/response.d.ts +36 -0
  100. package/dist/error/response.d.ts.map +1 -0
  101. package/dist/index.d.ts +56 -11
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +2034 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/locale/en.d.ts +5 -0
  106. package/dist/locale/en.d.ts.map +1 -0
  107. package/dist/locale/index.d.ts +57 -0
  108. package/dist/locale/index.d.ts.map +1 -0
  109. package/dist/locale/types.d.ts +11 -0
  110. package/dist/locale/types.d.ts.map +1 -0
  111. package/dist/locale/zh.d.ts +5 -0
  112. package/dist/locale/zh.d.ts.map +1 -0
  113. package/dist/plugins/cache/adapters/indexeddb.d.ts +52 -0
  114. package/dist/plugins/cache/adapters/indexeddb.d.ts.map +1 -0
  115. package/dist/plugins/cache/adapters/memory.d.ts +60 -0
  116. package/dist/plugins/cache/adapters/memory.d.ts.map +1 -0
  117. package/dist/plugins/cache/adapters/web-storage.d.ts +43 -0
  118. package/dist/plugins/cache/adapters/web-storage.d.ts.map +1 -0
  119. package/dist/plugins/cache/decorators.d.ts +62 -0
  120. package/dist/plugins/cache/decorators.d.ts.map +1 -0
  121. package/dist/plugins/cache/index.d.ts +41 -0
  122. package/dist/plugins/cache/index.d.ts.map +1 -0
  123. package/dist/plugins/cache/key.d.ts +48 -0
  124. package/dist/plugins/cache/key.d.ts.map +1 -0
  125. package/dist/plugins/cache/manager.d.ts +124 -0
  126. package/dist/plugins/cache/manager.d.ts.map +1 -0
  127. package/dist/plugins/cache/plugin.d.ts +64 -0
  128. package/dist/plugins/cache/plugin.d.ts.map +1 -0
  129. package/dist/plugins/cache/type.d.ts +116 -0
  130. package/dist/plugins/cache/type.d.ts.map +1 -0
  131. package/dist/plugins/index.d.ts +75 -0
  132. package/dist/plugins/index.d.ts.map +1 -0
  133. package/dist/plugins/index.js +2265 -0
  134. package/dist/plugins/index.js.map +1 -0
  135. package/dist/plugins/interceptor/decorators.d.ts +44 -0
  136. package/dist/plugins/interceptor/decorators.d.ts.map +1 -0
  137. package/dist/plugins/interceptor/index.d.ts +31 -0
  138. package/dist/plugins/interceptor/index.d.ts.map +1 -0
  139. package/dist/plugins/interceptor/manager.d.ts +27 -0
  140. package/dist/plugins/interceptor/manager.d.ts.map +1 -0
  141. package/dist/plugins/interceptor/plugin.d.ts +65 -0
  142. package/dist/plugins/interceptor/plugin.d.ts.map +1 -0
  143. package/dist/plugins/interceptor/type.d.ts +45 -0
  144. package/dist/plugins/interceptor/type.d.ts.map +1 -0
  145. package/dist/plugins/pool/index.d.ts +26 -0
  146. package/dist/plugins/pool/index.d.ts.map +1 -0
  147. package/dist/plugins/pool/plugin.d.ts +83 -0
  148. package/dist/plugins/pool/plugin.d.ts.map +1 -0
  149. package/dist/plugins/pool/scheduler.d.ts +153 -0
  150. package/dist/plugins/pool/scheduler.d.ts.map +1 -0
  151. package/dist/plugins/pool/type.d.ts +38 -0
  152. package/dist/plugins/pool/type.d.ts.map +1 -0
  153. package/dist/plugins/react/index.d.ts +20 -0
  154. package/dist/plugins/react/index.d.ts.map +1 -0
  155. package/dist/plugins/react/index.js +145 -0
  156. package/dist/plugins/react/index.js.map +1 -0
  157. package/dist/plugins/react/plugin.d.ts +33 -0
  158. package/dist/plugins/react/plugin.d.ts.map +1 -0
  159. package/dist/plugins/react/type.d.ts +32 -0
  160. package/dist/plugins/react/type.d.ts.map +1 -0
  161. package/dist/plugins/transform/decorators.d.ts +54 -0
  162. package/dist/plugins/transform/decorators.d.ts.map +1 -0
  163. package/dist/plugins/transform/hydrate.d.ts +47 -0
  164. package/dist/plugins/transform/hydrate.d.ts.map +1 -0
  165. package/dist/plugins/transform/index.d.ts +41 -0
  166. package/dist/plugins/transform/index.d.ts.map +1 -0
  167. package/dist/plugins/transform/plugin.d.ts +20 -0
  168. package/dist/plugins/transform/plugin.d.ts.map +1 -0
  169. package/dist/plugins/transform/type.d.ts +67 -0
  170. package/dist/plugins/transform/type.d.ts.map +1 -0
  171. package/dist/plugins/validate/decorators.d.ts +24 -0
  172. package/dist/plugins/validate/decorators.d.ts.map +1 -0
  173. package/dist/plugins/validate/index.d.ts +40 -0
  174. package/dist/plugins/validate/index.d.ts.map +1 -0
  175. package/dist/plugins/validate/plugin.d.ts +13 -0
  176. package/dist/plugins/validate/plugin.d.ts.map +1 -0
  177. package/dist/plugins/validate/type.d.ts +54 -0
  178. package/dist/plugins/validate/type.d.ts.map +1 -0
  179. package/dist/plugins/version/decorators.d.ts +22 -0
  180. package/dist/plugins/version/decorators.d.ts.map +1 -0
  181. package/dist/plugins/version/index.d.ts +23 -0
  182. package/dist/plugins/version/index.d.ts.map +1 -0
  183. package/dist/plugins/version/plugin.d.ts +13 -0
  184. package/dist/plugins/version/plugin.d.ts.map +1 -0
  185. package/dist/plugins/version/type.d.ts +47 -0
  186. package/dist/plugins/version/type.d.ts.map +1 -0
  187. package/dist/plugins/vue/index.d.ts +21 -0
  188. package/dist/plugins/vue/index.d.ts.map +1 -0
  189. package/dist/plugins/vue/index.js +114 -0
  190. package/dist/plugins/vue/index.js.map +1 -0
  191. package/dist/plugins/vue/plugin.d.ts +15 -0
  192. package/dist/plugins/vue/plugin.d.ts.map +1 -0
  193. package/dist/plugins/vue/type.d.ts +34 -0
  194. package/dist/plugins/vue/type.d.ts.map +1 -0
  195. package/dist/strategies/index.d.ts +2 -0
  196. package/dist/strategies/index.d.ts.map +1 -0
  197. package/dist/strategies/index.js +29 -0
  198. package/dist/strategies/index.js.map +1 -0
  199. package/dist/strategies/plain.d.ts +2 -0
  200. package/dist/strategies/plain.d.ts.map +1 -0
  201. package/dist/strategies/plain.js +23 -0
  202. package/dist/strategies/plain.js.map +1 -0
  203. package/dist/strategies/react.d.ts +2 -0
  204. package/dist/strategies/react.d.ts.map +1 -0
  205. package/dist/strategies/react.js +30 -0
  206. package/dist/strategies/react.js.map +1 -0
  207. package/dist/strategies/shared/adapter.d.ts +23 -0
  208. package/dist/strategies/shared/adapter.d.ts.map +1 -0
  209. package/dist/strategies/shared/dom.d.ts +53 -0
  210. package/dist/strategies/shared/dom.d.ts.map +1 -0
  211. package/dist/strategies/shared/error.d.ts +30 -0
  212. package/dist/strategies/shared/error.d.ts.map +1 -0
  213. package/dist/strategies/shared/method.d.ts +51 -0
  214. package/dist/strategies/shared/method.d.ts.map +1 -0
  215. package/dist/strategies/shared/public.d.ts +33 -0
  216. package/dist/strategies/shared/public.d.ts.map +1 -0
  217. package/dist/strategies/shared/queue.d.ts +30 -0
  218. package/dist/strategies/shared/queue.d.ts.map +1 -0
  219. package/dist/strategies/shared/retry.d.ts +51 -0
  220. package/dist/strategies/shared/retry.d.ts.map +1 -0
  221. package/dist/strategies/shared/state.d.ts +117 -0
  222. package/dist/strategies/shared/state.d.ts.map +1 -0
  223. package/dist/strategies/shared/timing.d.ts +53 -0
  224. package/dist/strategies/shared/timing.d.ts.map +1 -0
  225. package/dist/strategies/shared/watcher.d.ts +37 -0
  226. package/dist/strategies/shared/watcher.d.ts.map +1 -0
  227. package/dist/strategies/use-auto-request.d.ts +77 -0
  228. package/dist/strategies/use-auto-request.d.ts.map +1 -0
  229. package/dist/strategies/use-download.d.ts +103 -0
  230. package/dist/strategies/use-download.d.ts.map +1 -0
  231. package/dist/strategies/use-fetcher.d.ts +52 -0
  232. package/dist/strategies/use-fetcher.d.ts.map +1 -0
  233. package/dist/strategies/use-pagination.d.ts +101 -0
  234. package/dist/strategies/use-pagination.d.ts.map +1 -0
  235. package/dist/strategies/use-request.d.ts +50 -0
  236. package/dist/strategies/use-request.d.ts.map +1 -0
  237. package/dist/strategies/use-retriable-request.d.ts +46 -0
  238. package/dist/strategies/use-retriable-request.d.ts.map +1 -0
  239. package/dist/strategies/use-sse.d.ts +103 -0
  240. package/dist/strategies/use-sse.d.ts.map +1 -0
  241. package/dist/strategies/use-token-auth.d.ts +88 -0
  242. package/dist/strategies/use-token-auth.d.ts.map +1 -0
  243. package/dist/strategies/use-uploader.d.ts +88 -0
  244. package/dist/strategies/use-uploader.d.ts.map +1 -0
  245. package/dist/strategies/use-watcher.d.ts +69 -0
  246. package/dist/strategies/use-watcher.d.ts.map +1 -0
  247. package/dist/typings/adapter.d.ts +61 -0
  248. package/dist/typings/adapter.d.ts.map +1 -0
  249. package/dist/typings/api.d.ts +122 -0
  250. package/dist/typings/api.d.ts.map +1 -0
  251. package/dist/typings/args.d.ts +51 -0
  252. package/dist/typings/args.d.ts.map +1 -0
  253. package/dist/typings/index.d.ts +8 -12
  254. package/dist/typings/index.d.ts.map +1 -0
  255. package/dist/typings/meta.d.ts +44 -0
  256. package/dist/typings/meta.d.ts.map +1 -0
  257. package/dist/typings/plugin.d.ts +114 -0
  258. package/dist/typings/plugin.d.ts.map +1 -0
  259. package/dist/typings/response.d.ts +118 -0
  260. package/dist/typings/response.d.ts.map +1 -0
  261. package/dist/typings/server.d.ts +83 -0
  262. package/dist/typings/server.d.ts.map +1 -0
  263. package/dist/typings/stream.d.ts +150 -0
  264. package/dist/typings/stream.d.ts.map +1 -0
  265. package/dist/utils/download.d.ts +97 -0
  266. package/dist/utils/download.d.ts.map +1 -0
  267. package/dist/utils/emitter.d.ts +36 -0
  268. package/dist/utils/emitter.d.ts.map +1 -0
  269. package/dist/utils/index.d.ts +6 -1
  270. package/dist/utils/index.d.ts.map +1 -0
  271. package/dist/utils/is.d.ts +22 -0
  272. package/dist/utils/is.d.ts.map +1 -0
  273. package/dist/utils/object.d.ts +44 -0
  274. package/dist/utils/object.d.ts.map +1 -0
  275. package/dist/utils/url.d.ts +44 -0
  276. package/dist/utils/url.d.ts.map +1 -0
  277. package/package.json +90 -27
  278. package/README_EN.md +0 -607
  279. package/dist/cache/index.d.ts +0 -9
  280. package/dist/cache/indexDBCache.d.ts +0 -13
  281. package/dist/cache/localstorageCache.d.ts +0 -10
  282. package/dist/cache/memoryCache.d.ts +0 -11
  283. package/dist/core/index.d.ts +0 -4
  284. package/dist/core/snailApi.d.ts +0 -22
  285. package/dist/core/snailMethod.d.ts +0 -49
  286. package/dist/core/snailServer.d.ts +0 -35
  287. package/dist/core/snailSse.d.ts +0 -20
  288. package/dist/decorators/cache.d.ts +0 -15
  289. package/dist/decorators/method.d.ts +0 -38
  290. package/dist/decorators/sse.d.ts +0 -30
  291. package/dist/decorators/strategy.d.ts +0 -10
  292. package/dist/decorators/versioning.d.ts +0 -16
  293. package/dist/eventEmmit/index.d.ts +0 -1
  294. package/dist/eventEmmit/snail.event.d.ts +0 -10
  295. package/dist/snail-api.js +0 -4871
  296. package/dist/snail-api.umd.cjs +0 -4875
  297. package/dist/strategies/jwtStrategy.d.ts +0 -4
  298. package/dist/typings/api.option.d.ts +0 -10
  299. package/dist/typings/apiProxy.d.ts +0 -8
  300. package/dist/typings/cache.management.option.d.ts +0 -25
  301. package/dist/typings/cache.type.d.ts +0 -23
  302. package/dist/typings/content.type.d.ts +0 -5
  303. package/dist/typings/request.body.d.ts +0 -3
  304. package/dist/typings/response.data.d.ts +0 -13
  305. package/dist/typings/snail.event.d.ts +0 -6
  306. package/dist/typings/snail.method.d.ts +0 -14
  307. package/dist/typings/snail.option.d.ts +0 -16
  308. package/dist/typings/sse.d.ts +0 -15
  309. package/dist/typings/strategy.d.ts +0 -5
  310. package/dist/typings/versioning.option.d.ts +0 -30
  311. package/dist/utils/function.d.ts +0 -31
  312. package/dist/versioning/index.d.ts +0 -1
  313. package/dist/versioning/versioning.d.ts +0 -12
package/dist/index.js ADDED
@@ -0,0 +1,2034 @@
1
+ import { A as stableStringify, B as SnailError, C as buildRequestURL, D as pathParamNames, E as joinURL, F as isFunction, I as isObject, L as isPlainObject, M as isBinaryBody, N as isBrowser, O as replacePathParams, P as isDefined, R as isPromise, S as en, T as isAbsoluteURL, _ as localization, a as SnailHttpError, b as t, c as finalizeRequestURL, d as paramSources, f as registerParamResolver, g as languages, h as getLocale, i as SnailHookError, j as stripQuery, k as shortHash, l as hasParamResolver, m as Localization, n as createPlugin, o as SnailResponseError, r as definePlugin, s as applyParamDescriptors, t as composeChain, u as paramResolvers, v as registerMessages, w as capitalize, x as zh, y as setLocale, z as SnailDecoratorError } from "./chunks/plugin-D3RDffDG.js";
2
+ import { n as SnailRequestError, r as SnailTimeoutError, t as SnailCancelledError } from "./chunks/request-qeRhihEF.js";
3
+ import { a as omitUndefined, i as omit, n as deferred, o as pick, s as tryCatch, t as deepMerge } from "./chunks/object-Ct0w-h4i.js";
4
+ import { a as readKey, c as DEFAULT_API_OPTIONS, d as LOG_LEVEL_WEIGHT, l as DEFAULT_RESPONSE_KEYS, n as buildResult, r as coerceJSONStringBody, s as DEFAULT_ACCEPTED_CODES, t as assertBusinessCode, u as DEFAULT_SERVER_OPTIONS } from "./chunks/response-D-ubkAFl.js";
5
+ import { A as SNAIL_DOWNLOAD_PROGRESS, B as SNAIL_WS_OPTIONS, C as getMetadata, D as resolveOwner, E as mergeMetadata, F as SNAIL_SERVER_OPTIONS, I as SNAIL_SSE_HANDLERS, L as SNAIL_SSE_OPTIONS, M as SNAIL_HTTP_STREAM, N as SNAIL_PARAMS, O as SNAIL_API_OPTIONS, P as SNAIL_REQUEST_METHOD, R as SNAIL_UPLOAD_PROGRESS, S as deleteMetadata, T as hasMetadata, V as createLogger, _ as SnailPluginError, a as customMetadataKey, b as collectMethodKeys, c as getOwnMethodMetadata, d as HeaderValue, f as Params, g as normalizeParamInput, h as defineParamDescriptor, i as createPropertyDecorator, j as SNAIL_HEADERS, k as SNAIL_CUSTOM_KEY_PREFIX, l as Data, m as createParamDecoratorFor, n as createMethodDecorator, o as getClassMetadata, p as Query, r as createParamDecorator, s as getMethodMetadata, t as createClassDecorator, u as HeaderParam, v as appendMetadata, w as getOwnMetadata, x as defineMetadata, y as clearMetadataRegistry, z as SNAIL_WS_HANDLERS } from "./chunks/custom-UchOSg_s.js";
6
+ import { i as Emitter, n as triggerBlobDownload, r as triggerDownload, t as filenameFromDisposition } from "./chunks/download-lBC7oOTT.js";
7
+ import axios, { AxiosHeaders, isCancel } from "axios";
8
+ //#region src/core/state-bag.ts
9
+ /**
10
+ * Typed key/value bag.
11
+ *
12
+ * Two independent bags ride along every request:
13
+ *
14
+ * - `ctx.state` — **plugin scratch space**. Timers, cache keys, in-flight
15
+ * promises, tracing ids. Never exposed to the caller.
16
+ * - `ctx.meta` — **caller-visible reactive values** (`data`, `loading`, `error`)
17
+ * created by the framework adapter's `initMeta` hook.
18
+ *
19
+ * Keeping them separate matters: a plugin writing bookkeeping into `meta` would
20
+ * leak internal state into the object the UI renders.
21
+ */
22
+ var StateBag = class {
23
+ constructor() {
24
+ this.values = /* @__PURE__ */ new Map();
25
+ }
26
+ get(key, fallback) {
27
+ return this.values.has(key) ? this.values.get(key) : fallback;
28
+ }
29
+ /** Read a value, throwing when the key is absent. */
30
+ require(key) {
31
+ if (!this.values.has(key)) throw new ReferenceError(`[snail] context state "${key}" has not been set`);
32
+ return this.values.get(key);
33
+ }
34
+ /** Write a value. */
35
+ set(key, value) {
36
+ this.values.set(key, value);
37
+ return this;
38
+ }
39
+ /** Write a value only when the key is currently absent. */
40
+ setDefault(key, value) {
41
+ if (!this.values.has(key)) this.values.set(key, value);
42
+ return this.values.get(key);
43
+ }
44
+ /** `true` when the key was ever set (even to `undefined`). */
45
+ has(key) {
46
+ return this.values.has(key);
47
+ }
48
+ /** Remove a key. Returns whether it existed. */
49
+ delete(key) {
50
+ return this.values.delete(key);
51
+ }
52
+ /** Drop everything. */
53
+ clear() {
54
+ this.values.clear();
55
+ }
56
+ /** Current keys. */
57
+ keys() {
58
+ return [...this.values.keys()];
59
+ }
60
+ /** Shallow snapshot, handy for logging and assertions. */
61
+ snapshot() {
62
+ return Object.fromEntries(this.values);
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/core/context.ts
67
+ /**
68
+ * Per-request context — the single object every plugin hook receives.
69
+ *
70
+ * One context exists per `send()` call, so plugins may store freely in
71
+ * `ctx.state` without worrying about concurrent requests colliding. That is a
72
+ * deliberate fix over the pre-rewrite design, where the "event map" and the
73
+ * request config lived on the long-lived `SnailMethod` instance and two
74
+ * overlapping `send()` calls shared mutable state.
75
+ */
76
+ var SnailContext = class {
77
+ constructor(init) {
78
+ this.state = new StateBag();
79
+ this.meta = {};
80
+ this.pathParams = {};
81
+ this.startedAt = Date.now();
82
+ this.interrupted = false;
83
+ this.cacheHit = false;
84
+ this.server = init.server;
85
+ this.serverOptions = init.serverOptions;
86
+ this.apiClass = init.apiClass;
87
+ this.api = init.api;
88
+ this.apiName = init.apiName;
89
+ this.apiOptions = init.apiOptions;
90
+ this.methodName = init.methodName;
91
+ this.methodType = init.methodType;
92
+ this.route = init.route;
93
+ this.fullName = `${init.serverOptions.name}.${init.apiName}.${init.methodName}`;
94
+ this.request = init.request;
95
+ this.descriptors = init.descriptors;
96
+ this.logger = init.logger;
97
+ }
98
+ /** Milliseconds elapsed since `send()` started. */
99
+ get elapsed() {
100
+ return (this.finishedAt ?? Date.now()) - this.startedAt;
101
+ }
102
+ /**
103
+ * Stop the request.
104
+ *
105
+ * With a `response` argument the network call is skipped entirely and that
106
+ * response is used instead — this is exactly how a cache hit works. Without
107
+ * one, the request is abandoned and `send()` rejects with a cancellation error.
108
+ */
109
+ interrupt(response) {
110
+ this.interrupted = true;
111
+ if (response) this.response = response;
112
+ }
113
+ /** `true` when a plugin short-circuited the request. */
114
+ get isInterrupted() {
115
+ return this.interrupted;
116
+ }
117
+ /** Record that the current response came from a cache. */
118
+ markCacheHit() {
119
+ this.cacheHit = true;
120
+ }
121
+ /** `true` when the response was served from a cache. */
122
+ get isCacheHit() {
123
+ return this.cacheHit;
124
+ }
125
+ /** Replace the current response. */
126
+ setResponse(response) {
127
+ this.response = response;
128
+ }
129
+ /** Read the current response. */
130
+ getResponse() {
131
+ return this.response;
132
+ }
133
+ /** Read the current response, throwing when there is none. */
134
+ requireResponse() {
135
+ if (!this.response) throw new ReferenceError(`[snail] ${this.fullName} has no response at this point`);
136
+ return this.response;
137
+ }
138
+ /** Replace the request config. */
139
+ setRequest(request) {
140
+ this.request = request;
141
+ this.request.url = this.request.url ?? this.route;
142
+ }
143
+ /** Read the request config. */
144
+ getRequest() {
145
+ return this.request;
146
+ }
147
+ /** Replace the parsed result. */
148
+ setResult(result) {
149
+ this.result = result;
150
+ }
151
+ /**
152
+ * Clear everything that belongs to one `send()` while keeping the context
153
+ * identity, so `meta` — and therefore the caller's reactive handles — survive
154
+ * a re-send.
155
+ */
156
+ reset(request) {
157
+ this.state.clear();
158
+ this.pathParams = {};
159
+ this.response = void 0;
160
+ this.error = void 0;
161
+ this.result = void 0;
162
+ this.finishedAt = void 0;
163
+ this.interrupted = false;
164
+ this.cacheHit = false;
165
+ this.startedAt = Date.now();
166
+ this.request = request;
167
+ }
168
+ /** Shallow copy of the fields worth logging. */
169
+ describe() {
170
+ return {
171
+ name: this.fullName,
172
+ method: this.methodType,
173
+ url: this.request.url,
174
+ route: this.route,
175
+ baseURL: this.request.baseURL,
176
+ params: this.request.params,
177
+ fromCache: this.cacheHit,
178
+ elapsed: this.elapsed
179
+ };
180
+ }
181
+ };
182
+ //#endregion
183
+ //#region src/core/connection.ts
184
+ /**
185
+ * Attach `Symbol.dispose` / `Symbol.asyncDispose` to a connection.
186
+ *
187
+ * Defined as a helper rather than as two methods on each transport because the
188
+ * symbols are *optional*: `esnext.disposable` is a recent addition, and an engine
189
+ * that lacks them would otherwise end up with a property literally named
190
+ * `"undefined"` if the keys were written inline.
191
+ *
192
+ * The result is that a connection can be released three equivalent ways —
193
+ * `close()`, `using`, or `await using` — with `close()` staying the only one that
194
+ * works everywhere.
195
+ */
196
+ function withDispose(connection) {
197
+ const symbols = Symbol;
198
+ const target = connection;
199
+ if (typeof symbols.dispose === "symbol") target[symbols.dispose] = () => {
200
+ connection.close();
201
+ };
202
+ if (typeof symbols.asyncDispose === "symbol") target[symbols.asyncDispose] = async () => {
203
+ connection.close();
204
+ };
205
+ return connection;
206
+ }
207
+ //#endregion
208
+ //#region src/core/http-stream.ts
209
+ /**
210
+ * Streaming HTTP response.
211
+ *
212
+ * Uses `fetch` so the response body arrives as a `ReadableStream`, which works
213
+ * in browsers, Node 18+ and workers alike. The connection object is itself
214
+ * async-iterable, so callers write `for await (const chunk of stream)`.
215
+ */
216
+ function createHttpStream(init) {
217
+ const { url, options, body, name, logger } = init;
218
+ const controller = new AbortController();
219
+ const opened = deferred();
220
+ const closed = deferred();
221
+ opened.promise.catch(() => void 0);
222
+ const decodeText = options.decodeText ?? true;
223
+ const lineDelimited = options.lineDelimited ?? false;
224
+ let connected = false;
225
+ let stopped = false;
226
+ let iterator;
227
+ const start = async () => {
228
+ const method = (options.method ?? "POST").toUpperCase();
229
+ const hasBody = body !== void 0 && method !== "GET";
230
+ const response = await fetch(url, {
231
+ method,
232
+ headers: {
233
+ accept: "text/event-stream, application/x-ndjson, text/plain",
234
+ ...hasBody ? { "content-type": "application/json" } : {},
235
+ ...options.headers ?? {},
236
+ ...init.headers ?? {}
237
+ },
238
+ body: hasBody ? JSON.stringify(body) : void 0,
239
+ signal: controller.signal
240
+ });
241
+ if (!response.ok) throw new SnailRequestError(t("error.request.failed", name, `stream responded ${response.status} ${response.statusText}`));
242
+ if (!response.body) throw new SnailRequestError(t("error.request.failed", name, "stream response has no readable body"));
243
+ connected = true;
244
+ opened.resolve();
245
+ return response.body.getReader();
246
+ };
247
+ /**
248
+ * Start the request immediately rather than on first iteration.
249
+ *
250
+ * `opened` and `closed` are only useful if the request is already in flight — a
251
+ * lazily started stream leaves `await connection.opened` pending forever, which
252
+ * is a trap. Creating the connection *is* the "send" action for a stream.
253
+ */
254
+ const readerPromise = start();
255
+ readerPromise.catch(() => void 0);
256
+ const createIterator = () => {
257
+ const decoder = new TextDecoder("utf-8");
258
+ let pending = "";
259
+ let reader;
260
+ const next = async () => {
261
+ try {
262
+ reader ??= await readerPromise;
263
+ while (true) {
264
+ if (lineDelimited) {
265
+ const newlineIndex = pending.indexOf("\n");
266
+ if (newlineIndex !== -1) {
267
+ const line = pending.slice(0, newlineIndex);
268
+ pending = pending.slice(newlineIndex + 1);
269
+ if (line.trim().length === 0) continue;
270
+ return {
271
+ value: line,
272
+ done: false
273
+ };
274
+ }
275
+ } else if (pending.length > 0) {
276
+ const chunk = pending;
277
+ pending = "";
278
+ return {
279
+ value: chunk,
280
+ done: false
281
+ };
282
+ }
283
+ const { done, value } = await reader.read();
284
+ if (done) {
285
+ connected = false;
286
+ stopped = true;
287
+ closed.resolve();
288
+ if (pending.length > 0) {
289
+ const tail = pending;
290
+ pending = "";
291
+ return {
292
+ value: tail,
293
+ done: false
294
+ };
295
+ }
296
+ return {
297
+ value: void 0,
298
+ done: true
299
+ };
300
+ }
301
+ pending += decodeText ? decoder.decode(value, { stream: true }) : String(value);
302
+ }
303
+ } catch (error) {
304
+ connected = false;
305
+ stopped = true;
306
+ opened.reject(error);
307
+ closed.resolve();
308
+ throw error;
309
+ }
310
+ };
311
+ return {
312
+ next,
313
+ return: async () => {
314
+ stopped = true;
315
+ connected = false;
316
+ controller.abort();
317
+ closed.resolve();
318
+ return {
319
+ value: void 0,
320
+ done: true
321
+ };
322
+ }
323
+ };
324
+ };
325
+ const connection = withDispose({
326
+ get connected() {
327
+ return connected;
328
+ },
329
+ opened: opened.promise,
330
+ closed: closed.promise,
331
+ close() {
332
+ if (stopped) return;
333
+ stopped = true;
334
+ connected = false;
335
+ controller.abort();
336
+ logger.debug(t("info.sse.close", name));
337
+ closed.resolve();
338
+ },
339
+ [Symbol.asyncIterator]() {
340
+ iterator ??= createIterator();
341
+ return iterator;
342
+ },
343
+ async text() {
344
+ let out = "";
345
+ for await (const chunk of connection) out += chunk;
346
+ return out;
347
+ }
348
+ });
349
+ return connection;
350
+ }
351
+ //#endregion
352
+ //#region src/core/method.ts
353
+ /**
354
+ * One pending request.
355
+ *
356
+ * Created by calling a proxied api method — `userApi.getUser("1")` — and sent by
357
+ * calling {@link SnailMethod.send}. Nothing hits the network until `send()`.
358
+ *
359
+ * ## Why the context is built once and reset
360
+ *
361
+ * Framework adapters create the caller's reactive handles in their `initMeta`
362
+ * hook. Those handles must survive every re-send, so the context is constructed
363
+ * once and {@link SnailContext.reset} clears only the per-request fields. Calling
364
+ * `userApi.getUser()` twice would produce two independent sets of refs, which is
365
+ * exactly the bug the old `request()`-returns-fresh-state design had.
366
+ */
367
+ var SnailMethod = class {
368
+ /**
369
+ * Caller-visible reactive values created by framework adapters.
370
+ *
371
+ * A live view of `context.meta`, so the handles an adapter created in
372
+ * `initMeta` stay stable across every re-send. Empty when no adapter plugin is
373
+ * installed.
374
+ *
375
+ * `loading` and `error` are typed by the {@link SnailMeta} interface; the
376
+ * envelope handles are named after the server's configured keys, so augment
377
+ * `SnailMeta` to type them.
378
+ */
379
+ get meta() {
380
+ return this.context.meta;
381
+ }
382
+ constructor(init, args = []) {
383
+ this.emitter = new Emitter();
384
+ this.inFlight = false;
385
+ this.init = init;
386
+ this.args = args;
387
+ this.name = `${init.serverOptions.name}.${init.apiName}.${init.methodName}`;
388
+ this.methodName = init.methodName;
389
+ this.methodType = init.methodType;
390
+ this.route = init.route;
391
+ this.context = new SnailContext({
392
+ server: init.server,
393
+ serverOptions: init.serverOptions,
394
+ apiClass: init.apiClass,
395
+ api: init.api,
396
+ apiName: init.apiName,
397
+ apiOptions: init.apiOptions,
398
+ methodName: init.methodName,
399
+ methodType: init.methodType,
400
+ route: init.route,
401
+ request: init.requestConfig(),
402
+ descriptors: init.descriptors,
403
+ logger: init.logger
404
+ });
405
+ init.pluginManager.runEffectsSync("initMeta", this.context);
406
+ }
407
+ /** `true` while a request is in flight. */
408
+ get pending() {
409
+ return this.inFlight;
410
+ }
411
+ /** Result of the most recent successful request. */
412
+ get result() {
413
+ return this.context.result;
414
+ }
415
+ /** Error from the most recent failed request. */
416
+ get error() {
417
+ return this.context.error;
418
+ }
419
+ /** The final axios config of the most recent request. */
420
+ get request() {
421
+ return this.context.request;
422
+ }
423
+ /**
424
+ * Send the request.
425
+ *
426
+ * Any arguments given here replace the ones captured when the method was
427
+ * proxied, which is what lets a strategy own one instance and vary the
428
+ * arguments per call.
429
+ *
430
+ * ## One request in flight at a time
431
+ *
432
+ * A `SnailMethod` owns exactly one context, and that context is what holds the
433
+ * caller's reactive handles. Two overlapping `send()` calls would therefore
434
+ * race over the same `ctx.response`, and the slower response could land in the
435
+ * state the faster one already reported.
436
+ *
437
+ * Starting a second send therefore **aborts the first**, so the previous call
438
+ * rejects with a `SnailCancelledError` and "latest wins" holds. Callers that
439
+ * genuinely want two parallel requests should create two instances —
440
+ * `userApi.getUser("1")` and `userApi.getUser("2")` — which is also what makes
441
+ * the argument types line up.
442
+ */
443
+ async send(...args) {
444
+ const callArgs = args.length > 0 ? args : this.args;
445
+ const ctx = this.begin();
446
+ this.inFlight = true;
447
+ ctx.logger.info(t("info.request.start", this.methodType, `${ctx.request.baseURL ?? ""}${this.route}`, this.name));
448
+ try {
449
+ await this.init.pluginManager.ready;
450
+ if (Object.keys(ctx.meta).length === 0) this.init.pluginManager.runEffectsSync("initMeta", ctx);
451
+ this.init.pluginManager.runEffectsSync("beforeCreate", ctx);
452
+ applyParamDescriptors(ctx, callArgs);
453
+ finalizeRequestURL(ctx);
454
+ await this.init.pluginManager.runChain("beforeRequest", ctx, () => this.dispatch(ctx));
455
+ if (!ctx.response) throw new SnailCancelledError(t("error.request.cancelled", this.name));
456
+ await this.init.pluginManager.runChain("afterResponse", ctx);
457
+ const result = this.finalize(ctx);
458
+ this.emitter.emit("success", result);
459
+ return result;
460
+ } catch (error) {
461
+ throw await this.fail(ctx, error);
462
+ } finally {
463
+ this.inFlight = false;
464
+ ctx.finishedAt = Date.now();
465
+ try {
466
+ await this.init.pluginManager.runEffects("afterRequest", ctx);
467
+ } catch (cleanupError) {
468
+ ctx.logger.error(t("error.request.failed", this.name, `afterRequest hook: ${String(cleanupError)}`));
469
+ }
470
+ this.emitter.emit("finish", void 0);
471
+ }
472
+ }
473
+ /**
474
+ * Cancel the in-flight request.
475
+ *
476
+ * `send()` rejects with a {@link SnailCancelledError}, which strategies treat
477
+ * as expected control flow rather than a failure.
478
+ */
479
+ abort(reason) {
480
+ this.controller?.abort(reason);
481
+ }
482
+ /** Subscribe to a successful request. Returns an unsubscribe function. */
483
+ onSuccess(listener) {
484
+ return this.emitter.on("success", listener);
485
+ }
486
+ /** Subscribe to a failed request. */
487
+ onError(listener) {
488
+ return this.emitter.on("error", listener);
489
+ }
490
+ /**
491
+ * Subscribe to a rejected business code.
492
+ *
493
+ * Observation only: the request still rejects with a `SnailResponseError`, so
494
+ * this is the right place to raise a toast, not to recover.
495
+ */
496
+ onCodeError(listener) {
497
+ return this.emitter.on("codeError", listener);
498
+ }
499
+ /** Subscribe to settlement, successful or not. */
500
+ onFinish(listener) {
501
+ return this.emitter.on("finish", listener);
502
+ }
503
+ /** Subscribe to a response served from a cache. */
504
+ onHitCache(listener) {
505
+ return this.emitter.on("cache", listener);
506
+ }
507
+ /** Reset the context for a fresh send, superseding any request already in flight. */
508
+ begin() {
509
+ const ctx = this.context;
510
+ this.controller?.abort();
511
+ this.controller = new AbortController();
512
+ const config = this.init.requestConfig();
513
+ config.signal = this.controller.signal;
514
+ ctx.reset(config);
515
+ return ctx;
516
+ }
517
+ /**
518
+ * The transport step.
519
+ *
520
+ * Runs only when every `beforeRequest` hook called `next()`, i.e. only when the
521
+ * response has to come from the network. The `afterResponse` chain is *not* here
522
+ * — see {@link SnailMethod.send} for why it has to run on a cache hit too.
523
+ */
524
+ async dispatch(ctx) {
525
+ let config = this.init.pluginManager.reduce("requestInterceptor", ctx.request, ctx);
526
+ ctx.request = config;
527
+ let response;
528
+ try {
529
+ response = await this.init.axios.request(config);
530
+ } catch (error) {
531
+ throw this.normalizeTransportError(error);
532
+ }
533
+ response = coerceJSONStringBody(response, ctx.serverOptions.coerceJSONString);
534
+ ctx.setResponse(response);
535
+ const intercepted = this.init.pluginManager.reduce("responseInterceptor", response, ctx);
536
+ ctx.setResponse(intercepted);
537
+ }
538
+ /** Validate the envelope and assemble the caller-facing result. */
539
+ finalize(ctx) {
540
+ const response = ctx.requireResponse();
541
+ const envelope = response.data;
542
+ const { codeKey, messageKey, dataKey, validateCode } = ctx.serverOptions;
543
+ const code = readKey(envelope, codeKey);
544
+ assertBusinessCode({
545
+ body: envelope,
546
+ code,
547
+ dataKey,
548
+ validate: validateCode,
549
+ fullName: this.name,
550
+ message: t("error.response.code", this.name, String(code))
551
+ });
552
+ const result = buildResult({
553
+ response,
554
+ envelope,
555
+ codeKey,
556
+ messageKey,
557
+ dataKey,
558
+ fromCache: ctx.isCacheHit,
559
+ config: ctx.request
560
+ });
561
+ ctx.setResult(result);
562
+ if (ctx.isCacheHit) this.emitter.emit("cache", void 0);
563
+ ctx.logger.info(t("info.request.success", this.methodType, `${response.config.baseURL ?? ""}${response.config.url ?? ""}`, this.name, `${Math.round(ctx.elapsed)}ms`));
564
+ return result;
565
+ }
566
+ /** Report a failure through the plugin hooks and the events, then rethrow it. */
567
+ async fail(ctx, error) {
568
+ ctx.error = error;
569
+ try {
570
+ await this.init.pluginManager.runEffects("onError", ctx, error);
571
+ } catch (hookError) {
572
+ ctx.logger.error(t("error.request.failed", this.name, `onError hook: ${String(hookError)}`));
573
+ }
574
+ if (error instanceof SnailResponseError) {
575
+ this.emitter.emit("codeError", {
576
+ code: error.businessCode,
577
+ payload: error.payload,
578
+ error
579
+ });
580
+ ctx.logger.warn(t("info.request.codeError", this.methodType, this.route, this.name, String(error.businessCode)));
581
+ } else {
582
+ this.emitter.emit("error", error);
583
+ if (!(error instanceof SnailCancelledError)) ctx.logger.error(t("info.request.error", this.methodType, this.route, this.name, String(error)));
584
+ }
585
+ return error;
586
+ }
587
+ /**
588
+ * Turn axios' error vocabulary into ours.
589
+ *
590
+ * The rule is: **if the server answered, hand back axios' error unchanged** —
591
+ * it carries `response.status` and `response.data`, which applications and the
592
+ * auth strategies both branch on. Only when no response exists at all (DNS
593
+ * failure, offline, CORS rejection) is there nothing useful to preserve, so
594
+ * that case becomes a typed {@link SnailHttpError} with the axios error as its
595
+ * `cause`.
596
+ */
597
+ normalizeTransportError(error) {
598
+ if (error instanceof SnailCancelledError) return error;
599
+ const axiosError = error;
600
+ if (isCancel(error) || axiosError?.code === "ERR_CANCELED") return new SnailCancelledError(t("error.request.cancelled", this.name), { cause: error });
601
+ if (axiosError?.code === "ECONNABORTED" || axiosError?.code === "ETIMEDOUT") return new SnailTimeoutError(t("error.request.timeout", this.name, String(axiosError.config?.timeout ?? "")), {
602
+ timeout: axiosError.config?.timeout,
603
+ cause: error
604
+ });
605
+ if (axiosError?.isAxiosError === true && !axiosError.response) return new SnailHttpError(t("error.request.failed", this.name, axiosError.message ?? "network error"), {
606
+ cause: error,
607
+ code: "SNAIL_NETWORK_ERROR"
608
+ });
609
+ return error;
610
+ }
611
+ };
612
+ //#endregion
613
+ //#region src/core/plugin-manager.ts
614
+ /** Hook names that traverse the plugin list in the forward direction. */
615
+ var FORWARD_HOOKS = /* @__PURE__ */ new Set([
616
+ "configureServer",
617
+ "configureApi",
618
+ "configureMethod",
619
+ "initMeta",
620
+ "beforeCreate",
621
+ "beforeRequest",
622
+ "requestInterceptor"
623
+ ]);
624
+ /** Hook names that traverse the plugin list in the unwind direction. */
625
+ var UNWIND_HOOKS = /* @__PURE__ */ new Set([
626
+ "afterResponse",
627
+ "responseInterceptor",
628
+ "onError",
629
+ "afterRequest"
630
+ ]);
631
+ /**
632
+ * Per-server plugin registry.
633
+ *
634
+ * ## Why this is not a singleton
635
+ *
636
+ * The pre-rewrite implementation kept one global manager plus a mutable
637
+ * `_server` pointer that every caller had to set with `switchServer()` before
638
+ * `getHooks()`. Any two servers in flight — or simply two interleaved `await`s —
639
+ * silently read the *wrong* server's plugins. Each `SnailServer` now owns its
640
+ * own manager and passes its name in the constructor, so that class of bug is
641
+ * unrepresentable.
642
+ *
643
+ * ## Ordering
644
+ *
645
+ * Plugins sort by `priority` descending, ties broken by registration order.
646
+ *
647
+ * - **Forward hooks** run highest-priority first, so an interceptor (`100`)
648
+ * installs before the cache (`-100`).
649
+ * - **Unwind hooks** run in the opposite direction, closing the onion: the
650
+ * plugin closest to the network reacts to the response first.
651
+ *
652
+ * ## Sync registration, async install
653
+ *
654
+ * `register()` is synchronous and returns immediately, so `Service.use(A).use(B)`
655
+ * chains naturally while a `dependsOn` mistake still throws on the spot. The
656
+ * `install` hooks themselves may be async; {@link PluginManager.ready} is awaited
657
+ * once per request, before any plugin hook runs.
658
+ */
659
+ var PluginManager = class {
660
+ constructor(serverName, serverOptions) {
661
+ this.serverName = serverName;
662
+ this.serverOptions = serverOptions;
663
+ this.registry = /* @__PURE__ */ new Map();
664
+ this.order = [];
665
+ this.installTasks = [];
666
+ this.sequence = 0;
667
+ }
668
+ /** Number of registered plugins. */
669
+ get size() {
670
+ return this.registry.size;
671
+ }
672
+ /** Resolves once every `install` hook has settled. */
673
+ get ready() {
674
+ return Promise.all(this.installTasks).then(() => void 0);
675
+ }
676
+ /** `true` when a plugin with this name is registered. */
677
+ has(name) {
678
+ return this.registry.has(name);
679
+ }
680
+ /** Read one registered plugin. */
681
+ get(name) {
682
+ return this.registry.get(name);
683
+ }
684
+ /** Registered plugin names in chain order (forward phase). */
685
+ names() {
686
+ return this.sorted("forward").map((entry) => entry.name);
687
+ }
688
+ /** All registered plugins in chain order (forward phase). */
689
+ list() {
690
+ return this.sorted("forward");
691
+ }
692
+ /**
693
+ * Validate and register a plugin. Synchronous by design.
694
+ *
695
+ * Throws {@link SnailPluginError} for a nameless plugin, a duplicate name or
696
+ * an unsatisfied `dependsOn` — all before anything is mutated.
697
+ */
698
+ register(plugin) {
699
+ if (!plugin || typeof plugin !== "object") throw new SnailPluginError("[snail] use() expects a plugin object");
700
+ const { name } = plugin;
701
+ if (typeof name !== "string" || name.length === 0) throw new SnailPluginError("[snail] a plugin must declare a non-empty `name`");
702
+ if (this.registry.has(name)) throw new SnailPluginError(t("error.options.plugin.exists", name, this.serverName), { pluginName: name });
703
+ for (const dependency of plugin.dependsOn ?? []) if (!this.registry.has(dependency)) throw new SnailPluginError(t("error.options.plugin.missing", name, dependency), { pluginName: name });
704
+ this.registry.set(name, {
705
+ name,
706
+ priority: Number.isFinite(plugin.priority) ? plugin.priority : 0,
707
+ index: this.sequence++,
708
+ instance: plugin
709
+ });
710
+ this.order.push(name);
711
+ if (typeof plugin.install === "function") {
712
+ const context = {
713
+ serverName: this.serverName,
714
+ serverOptions: this.serverOptions,
715
+ pluginNames: this.names()
716
+ };
717
+ let outcome;
718
+ try {
719
+ outcome = plugin.install(context, plugin.options);
720
+ } catch (error) {
721
+ this.registry.delete(name);
722
+ this.order.pop();
723
+ throw new SnailPluginError(`[snail] plugin "${name}" failed to install: ${String(error)}`, {
724
+ pluginName: name,
725
+ cause: error
726
+ });
727
+ }
728
+ if (outcome && typeof outcome.then === "function") this.installTasks.push(Promise.resolve(outcome).then(() => void 0, (error) => {
729
+ throw new SnailPluginError(`[snail] plugin "${name}" failed to install: ${String(error)}`, {
730
+ pluginName: name,
731
+ cause: error
732
+ });
733
+ }));
734
+ }
735
+ }
736
+ /** Unregister a plugin, running its `uninstall` hook first. */
737
+ async remove(name) {
738
+ const entry = this.registry.get(name);
739
+ if (!entry) throw new SnailPluginError(t("error.options.plugin.notFound", name, this.serverName), { pluginName: name });
740
+ await entry.instance.uninstall?.({
741
+ serverName: this.serverName,
742
+ serverOptions: this.serverOptions,
743
+ pluginNames: this.names()
744
+ }, entry.instance.options);
745
+ this.registry.delete(name);
746
+ const at = this.order.indexOf(name);
747
+ if (at !== -1) this.order.splice(at, 1);
748
+ }
749
+ /** Remove every plugin, unwinding in reverse registration order. */
750
+ async clear() {
751
+ for (const name of [...this.order].reverse()) await this.remove(name);
752
+ }
753
+ /**
754
+ * Sorted plugin list.
755
+ *
756
+ * `"forward"` → priority descending (highest first).
757
+ * `"unwind"` → priority ascending (highest last).
758
+ */
759
+ sorted(direction) {
760
+ const entries = this.order.map((name) => this.registry.get(name)).filter((entry) => entry !== void 0);
761
+ entries.sort((a, b) => a.priority === b.priority ? a.index - b.index : b.priority - a.priority);
762
+ return direction === "forward" ? entries : entries.reverse();
763
+ }
764
+ /** Hooks of one name, in the direction that hook traverses. */
765
+ hooks(hookName) {
766
+ if (!FORWARD_HOOKS.has(hookName) && !UNWIND_HOOKS.has(hookName)) throw new SnailPluginError(t("error.hook.unknown", hookName));
767
+ const direction = FORWARD_HOOKS.has(hookName) ? "forward" : "unwind";
768
+ const bound = [];
769
+ for (const entry of this.sorted(direction)) {
770
+ const hook = entry.instance[hookName];
771
+ if (typeof hook === "function") bound.push({
772
+ pluginName: entry.name,
773
+ hook
774
+ });
775
+ }
776
+ return bound;
777
+ }
778
+ /** `true` when at least one plugin implements `hookName`. */
779
+ hasHook(hookName) {
780
+ for (const name of this.order) if (typeof this.registry.get(name).instance[hookName] === "function") return true;
781
+ return false;
782
+ }
783
+ /**
784
+ * Run a chain hook over every plugin, then `downstream`.
785
+ *
786
+ * A plugin that never calls `next()` stops the chain: `downstream` (for
787
+ * `beforeRequest`, the HTTP request itself) never runs. That is the mechanism
788
+ * behind a cache hit.
789
+ */
790
+ async runChain(hookName, ctx, downstream) {
791
+ const hooks = this.hooks(hookName);
792
+ if (hooks.length === 0) {
793
+ await downstream?.();
794
+ return;
795
+ }
796
+ await composeChain(hookName, hooks)(ctx, downstream);
797
+ }
798
+ /** Run a non-chain hook over every plugin, sequentially and awaited. */
799
+ async runEffects(hookName, ...args) {
800
+ for (const { hook } of this.hooks(hookName)) await hook(...args);
801
+ }
802
+ /** Run a non-chain hook synchronously — for constructor-time hooks. */
803
+ runEffectsSync(hookName, ...args) {
804
+ for (const { hook } of this.hooks(hookName)) hook(...args);
805
+ }
806
+ /**
807
+ * Fold a value through every plugin's hook.
808
+ *
809
+ * Used by `requestInterceptor` / `responseInterceptor`, where each plugin may
810
+ * return a replacement for the value it received. Returning `undefined` keeps
811
+ * the previous value.
812
+ */
813
+ reduce(hookName, initial, ...args) {
814
+ let current = initial;
815
+ for (const { hook } of this.hooks(hookName)) {
816
+ const next = hook(current, ...args);
817
+ if (next !== void 0 && next !== null) current = next;
818
+ }
819
+ return current;
820
+ }
821
+ };
822
+ //#endregion
823
+ //#region src/error/options.ts
824
+ /**
825
+ * Thrown when a required configuration block is missing — a server class
826
+ * without `@Server()`, an api class without `@Api()`, or a value that failed
827
+ * one of the option guards.
828
+ */
829
+ var SnailOptionsError = class extends SnailError {
830
+ constructor(message, options = {}) {
831
+ super(message, {
832
+ code: "SNAIL_OPTIONS_ERROR",
833
+ cause: options.cause
834
+ });
835
+ }
836
+ };
837
+ //#endregion
838
+ //#region src/core/resolve.ts
839
+ /**
840
+ * Turns raw decorator metadata into concrete, fully-defaulted options.
841
+ *
842
+ * Everything a request needs is resolved *here* rather than spread across the
843
+ * server, the proxy and the method. One place to read, one place to test.
844
+ */
845
+ /** Read and default the options written by `@Server(...)`. */
846
+ function resolveServerOptions(serverClass, fallbackName) {
847
+ const declared = getMetadata(SNAIL_SERVER_OPTIONS, serverClass);
848
+ if (!declared) throw new SnailOptionsError(t("error.options.server.missing", fallbackName));
849
+ const baseURL = declared.baseURL ?? DEFAULT_SERVER_OPTIONS.baseURL;
850
+ if (typeof baseURL !== "string" || baseURL.length === 0) throw new SnailOptionsError(t("error.options.server.baseURL"));
851
+ return {
852
+ ...declared,
853
+ name: declared.name ?? fallbackName,
854
+ baseURL,
855
+ timeout: declared.timeout ?? DEFAULT_SERVER_OPTIONS.timeout,
856
+ codeKey: declared.codeKey ?? DEFAULT_SERVER_OPTIONS.codeKey,
857
+ messageKey: declared.messageKey ?? DEFAULT_SERVER_OPTIONS.messageKey,
858
+ dataKey: declared.dataKey ?? DEFAULT_SERVER_OPTIONS.dataKey,
859
+ logLevel: declared.logLevel ?? DEFAULT_SERVER_OPTIONS.logLevel,
860
+ coerceJSONString: declared.coerceJSONString ?? DEFAULT_SERVER_OPTIONS.coerceJSONString
861
+ };
862
+ }
863
+ /** Read and default the options written by `@Api(...)`. */
864
+ function resolveApiOptions(apiClass, fallbackName) {
865
+ const declared = getMetadata(SNAIL_API_OPTIONS, apiClass) ?? {};
866
+ if (declared.url !== void 0 && typeof declared.url !== "string") throw new SnailOptionsError(t("error.options.api.url"));
867
+ return {
868
+ url: declared.url ?? "",
869
+ name: declared.name ?? fallbackName,
870
+ timeout: declared.timeout,
871
+ adapter: declared.adapter,
872
+ responseType: declared.responseType,
873
+ withCredentials: declared.withCredentials
874
+ };
875
+ }
876
+ /** Read the full request-method options written by `@Get(path, options)`. */
877
+ function resolveMethodDecoratorOptions(apiClass, methodName) {
878
+ return getMetadata(SNAIL_REQUEST_METHOD, apiClass, methodName);
879
+ }
880
+ /**
881
+ * Merge the three levels of `@Header(...)` — api class, then method.
882
+ *
883
+ * Method-level headers win, which is the only ordering that lets a single
884
+ * endpoint override a class-wide default.
885
+ */
886
+ function resolveHeaders(apiClass, methodName) {
887
+ const apiHeaders = getMetadata(SNAIL_HEADERS, apiClass) ?? {};
888
+ const methodHeaders = getMetadata(SNAIL_HEADERS, apiClass, methodName) ?? {};
889
+ return AxiosHeaders.from({
890
+ ...apiHeaders,
891
+ ...methodHeaders
892
+ });
893
+ }
894
+ /** Progress callbacks written by `@UploadProgress()` / `@DownloadProgress()`. */
895
+ function resolveProgress(apiClass, methodName) {
896
+ return {
897
+ onUploadProgress: getMetadata(SNAIL_UPLOAD_PROGRESS, apiClass, methodName),
898
+ onDownloadProgress: getMetadata(SNAIL_DOWNLOAD_PROGRESS, apiClass, methodName)
899
+ };
900
+ }
901
+ /** Join the api prefix with a method path. */
902
+ function resolveRoute(apiURL, methodPath) {
903
+ return buildRequestURL(apiURL, methodPath || "");
904
+ }
905
+ /**
906
+ * Build the axios config a request starts from.
907
+ *
908
+ * Values cascade method → api → server, and only the winner survives. The
909
+ * argument decorators and the plugins refine this further during the pipeline.
910
+ */ function buildBaseRequestConfig(input) {
911
+ const { serverOptions, apiOptions, methodOptions, methodType, headers } = input;
912
+ const merged = AxiosHeaders.concat(headers);
913
+ for (const [key, value] of Object.entries(serverOptions.headers ?? {})) if (!merged.has(key)) merged.set(key, value);
914
+ return {
915
+ url: methodOptions.url,
916
+ method: methodType.toLowerCase(),
917
+ baseURL: serverOptions.baseURL,
918
+ timeout: methodOptions.timeout ?? apiOptions.timeout ?? serverOptions.timeout,
919
+ responseType: methodOptions.responseType ?? apiOptions.responseType ?? serverOptions.responseType ?? "json",
920
+ withCredentials: methodOptions.withCredentials ?? apiOptions.withCredentials ?? serverOptions.withCredentials,
921
+ adapter: methodOptions.adapter ?? apiOptions.adapter ?? serverOptions.adapter,
922
+ headers: merged,
923
+ params: {
924
+ ...serverOptions.params ?? {},
925
+ ...methodOptions.params ?? {}
926
+ },
927
+ data: methodOptions.data,
928
+ onUploadProgress: methodOptions.onUploadProgress,
929
+ onDownloadProgress: methodOptions.onDownloadProgress
930
+ };
931
+ }
932
+ //#endregion
933
+ //#region src/core/resolve-stream.ts
934
+ var EMPTY_SSE_HANDLERS = {
935
+ open: [],
936
+ error: [],
937
+ events: []
938
+ };
939
+ var EMPTY_WS_HANDLERS = {
940
+ open: [],
941
+ message: [],
942
+ close: [],
943
+ error: []
944
+ };
945
+ /** Read the `@Sse(...)` options and its registered handlers. */
946
+ function resolveSseEndpoint(streamClass) {
947
+ const declared = getMetadata(SNAIL_SSE_OPTIONS, streamClass);
948
+ if (!declared) return void 0;
949
+ return {
950
+ url: declared.url ?? "",
951
+ options: declared,
952
+ handlers: getMetadata(SNAIL_SSE_HANDLERS, streamClass) ?? EMPTY_SSE_HANDLERS
953
+ };
954
+ }
955
+ /** Read the `@WebSocket(...)` options and its registered handlers. */
956
+ function resolveWsEndpoint(streamClass) {
957
+ const declared = getMetadata(SNAIL_WS_OPTIONS, streamClass);
958
+ if (!declared) return void 0;
959
+ return {
960
+ url: declared.url ?? "",
961
+ options: declared,
962
+ handlers: getMetadata(SNAIL_WS_HANDLERS, streamClass) ?? EMPTY_WS_HANDLERS
963
+ };
964
+ }
965
+ /** Read the `@HttpStream(...)` options of one method. */
966
+ function resolveHttpStreamEndpoint(apiClass, methodName) {
967
+ const declared = getMetadata(SNAIL_HTTP_STREAM, apiClass, methodName);
968
+ if (!declared) return void 0;
969
+ return {
970
+ url: declared.url ?? "",
971
+ options: declared
972
+ };
973
+ }
974
+ /**
975
+ * Turn an http(s) `baseURL` into the matching WebSocket origin.
976
+ *
977
+ * `https://api.example.com` → `wss://api.example.com`. A relative `baseURL`
978
+ * (the common browser case) keeps working because `new WebSocket` resolves a
979
+ * relative url against the document base — but the scheme still has to be
980
+ * upgraded explicitly, which is what this does.
981
+ */
982
+ function toWebSocketURL(url) {
983
+ if (url.startsWith("https://")) return `wss://${url.slice(8)}`;
984
+ if (url.startsWith("http://")) return `ws://${url.slice(7)}`;
985
+ return url;
986
+ }
987
+ /**
988
+ * Bind every SSE handler to the instance that will receive the events.
989
+ *
990
+ * Decorators only ever see the prototype, so the raw functions are stored and
991
+ * bound here — one binding per `open()`, against the instance actually created by
992
+ * `createSse`.
993
+ */
994
+ function rebindSseHandlers(handlers, instance) {
995
+ return {
996
+ open: handlers.open.map((fn) => fn.bind(instance)),
997
+ error: handlers.error.map((fn) => fn.bind(instance)),
998
+ events: handlers.events.map((entry) => ({
999
+ event: entry.event,
1000
+ handler: entry.handler.bind(instance)
1001
+ }))
1002
+ };
1003
+ }
1004
+ /** Bind every WebSocket handler to the instance. @see rebindSseHandlers */
1005
+ function rebindWsHandlers(handlers, instance) {
1006
+ return {
1007
+ open: handlers.open.map((fn) => fn.bind(instance)),
1008
+ message: handlers.message.map((fn) => fn.bind(instance)),
1009
+ close: handlers.close.map((fn) => fn.bind(instance)),
1010
+ error: handlers.error.map((fn) => fn.bind(instance))
1011
+ };
1012
+ }
1013
+ //#endregion
1014
+ //#region src/core/reconnect.ts
1015
+ /** Defaults used when a transport enables reconnecting without a policy. */
1016
+ var DEFAULT_RECONNECT_POLICY = {
1017
+ retries: 3,
1018
+ delayMs: 1e3,
1019
+ maxDelayMs: 3e4,
1020
+ factor: 2,
1021
+ jitter: true
1022
+ };
1023
+ /**
1024
+ * Normalise the `reconnect` option.
1025
+ *
1026
+ * Returns `undefined` when reconnecting is switched off, so callers can branch on
1027
+ * a single falsy check instead of handling `false | undefined | policy`.
1028
+ */
1029
+ function resolveReconnectPolicy(policy) {
1030
+ if (policy === false) return void 0;
1031
+ if (policy === void 0) return { ...DEFAULT_RECONNECT_POLICY };
1032
+ return {
1033
+ ...DEFAULT_RECONNECT_POLICY,
1034
+ ...policy
1035
+ };
1036
+ }
1037
+ /**
1038
+ * Exponential backoff with optional full jitter.
1039
+ *
1040
+ * `attempt` is 1-based: `attempt: 1` returns the first delay. The result is
1041
+ * always capped by `maxDelayMs`, so a long outage cannot push a retry hours out.
1042
+ */
1043
+ function backoffDelay(attempt, policy) {
1044
+ const base = policy.delayMs * Math.pow(policy.factor, Math.max(0, attempt - 1));
1045
+ const capped = Math.min(base, policy.maxDelayMs);
1046
+ if (!policy.jitter) return capped;
1047
+ return Math.max(50, Math.round(Math.random() * capped));
1048
+ }
1049
+ /** `true` when another attempt is allowed. */
1050
+ function canRetry(attempt, policy) {
1051
+ return attempt <= policy.retries;
1052
+ }
1053
+ //#endregion
1054
+ //#region src/core/sse.ts
1055
+ /**
1056
+ * Server-Sent Events over `fetch`.
1057
+ *
1058
+ * `EventSource` is deliberately not used: it cannot send request headers, cannot
1059
+ * `POST`, cannot be aborted cleanly and reconnects on its own terms. A `fetch`
1060
+ * stream reader supports all four, and the parser below is ~50 lines.
1061
+ */
1062
+ function createSseConnection(init) {
1063
+ const { url, options, handlers, name, logger } = init;
1064
+ const policy = resolveReconnectPolicy(options.reconnect ?? { retries: 3 });
1065
+ const opened = deferred();
1066
+ const closed = deferred();
1067
+ const controller = new AbortController();
1068
+ opened.promise.catch(() => void 0);
1069
+ let connected = false;
1070
+ let stopped = false;
1071
+ let attempt = 0;
1072
+ let retryHint;
1073
+ let timer;
1074
+ let lastError;
1075
+ /**
1076
+ * Whether the current attempt delivered at least one event.
1077
+ *
1078
+ * This is what separates a healthy connection from a doomed one. A server that
1079
+ * accepts the request, sends events and then closes the stream is behaving
1080
+ * correctly and should reconnect with a fresh retry budget; a server that
1081
+ * accepts and immediately closes without ever sending anything is looping, and
1082
+ * its retry budget must actually run out.
1083
+ *
1084
+ * Resetting the counter as soon as headers arrive — the obvious implementation
1085
+ * — makes the budget meaningless and produces an unbounded hot reconnect loop.
1086
+ */
1087
+ let sawEvent = false;
1088
+ const messageListeners = /* @__PURE__ */ new Set();
1089
+ const eventListeners = /* @__PURE__ */ new Map();
1090
+ /** Invoke one listener set, keeping a broken listener from killing the stream. */
1091
+ const notify = (listeners, message) => {
1092
+ for (const listener of [...listeners]) try {
1093
+ listener(message);
1094
+ } catch (error) {
1095
+ logger.error(`[snail] ${name} SSE message listener threw`, error);
1096
+ }
1097
+ };
1098
+ const dispatch = (message) => {
1099
+ sawEvent = true;
1100
+ for (const entry of handlers.events) {
1101
+ if (entry.event !== message.event) continue;
1102
+ try {
1103
+ entry.handler(message);
1104
+ } catch (error) {
1105
+ logger.error(`[snail] ${name} SSE handler for "${message.event}" threw`, error);
1106
+ }
1107
+ }
1108
+ notify(messageListeners, message);
1109
+ notify(eventListeners.get(message.event) ?? [], message);
1110
+ };
1111
+ const scheduleReconnect = () => {
1112
+ if (stopped) return;
1113
+ if (!policy || !canRetry(attempt + 1, policy)) {
1114
+ if (!connected) opened.reject(lastError ?? new SnailRequestError(t("error.request.failed", name, `SSE connection failed after ${attempt} attempt(s)`)));
1115
+ stopped = true;
1116
+ closed.resolve();
1117
+ return;
1118
+ }
1119
+ attempt += 1;
1120
+ const delay = retryHint ?? backoffDelay(attempt, policy);
1121
+ retryHint = void 0;
1122
+ timer = setTimeout(() => {
1123
+ run();
1124
+ }, delay);
1125
+ };
1126
+ const run = async () => {
1127
+ if (stopped) return;
1128
+ sawEvent = false;
1129
+ try {
1130
+ const response = await fetch(url, {
1131
+ method: options.method ?? "GET",
1132
+ headers: {
1133
+ accept: "text/event-stream",
1134
+ ...options.headers ?? {},
1135
+ ...init.headers ?? {}
1136
+ },
1137
+ body: options.method === "POST" && options.data !== void 0 ? JSON.stringify(options.data) : void 0,
1138
+ credentials: options.withCredentials ? "include" : "same-origin",
1139
+ signal: controller.signal
1140
+ });
1141
+ if (!response.ok) throw new SnailRequestError(t("error.request.failed", name, `SSE responded ${response.status} ${response.statusText}`));
1142
+ if (!response.body) throw new SnailRequestError(t("error.request.failed", name, "SSE response has no readable body"));
1143
+ if (!connected) {
1144
+ connected = true;
1145
+ opened.resolve();
1146
+ for (const handler of handlers.open) try {
1147
+ handler(new Event("open"));
1148
+ } catch (error) {
1149
+ logger.error(`[snail] ${name} SSE open handler threw`, error);
1150
+ }
1151
+ logger.info(t("info.sse.open", name));
1152
+ }
1153
+ await readStream(response.body, dispatch, (hint) => {
1154
+ retryHint = hint;
1155
+ });
1156
+ connected = false;
1157
+ lastError = void 0;
1158
+ if (sawEvent) attempt = 0;
1159
+ scheduleReconnect();
1160
+ } catch (error) {
1161
+ if (controller.signal.aborted || stopped) {
1162
+ stopped = true;
1163
+ closed.resolve();
1164
+ return;
1165
+ }
1166
+ lastError = error;
1167
+ for (const handler of handlers.error) try {
1168
+ handler(new Event("error"));
1169
+ } catch (handlerError) {
1170
+ logger.error(`[snail] ${name} SSE error handler threw`, handlerError);
1171
+ }
1172
+ logger.warn(t("error.request.failed", name, `SSE ${String(error)}`));
1173
+ connected = false;
1174
+ scheduleReconnect();
1175
+ }
1176
+ };
1177
+ run();
1178
+ return withDispose({
1179
+ get connected() {
1180
+ return connected;
1181
+ },
1182
+ opened: opened.promise,
1183
+ closed: closed.promise,
1184
+ onMessage(listener) {
1185
+ messageListeners.add(listener);
1186
+ return () => {
1187
+ messageListeners.delete(listener);
1188
+ };
1189
+ },
1190
+ on(event, listener) {
1191
+ let listeners = eventListeners.get(event);
1192
+ if (!listeners) {
1193
+ listeners = /* @__PURE__ */ new Set();
1194
+ eventListeners.set(event, listeners);
1195
+ }
1196
+ listeners.add(listener);
1197
+ return () => {
1198
+ listeners.delete(listener);
1199
+ };
1200
+ },
1201
+ close() {
1202
+ if (stopped) return;
1203
+ stopped = true;
1204
+ if (timer) clearTimeout(timer);
1205
+ controller.abort();
1206
+ connected = false;
1207
+ messageListeners.clear();
1208
+ eventListeners.clear();
1209
+ logger.info(t("info.sse.close", name));
1210
+ closed.resolve();
1211
+ }
1212
+ });
1213
+ }
1214
+ /**
1215
+ * Parse an event-stream body.
1216
+ *
1217
+ * Follows the WHATWG rules that matter in practice: lines are terminated by
1218
+ * `\n`, `\r\n` or `\r`; a blank line dispatches the buffered event; `:` starts a
1219
+ * comment; a field value keeps everything after the first colon, minus one
1220
+ * leading space.
1221
+ */
1222
+ async function readStream(body, dispatch, onRetry) {
1223
+ const reader = body.getReader();
1224
+ const decoder = new TextDecoder("utf-8");
1225
+ let buffer = "";
1226
+ let eventName = "message";
1227
+ let dataLines = [];
1228
+ let lastId = "";
1229
+ let sawData = false;
1230
+ const flush = () => {
1231
+ if (!sawData) {
1232
+ eventName = "message";
1233
+ dataLines = [];
1234
+ return;
1235
+ }
1236
+ dispatch({
1237
+ event: eventName || "message",
1238
+ data: dataLines.join("\n"),
1239
+ id: lastId,
1240
+ retry: void 0
1241
+ });
1242
+ eventName = "message";
1243
+ dataLines = [];
1244
+ sawData = false;
1245
+ };
1246
+ while (true) {
1247
+ const { done, value } = await reader.read();
1248
+ if (done) break;
1249
+ buffer += decoder.decode(value, { stream: true });
1250
+ let newlineIndex = buffer.search(/\r\n|\r|\n/);
1251
+ while (newlineIndex !== -1) {
1252
+ const line = buffer.slice(0, newlineIndex);
1253
+ const terminatorLength = buffer.startsWith("\r\n", newlineIndex) ? 2 : 1;
1254
+ buffer = buffer.slice(newlineIndex + terminatorLength);
1255
+ newlineIndex = buffer.search(/\r\n|\r|\n/);
1256
+ if (line.length === 0) {
1257
+ flush();
1258
+ continue;
1259
+ }
1260
+ if (line.startsWith(":")) continue;
1261
+ const colon = line.indexOf(":");
1262
+ const field = colon === -1 ? line : line.slice(0, colon);
1263
+ let value = colon === -1 ? "" : line.slice(colon + 1);
1264
+ if (value.startsWith(" ")) value = value.slice(1);
1265
+ switch (field) {
1266
+ case "event":
1267
+ eventName = value;
1268
+ break;
1269
+ case "data":
1270
+ dataLines.push(value);
1271
+ sawData = true;
1272
+ break;
1273
+ case "id":
1274
+ lastId = value;
1275
+ break;
1276
+ case "retry": {
1277
+ const parsed = Number.parseInt(value, 10);
1278
+ if (Number.isFinite(parsed)) onRetry(parsed);
1279
+ break;
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1284
+ flush();
1285
+ }
1286
+ //#endregion
1287
+ //#region src/core/websocket.ts
1288
+ /**
1289
+ * WebSocket with reconnecting and an outbound queue.
1290
+ *
1291
+ * Two behaviours the platform socket does not give you, and which every real
1292
+ * application ends up writing by hand:
1293
+ *
1294
+ * - **Reconnect with backoff** — `close` is normal; without a policy the socket
1295
+ * simply stays dead after a blip.
1296
+ * - **Send-before-open queueing** — `open()` returns immediately, so a caller
1297
+ * that sends on the next line would otherwise throw `InvalidStateError`.
1298
+ */
1299
+ function createWsConnection(init) {
1300
+ const { url, options, handlers, name, logger } = init;
1301
+ const policy = resolveReconnectPolicy(options.reconnect ?? { retries: 3 });
1302
+ const serializer = normalizeSerializer(options.serializer);
1303
+ const queueWhileConnecting = options.queueWhileConnecting ?? true;
1304
+ const opened = deferred();
1305
+ const closed = deferred();
1306
+ opened.promise.catch(() => void 0);
1307
+ let socket;
1308
+ let connected = false;
1309
+ let stopped = false;
1310
+ let attempt = 0;
1311
+ let timer;
1312
+ let queue = [];
1313
+ const emit = (kind, event) => {
1314
+ for (const handler of handlers[kind]) try {
1315
+ handler(event);
1316
+ } catch (error) {
1317
+ logger.error(`[snail] ${name} WS ${kind} handler threw`, error);
1318
+ }
1319
+ };
1320
+ const flush = () => {
1321
+ if (!socket || !connected || queue.length === 0) return;
1322
+ const pending = queue;
1323
+ queue = [];
1324
+ for (const message of pending) socket.send(serializer.serialize(message));
1325
+ };
1326
+ const scheduleReconnect = () => {
1327
+ if (stopped) return;
1328
+ if (!policy || !canRetry(attempt + 1, policy)) {
1329
+ if (!connected) opened.reject(new SnailRequestError(t("error.request.failed", name, `WebSocket failed after ${attempt} attempt(s)`)));
1330
+ stopped = true;
1331
+ closed.resolve();
1332
+ return;
1333
+ }
1334
+ attempt += 1;
1335
+ timer = setTimeout(connect, backoffDelay(attempt, policy));
1336
+ };
1337
+ const connect = () => {
1338
+ if (stopped) return;
1339
+ let ws;
1340
+ try {
1341
+ ws = new WebSocket(url, options.protocols);
1342
+ } catch (error) {
1343
+ logger.warn(t("error.request.failed", name, `WebSocket ${String(error)}`));
1344
+ scheduleReconnect();
1345
+ return;
1346
+ }
1347
+ socket = ws;
1348
+ ws.onopen = (event) => {
1349
+ connected = true;
1350
+ attempt = 0;
1351
+ opened.resolve();
1352
+ emit("open", event);
1353
+ logger.info(t("info.ws.open", name));
1354
+ flush();
1355
+ };
1356
+ ws.onmessage = (event) => {
1357
+ const raw = options.serializer === "text" ? event : deserializeEvent(event, serializer);
1358
+ emit("message", raw);
1359
+ };
1360
+ ws.onerror = (event) => {
1361
+ emit("error", event);
1362
+ };
1363
+ ws.onclose = (event) => {
1364
+ connected = false;
1365
+ emit("close", event);
1366
+ logger.info(t("info.ws.close", name, String(event.code)));
1367
+ if (!stopped) scheduleReconnect();
1368
+ };
1369
+ };
1370
+ connect();
1371
+ return withDispose({
1372
+ get connected() {
1373
+ return connected;
1374
+ },
1375
+ opened: opened.promise,
1376
+ closed: closed.promise,
1377
+ send(data) {
1378
+ if (stopped) throw new SnailRequestError(t("error.request.failed", name, "cannot send on a closed WebSocket"));
1379
+ if (!connected) {
1380
+ if (!queueWhileConnecting) throw new SnailRequestError(t("error.request.failed", name, "WebSocket is not open yet"));
1381
+ queue.push(data);
1382
+ return;
1383
+ }
1384
+ socket.send(serializer.serialize(data));
1385
+ },
1386
+ close() {
1387
+ if (stopped) return;
1388
+ stopped = true;
1389
+ if (timer) clearTimeout(timer);
1390
+ queue = [];
1391
+ connected = false;
1392
+ try {
1393
+ socket?.close(1e3, "client closed");
1394
+ } catch {}
1395
+ closed.resolve();
1396
+ }
1397
+ });
1398
+ }
1399
+ function normalizeSerializer(input) {
1400
+ if (!input || input === "json") return {
1401
+ serialize: (value) => typeof value === "string" ? value : JSON.stringify(value),
1402
+ deserialize: (raw) => {
1403
+ try {
1404
+ return JSON.parse(raw);
1405
+ } catch {
1406
+ return raw;
1407
+ }
1408
+ }
1409
+ };
1410
+ if (input === "text") return {
1411
+ serialize: (value) => String(value),
1412
+ deserialize: (raw) => raw
1413
+ };
1414
+ return {
1415
+ serialize: (value) => String(input.serialize?.(value) ?? value),
1416
+ deserialize: (raw) => input.deserialize?.(raw) ?? raw
1417
+ };
1418
+ }
1419
+ /** Decode a `MessageEvent` into a plain value for the handlers. */
1420
+ function deserializeEvent(event, serializer) {
1421
+ if (typeof event.data !== "string") return event;
1422
+ const decoded = serializer.deserialize(event.data);
1423
+ return {
1424
+ ...event,
1425
+ data: decoded
1426
+ };
1427
+ }
1428
+ //#endregion
1429
+ //#region src/core/server.ts
1430
+ /**
1431
+ * The server base class.
1432
+ *
1433
+ * Extend it, decorate the subclass with `@Server(...)` and instantiate once:
1434
+ *
1435
+ * ```ts
1436
+ * @Server({ baseURL: "/api", timeout: 5000 })
1437
+ * class BackEnd extends SnailServer {}
1438
+ *
1439
+ * export const Service = new BackEnd();
1440
+ * ```
1441
+ *
1442
+ * The instance owns an axios instance, a plugin registry and the resolved
1443
+ * options. It is created eagerly at module load, so `use()` is synchronous and
1444
+ * chainable: `Service.use(A()).use(B())`.
1445
+ */
1446
+ var SnailServer = class {
1447
+ constructor() {
1448
+ this.apiCache = /* @__PURE__ */ new WeakMap();
1449
+ const serverClass = this.constructor;
1450
+ this.options = resolveServerOptions(serverClass, serverClass.name);
1451
+ this.name = this.options.name;
1452
+ this.logger = createLogger(this.options.logLevel);
1453
+ this.axios = axios.create();
1454
+ this.pluginManager = new PluginManager(this.name, this.options);
1455
+ for (const { hook } of this.pluginManager.hooks("configureServer")) hook(this.options);
1456
+ }
1457
+ /**
1458
+ * Register a plugin.
1459
+ *
1460
+ * Synchronous and chainable. Validation (name, duplicates, `dependsOn`) throws
1461
+ * immediately; an async `install` hook is awaited once, before the first
1462
+ * request, via the manager's `ready` promise.
1463
+ */
1464
+ use(plugin) {
1465
+ const instance = typeof plugin === "function" ? plugin() : plugin;
1466
+ this.pluginManager.register(instance);
1467
+ return this;
1468
+ }
1469
+ /** Unregister a plugin by instance or by name. */
1470
+ async remove(plugin) {
1471
+ const name = typeof plugin === "string" ? plugin : plugin?.name;
1472
+ if (!name || !this.pluginManager.has(name)) return false;
1473
+ await this.pluginManager.remove(name);
1474
+ return true;
1475
+ }
1476
+ /** `true` when a plugin with this name is registered. */
1477
+ hasPlugin(name) {
1478
+ return this.pluginManager.has(name);
1479
+ }
1480
+ /** Registered plugin names, in chain order. */
1481
+ get plugins() {
1482
+ return this.pluginManager.names();
1483
+ }
1484
+ /**
1485
+ * Turn a decorated api class into a proxy whose methods build requests.
1486
+ *
1487
+ * ```ts
1488
+ * @Api("/user")
1489
+ * class UserApi {
1490
+ * @Get("/:id")
1491
+ * getUser(@Params("id") id: string): Promise<User> { return null!; }
1492
+ * }
1493
+ *
1494
+ * export const userApi = Service.createApi(UserApi);
1495
+ * const method = userApi.getUser("1"); // nothing sent yet
1496
+ * const { data } = await method.send();
1497
+ * ```
1498
+ *
1499
+ * A declared return type — `Promise<User>` above — becomes the payload type,
1500
+ * so `data` is `User` with no explicit generic.
1501
+ */
1502
+ createApi(apiClass) {
1503
+ const cached = this.apiCache.get(apiClass);
1504
+ if (cached) return cached;
1505
+ const apiOptions = resolveApiOptions(apiClass, apiClass.name);
1506
+ for (const { hook } of this.pluginManager.hooks("configureApi")) hook(apiOptions, apiClass);
1507
+ const instance = new apiClass();
1508
+ const descriptors = /* @__PURE__ */ new Map();
1509
+ const descriptorFor = (methodName) => {
1510
+ if (descriptors.has(methodName)) return descriptors.get(methodName);
1511
+ const resolved = this.resolveMethodDescriptor(apiClass, apiOptions, methodName);
1512
+ descriptors.set(methodName, resolved);
1513
+ return resolved;
1514
+ };
1515
+ const proxied = new Proxy(instance, { get: (target, propertyKey, receiver) => {
1516
+ const value = Reflect.get(target, propertyKey, receiver);
1517
+ if (typeof propertyKey === "symbol" || typeof value !== "function") return value;
1518
+ const descriptor = descriptorFor(String(propertyKey));
1519
+ if (!descriptor) return value;
1520
+ if (descriptor.kind === "stream") return (...args) => this.createHttpStream(apiClass, apiOptions, descriptor, args);
1521
+ return (...args) => this.createMethod(apiClass, target, apiOptions, descriptor, args);
1522
+ } });
1523
+ this.apiCache.set(apiClass, proxied);
1524
+ return proxied;
1525
+ }
1526
+ /**
1527
+ * Turn a class decorated with `@Sse(...)` into an endpoint factory.
1528
+ *
1529
+ * ```ts
1530
+ * @Sse("/events")
1531
+ * class Events { @SseEvent() onMessage(m: SnailSseMessage) {} }
1532
+ *
1533
+ * const events = Service.createSse(Events);
1534
+ * const connection = events.open();
1535
+ * ```
1536
+ */
1537
+ createSse(sseClass) {
1538
+ const endpoint = resolveSseEndpoint(sseClass);
1539
+ if (!endpoint) throw new SnailDecoratorError(`[snail] ${sseClass.name} is missing the @Sse() decorator`);
1540
+ const instance = new sseClass();
1541
+ return { open: () => createSseConnection({
1542
+ url: buildRequestURL(this.options.baseURL, endpoint.url),
1543
+ options: endpoint.options,
1544
+ handlers: rebindSseHandlers(endpoint.handlers, instance),
1545
+ name: `${this.options.name}.${sseClass.name}`,
1546
+ logger: this.logger
1547
+ }) };
1548
+ }
1549
+ /**
1550
+ * Turn a class decorated with `@WebSocket(...)` into an endpoint factory.
1551
+ *
1552
+ * ```ts
1553
+ * @WebSocket("/ws")
1554
+ * class Chat { @OnWsMessage() incoming(event: MessageEvent) {} }
1555
+ *
1556
+ * const chat = Service.createWebSocket(Chat);
1557
+ * const socket = chat.open();
1558
+ * socket.send({ hello: "world" });
1559
+ * ```
1560
+ */
1561
+ createWebSocket(wsClass) {
1562
+ const endpoint = resolveWsEndpoint(wsClass);
1563
+ if (!endpoint) throw new SnailDecoratorError(`[snail] ${wsClass.name} is missing the @WebSocket() decorator`);
1564
+ const instance = new wsClass();
1565
+ return { open: () => createWsConnection({
1566
+ url: toWebSocketURL(buildRequestURL(this.options.baseURL, endpoint.url)),
1567
+ options: endpoint.options,
1568
+ handlers: rebindWsHandlers(endpoint.handlers, instance),
1569
+ name: `${this.options.name}.${wsClass.name}`,
1570
+ logger: this.logger
1571
+ }) };
1572
+ }
1573
+ /**
1574
+ * Send a one-off request that is **not** backed by a decorated api class.
1575
+ *
1576
+ * This is an escape hatch, and it is deliberately thin: it awaits plugin
1577
+ * installation and then calls the axios instance directly. **No lifecycle hook
1578
+ * runs** — no `beforeRequest`, no `afterResponse`, no caching, no validation, no
1579
+ * response transformation — and the envelope is *not* unwrapped, so you get the
1580
+ * raw `AxiosResponse` back.
1581
+ *
1582
+ * Prefer a decorated api method for anything that should participate in the
1583
+ * plugin pipeline. Reach for this only for a call that genuinely has no place in
1584
+ * a service definition, such as a health check against a third party.
1585
+ */
1586
+ async request(config) {
1587
+ await this.pluginManager.ready;
1588
+ const merged = {
1589
+ baseURL: this.options.baseURL,
1590
+ timeout: this.options.timeout,
1591
+ ...config
1592
+ };
1593
+ return await this.axios.request(merged);
1594
+ }
1595
+ /** Uninstall every plugin, running their cleanup hooks. */
1596
+ async dispose() {
1597
+ await this.pluginManager.clear();
1598
+ }
1599
+ /** Metadata helper for tooling and tests. */
1600
+ describe() {
1601
+ return {
1602
+ name: this.name,
1603
+ baseURL: this.options.baseURL,
1604
+ timeout: this.options.timeout,
1605
+ codeKey: this.options.codeKey,
1606
+ messageKey: this.options.messageKey,
1607
+ dataKey: this.options.dataKey,
1608
+ logLevel: this.options.logLevel,
1609
+ plugins: this.pluginManager.list().map((entry) => ({
1610
+ name: entry.name,
1611
+ priority: entry.priority
1612
+ }))
1613
+ };
1614
+ }
1615
+ /**
1616
+ * Resolve everything static about one decorated method.
1617
+ *
1618
+ * Called once per method per api class. Returns `null` for a member that is not
1619
+ * a decorated endpoint, which is how an api class may keep plain helpers next to
1620
+ * its requests.
1621
+ */
1622
+ resolveMethodDescriptor(apiClass, apiOptions, methodName) {
1623
+ const streamEndpoint = resolveHttpStreamEndpoint(apiClass, methodName);
1624
+ if (streamEndpoint) return {
1625
+ kind: "stream",
1626
+ methodName,
1627
+ methodType: (streamEndpoint.options.method ?? "POST").toUpperCase(),
1628
+ route: resolveRoute(apiOptions.url, streamEndpoint.url),
1629
+ streamOptions: streamEndpoint.options,
1630
+ headers: resolveHeaders(apiClass, methodName),
1631
+ descriptors: getMetadata(SNAIL_PARAMS, apiClass, methodName) ?? []
1632
+ };
1633
+ const methodOptions = resolveMethodDecoratorOptions(apiClass, methodName);
1634
+ if (!methodOptions) return null;
1635
+ const methodType = methodOptions.method;
1636
+ const progress = resolveProgress(apiClass, methodName);
1637
+ const headers = resolveHeaders(apiClass, methodName);
1638
+ const route = resolveRoute(apiOptions.url, methodOptions.url ?? "");
1639
+ const resolvedMethodOptions = {
1640
+ ...methodOptions,
1641
+ url: route,
1642
+ onUploadProgress: methodOptions.onUploadProgress ?? progress.onUploadProgress,
1643
+ onDownloadProgress: methodOptions.onDownloadProgress ?? progress.onDownloadProgress
1644
+ };
1645
+ for (const { hook } of this.pluginManager.hooks("configureMethod")) hook(resolvedMethodOptions, methodName, apiOptions.name);
1646
+ return {
1647
+ kind: "request",
1648
+ methodName,
1649
+ methodType,
1650
+ route,
1651
+ methodOptions: resolvedMethodOptions,
1652
+ headers,
1653
+ descriptors: getMetadata(SNAIL_PARAMS, apiClass, methodName) ?? []
1654
+ };
1655
+ }
1656
+ /**
1657
+ * Build the stream controller behind an `@HttpStream` method.
1658
+ *
1659
+ * A real context is constructed rather than a look-alike, so the `@Query()` /
1660
+ * `@Data()` / `@HeaderValue()` resolvers behave exactly as they do for a normal
1661
+ * request. The plugin pipeline is deliberately skipped: a byte stream has no
1662
+ * envelope to cache or validate.
1663
+ */
1664
+ createHttpStream(apiClass, apiOptions, descriptor, args) {
1665
+ const streamOptions = descriptor.streamOptions;
1666
+ const methodOptions = {
1667
+ ...streamOptions,
1668
+ url: descriptor.route,
1669
+ method: descriptor.methodType
1670
+ };
1671
+ const config = buildBaseRequestConfig({
1672
+ serverOptions: this.options,
1673
+ apiOptions,
1674
+ methodOptions,
1675
+ methodType: descriptor.methodType,
1676
+ headers: descriptor.headers
1677
+ });
1678
+ config.url = descriptor.route;
1679
+ const ctx = new SnailContext({
1680
+ server: this,
1681
+ serverOptions: this.options,
1682
+ apiClass,
1683
+ api: void 0,
1684
+ apiName: apiOptions.name || apiClass.name,
1685
+ apiOptions,
1686
+ methodName: descriptor.methodName,
1687
+ methodType: descriptor.methodType,
1688
+ route: descriptor.route,
1689
+ request: config,
1690
+ descriptors: descriptor.descriptors,
1691
+ logger: this.logger
1692
+ });
1693
+ applyParamDescriptors(ctx, args);
1694
+ finalizeRequestURL(ctx);
1695
+ const headers = Object.fromEntries(Object.entries(ctx.request.headers.toJSON()).map(([key, value]) => [key, String(value)]));
1696
+ return createHttpStream({
1697
+ url: buildRequestURL(this.options.baseURL, ctx.request.url ?? descriptor.route),
1698
+ options: {
1699
+ ...streamOptions,
1700
+ method: descriptor.methodType
1701
+ },
1702
+ body: ctx.request.data,
1703
+ name: ctx.fullName,
1704
+ headers,
1705
+ logger: this.logger
1706
+ });
1707
+ }
1708
+ /** Construct the request object a proxied method call returns. */
1709
+ createMethod(apiClass, api, apiOptions, descriptor, args) {
1710
+ const { methodName, methodType, route, methodOptions, headers, descriptors } = descriptor;
1711
+ return new SnailMethod({
1712
+ server: this,
1713
+ pluginManager: this.pluginManager,
1714
+ axios: this.axios,
1715
+ apiClass,
1716
+ api,
1717
+ apiName: apiOptions.name || apiClass.name,
1718
+ apiOptions,
1719
+ serverOptions: this.options,
1720
+ methodName,
1721
+ methodType,
1722
+ route,
1723
+ methodOptions,
1724
+ descriptors,
1725
+ headers,
1726
+ logger: this.logger,
1727
+ requestConfig: () => buildBaseRequestConfig({
1728
+ serverOptions: this.options,
1729
+ apiOptions,
1730
+ methodOptions,
1731
+ methodType,
1732
+ headers
1733
+ })
1734
+ }, args);
1735
+ }
1736
+ };
1737
+ //#endregion
1738
+ //#region src/decorators/api.ts
1739
+ function Api(urlOrOptions) {
1740
+ const options = typeof urlOrOptions === "string" || urlOrOptions === void 0 ? { url: urlOrOptions ?? "" } : urlOrOptions;
1741
+ if (options.url !== void 0 && typeof options.url !== "string") throw new SnailDecoratorError(t("error.options.api.url"));
1742
+ return (target) => {
1743
+ if (typeof target !== "function") throw new SnailDecoratorError(t("error.decorator.class.target", "Api"));
1744
+ const previous = getOwnMetadata(SNAIL_API_OPTIONS, target);
1745
+ defineMetadata(SNAIL_API_OPTIONS, previous ? {
1746
+ ...previous,
1747
+ ...options
1748
+ } : { ...options }, target);
1749
+ };
1750
+ }
1751
+ //#endregion
1752
+ //#region src/decorators/server.ts
1753
+ function Server(baseURLOrOptions) {
1754
+ const options = typeof baseURLOrOptions === "string" ? { baseURL: baseURLOrOptions } : baseURLOrOptions;
1755
+ return (target) => {
1756
+ if (typeof target !== "function") throw new SnailDecoratorError(t("error.decorator.class.target", "Server"));
1757
+ const previous = getOwnMetadata(SNAIL_SERVER_OPTIONS, target);
1758
+ defineMetadata(SNAIL_SERVER_OPTIONS, previous ? {
1759
+ ...previous,
1760
+ ...options
1761
+ } : { ...options }, target);
1762
+ };
1763
+ }
1764
+ //#endregion
1765
+ //#region src/decorators/methods.ts
1766
+ /**
1767
+ * Build one request-method decorator.
1768
+ *
1769
+ * Decorated methods are never executed: `createApi` replaces them with a factory
1770
+ * that returns a request object. Their body exists purely to declare the argument
1771
+ * and return types, so `return null!` (or nothing at all) is the convention.
1772
+ *
1773
+ * Applying two request-method decorators to one method is an error and throws
1774
+ * while the class is being defined, which is the earliest possible moment.
1775
+ */
1776
+ function createRequestMethod(method) {
1777
+ return (path = "", options = {}) => {
1778
+ if (typeof path !== "string") throw new SnailDecoratorError(`@${method} expects its first argument to be a string path`);
1779
+ return (target, propertyKey) => {
1780
+ if (propertyKey === void 0) throw new SnailDecoratorError(`@${method} must decorate a method, not a class or a property`);
1781
+ if (getOwnMetadata(SNAIL_REQUEST_METHOD, target, propertyKey)) throw new SnailDecoratorError(t("error.decorator.method.duplicate", String(propertyKey)));
1782
+ defineMetadata(SNAIL_REQUEST_METHOD, {
1783
+ ...options,
1784
+ method,
1785
+ url: path
1786
+ }, target, propertyKey);
1787
+ };
1788
+ };
1789
+ }
1790
+ /** `GET` request. */
1791
+ var Get = createRequestMethod("GET");
1792
+ /** `POST` request. */
1793
+ var Post = createRequestMethod("POST");
1794
+ /** `PUT` request. */
1795
+ var Put = createRequestMethod("PUT");
1796
+ /** `DELETE` request. */
1797
+ var Delete = createRequestMethod("DELETE");
1798
+ /** `PATCH` request. */
1799
+ var Patch = createRequestMethod("PATCH");
1800
+ /** `HEAD` request. */
1801
+ var Head = createRequestMethod("HEAD");
1802
+ /** `OPTIONS` request. */
1803
+ var Options = createRequestMethod("OPTIONS");
1804
+ /** Alias kept for symmetry with `axios.request` style naming. */
1805
+ var Request = createRequestMethod;
1806
+ //#endregion
1807
+ //#region src/decorators/header.ts
1808
+ /**
1809
+ * Static request headers.
1810
+ *
1811
+ * Works on an api class and on a single method; the two levels merge, with the
1812
+ * method winning:
1813
+ *
1814
+ * ```ts
1815
+ * @Api("/user")
1816
+ * @Header({ "x-client": "web" })
1817
+ * class UserApi {
1818
+ * @Get("/secret")
1819
+ * @Header({ "x-scope": "admin" })
1820
+ * secret() {}
1821
+ * }
1822
+ * // → x-client: web, x-scope: admin
1823
+ * ```
1824
+ *
1825
+ * For a header whose value comes from an argument, use the `@HeaderValue()`
1826
+ * parameter decorator.
1827
+ */
1828
+ function Header(record) {
1829
+ return ((target, propertyKey) => {
1830
+ mergeMetadata(SNAIL_HEADERS, record, target, propertyKey);
1831
+ });
1832
+ }
1833
+ //#endregion
1834
+ //#region src/decorators/progress.ts
1835
+ /**
1836
+ * Report upload progress for the decorated method.
1837
+ *
1838
+ * ```ts
1839
+ * @Post("/upload")
1840
+ * @UploadProgress((e) => { if (e.total) bar.value = e.loaded / e.total; })
1841
+ * upload(@Data() file: FormData) {}
1842
+ * ```
1843
+ *
1844
+ * Progress events require the `xhr` adapter; axios' `fetch` adapter cannot
1845
+ * report them. Set `adapter: "xhr"` on the method or the server when using this
1846
+ * decorator in a browser.
1847
+ */
1848
+ function UploadProgress(callback) {
1849
+ return (target, propertyKey) => {
1850
+ defineMetadata(SNAIL_UPLOAD_PROGRESS, callback, target, propertyKey);
1851
+ };
1852
+ }
1853
+ /**
1854
+ * Report download progress for the decorated method.
1855
+ *
1856
+ * @see {@link UploadProgress} for the adapter caveat.
1857
+ */
1858
+ function DownloadProgress(callback) {
1859
+ return (target, propertyKey) => {
1860
+ defineMetadata(SNAIL_DOWNLOAD_PROGRESS, callback, target, propertyKey);
1861
+ };
1862
+ }
1863
+ //#endregion
1864
+ //#region src/decorators/stream.ts
1865
+ /**
1866
+ * Declare a Server-Sent Events endpoint.
1867
+ *
1868
+ * ```ts
1869
+ * @Sse("/events")
1870
+ * class Ticker {
1871
+ * @OnSseOpen()
1872
+ * open() { console.log("connected"); }
1873
+ *
1874
+ * @OnSseError()
1875
+ * failed(event: Event) { console.warn("lost", event); }
1876
+ *
1877
+ * @SseEvent() // the default `message` event
1878
+ * message(message: SnailSseMessage) {}
1879
+ *
1880
+ * @SseEvent("tick")
1881
+ * tick(message: SnailSseMessage) {}
1882
+ * }
1883
+ *
1884
+ * const ticker = Service.createSse(Ticker);
1885
+ * const connection = ticker.open();
1886
+ *
1887
+ * // Decorate with @SseEvent, or subscribe on the connection itself:
1888
+ * const off = connection.on("tick", (message) => console.log(message.data));
1889
+ * connection.close();
1890
+ * ```
1891
+ *
1892
+ * The transport is `fetch` + a stream reader rather than `EventSource`: that is
1893
+ * what makes request headers, `POST` and `withCredentials` possible, none of which
1894
+ * `EventSource` supports.
1895
+ */
1896
+ function Sse(path, options = {}) {
1897
+ return (target) => {
1898
+ if (typeof target !== "function") throw new SnailDecoratorError(t("error.decorator.class.target", "Sse"));
1899
+ if (getOwnMetadata(SNAIL_WS_OPTIONS, target)) throw new SnailDecoratorError(t("error.decorator.stream.duplicate", target.name));
1900
+ mergeMetadata(SNAIL_SSE_OPTIONS, {
1901
+ ...options,
1902
+ url: path
1903
+ }, target);
1904
+ };
1905
+ }
1906
+ /** Register a handler for the SSE `open` event. */
1907
+ function OnSseOpen() {
1908
+ return (target, propertyKey) => {
1909
+ appendSseHandler(target, propertyKey, (handlers, fn) => handlers.open.push(fn));
1910
+ };
1911
+ }
1912
+ /** Register a handler for the SSE `error` event. */
1913
+ function OnSseError() {
1914
+ return (target, propertyKey) => {
1915
+ appendSseHandler(target, propertyKey, (handlers, fn) => handlers.error.push(fn));
1916
+ };
1917
+ }
1918
+ /**
1919
+ * Register a handler for a named SSE event.
1920
+ *
1921
+ * @param event event name; omit for the default `message` event
1922
+ */
1923
+ function SseEvent(event = "message") {
1924
+ return (target, propertyKey) => {
1925
+ appendSseHandler(target, propertyKey, (handlers, fn) => {
1926
+ handlers.events.push({
1927
+ event,
1928
+ handler: fn
1929
+ });
1930
+ });
1931
+ };
1932
+ }
1933
+ function appendSseHandler(target, propertyKey, push) {
1934
+ if (propertyKey === void 0) throw new SnailDecoratorError("[snail] SSE handler decorators must be used on methods");
1935
+ const fn = target[propertyKey];
1936
+ if (typeof fn !== "function") throw new SnailDecoratorError(`[snail] @SseEvent/@OnSseOpen/@OnSseError must decorate a method, got "${String(propertyKey)}"`);
1937
+ const handlers = getOwnMetadata(SNAIL_SSE_HANDLERS, target) ?? {
1938
+ open: [],
1939
+ error: [],
1940
+ events: []
1941
+ };
1942
+ push(handlers, fn);
1943
+ defineMetadata(SNAIL_SSE_HANDLERS, handlers, target);
1944
+ }
1945
+ /**
1946
+ * Declare a WebSocket endpoint.
1947
+ *
1948
+ * ```ts
1949
+ * @WebSocket("/ws")
1950
+ * class ChatSocket {
1951
+ * @OnWsOpen() connected() {}
1952
+ * @OnWsMessage() incoming(event: MessageEvent) {}
1953
+ * @OnWsClose() gone(event: CloseEvent) {}
1954
+ * @OnWsError() failed(event: Event) {}
1955
+ * }
1956
+ *
1957
+ * const chat = Service.createWebSocket(ChatSocket);
1958
+ * const socket = chat.open();
1959
+ * socket.send({ hello: "world" });
1960
+ * ```
1961
+ */
1962
+ function WebSocket$1(path, options = {}) {
1963
+ return (target) => {
1964
+ if (typeof target !== "function") throw new SnailDecoratorError(t("error.decorator.class.target", "WebSocket"));
1965
+ if (getOwnMetadata(SNAIL_SSE_OPTIONS, target)) throw new SnailDecoratorError(t("error.decorator.stream.duplicate", target.name));
1966
+ mergeMetadata(SNAIL_WS_OPTIONS, {
1967
+ ...options,
1968
+ url: path
1969
+ }, target);
1970
+ };
1971
+ }
1972
+ /** Register a handler for the socket `open` event. */
1973
+ function OnWsOpen() {
1974
+ return wsHandlerDecorator("open");
1975
+ }
1976
+ /** Register a handler for incoming messages. */
1977
+ function OnWsMessage() {
1978
+ return wsHandlerDecorator("message");
1979
+ }
1980
+ /** Register a handler for the socket `close` event. */
1981
+ function OnWsClose() {
1982
+ return wsHandlerDecorator("close");
1983
+ }
1984
+ /** Register a handler for the socket `error` event. */
1985
+ function OnWsError() {
1986
+ return wsHandlerDecorator("error");
1987
+ }
1988
+ function wsHandlerDecorator(kind) {
1989
+ return (target, propertyKey) => {
1990
+ if (propertyKey === void 0) throw new SnailDecoratorError("[snail] WebSocket handler decorators must be used on methods");
1991
+ const fn = target[propertyKey];
1992
+ if (typeof fn !== "function") throw new SnailDecoratorError(`[snail] @OnWs* must decorate a method, got "${String(propertyKey)}"`);
1993
+ const handlers = getOwnMetadata(SNAIL_WS_HANDLERS, target) ?? {
1994
+ open: [],
1995
+ message: [],
1996
+ close: [],
1997
+ error: []
1998
+ };
1999
+ handlers[kind].push(fn);
2000
+ defineMetadata(SNAIL_WS_HANDLERS, handlers, target);
2001
+ };
2002
+ }
2003
+ /** Short alias for {@link WebSocket}. */
2004
+ var Ws = WebSocket$1;
2005
+ /**
2006
+ * Declare a streaming HTTP endpoint.
2007
+ *
2008
+ * ```ts
2009
+ * @Api("/ai")
2010
+ * class AiApi {
2011
+ * @HttpStream("/chat", { method: "POST" })
2012
+ * chat(@Data() prompt: { text: string }) {}
2013
+ * }
2014
+ *
2015
+ * const stream = aiApi.chat({ text: "hi" });
2016
+ * for await (const chunk of stream.stream()) { render(chunk); }
2017
+ * ```
2018
+ *
2019
+ * Unlike `@Get`/`@Post`, the proxied method returns a stream controller instead
2020
+ * of a `SnailMethod`, so no envelope validation happens.
2021
+ */
2022
+ function HttpStream(path = "", options = {}) {
2023
+ return (target, propertyKey) => {
2024
+ if (propertyKey === void 0) throw new SnailDecoratorError("[snail] @HttpStream must decorate a method");
2025
+ defineMetadata(SNAIL_HTTP_STREAM, {
2026
+ ...options,
2027
+ url: path
2028
+ }, target, propertyKey);
2029
+ };
2030
+ }
2031
+ //#endregion
2032
+ export { Api, DEFAULT_ACCEPTED_CODES, DEFAULT_API_OPTIONS, DEFAULT_RESPONSE_KEYS, DEFAULT_SERVER_OPTIONS, Data, Delete, DownloadProgress, Emitter, Get, Head, Header, HeaderParam, HeaderValue, HttpStream, LOG_LEVEL_WEIGHT, Localization, OnSseError, OnSseOpen, OnWsClose, OnWsError, OnWsMessage, OnWsOpen, Options, Params, Patch, PluginManager, Post, Put, Query, Request, SNAIL_API_OPTIONS, SNAIL_CUSTOM_KEY_PREFIX, SNAIL_DOWNLOAD_PROGRESS, SNAIL_HEADERS, SNAIL_HTTP_STREAM, SNAIL_PARAMS, SNAIL_REQUEST_METHOD, SNAIL_SERVER_OPTIONS, SNAIL_SSE_HANDLERS, SNAIL_SSE_OPTIONS, SNAIL_UPLOAD_PROGRESS, SNAIL_WS_HANDLERS, SNAIL_WS_OPTIONS, Server, SnailCancelledError, SnailContext, SnailDecoratorError, SnailError, SnailHookError, SnailHttpError, SnailMethod, SnailOptionsError, SnailPluginError, SnailRequestError, SnailResponseError, SnailServer, SnailTimeoutError, Sse, SseEvent, StateBag, UploadProgress, WebSocket$1 as WebSocket, Ws, appendMetadata, buildRequestURL, capitalize, clearMetadataRegistry, collectMethodKeys, composeChain, createClassDecorator, createLogger, createMethodDecorator, createParamDecorator, createParamDecoratorFor, createPlugin, createPropertyDecorator, customMetadataKey, deepMerge, deferred, defineMetadata, defineParamDescriptor, definePlugin, deleteMetadata, en, filenameFromDisposition, getClassMetadata, getLocale, getMetadata, getMethodMetadata, getOwnMetadata, getOwnMethodMetadata, hasMetadata, hasParamResolver, isAbsoluteURL, isBinaryBody, isBrowser, isDefined, isFunction, isObject, isPlainObject, isPromise, joinURL, languages, localization, mergeMetadata, normalizeParamInput, omit, omitUndefined, paramResolvers, paramSources, pathParamNames, pick, registerMessages, registerParamResolver, replacePathParams, resolveOwner, setLocale, shortHash, stableStringify, stripQuery, t, triggerBlobDownload, triggerDownload, tryCatch, zh };
2033
+
2034
+ //# sourceMappingURL=index.js.map