@snail-js/api 0.1.27 → 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 -4868
  296. package/dist/snail-api.umd.cjs +0 -4872
  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
@@ -0,0 +1,2265 @@
1
+ import { A as stableStringify, B as SnailError, L as isPlainObject, T as isAbsoluteURL, b as t, k as shortHash, n as createPlugin, z as SnailDecoratorError } from "../chunks/plugin-D3RDffDG.js";
2
+ import { n as deferred } from "../chunks/object-Ct0w-h4i.js";
3
+ import { i as looksLikeEnvelope, o as unwrapEnvelope, r as coerceJSONStringBody } from "../chunks/response-D-ubkAFl.js";
4
+ import { C as getMetadata, V as createLogger, _ as SnailPluginError, a as customMetadataKey, b as collectMethodKeys, i as createPropertyDecorator, n as createMethodDecorator, o as getClassMetadata, s as getMethodMetadata, t as createClassDecorator, x as defineMetadata } from "../chunks/custom-UchOSg_s.js";
5
+ import { AxiosHeaders } from "axios";
6
+ //#region src/plugins/cache/adapters/indexeddb.ts
7
+ /**
8
+ * L2 adapter over IndexedDB.
9
+ *
10
+ * ## Why every method degrades instead of rejecting
11
+ *
12
+ * IndexedDB is absent in Node and can be unavailable in private browsing modes.
13
+ * An L2 store is an optimisation: a request must succeed (and stay correct) when
14
+ * it is missing. Every public method therefore resolves to "nothing stored" or a
15
+ * no-op rather than throwing, and the connection is opened lazily so importing
16
+ * this module — or constructing the adapter — never touches a global.
17
+ *
18
+ * ## Why the timeout is stored per record, not as an index
19
+ *
20
+ * A TTL index would make reads of an expired key need a second query anyway. The
21
+ * record carries its own `expiresAt`, which makes the freshness check a pure
22
+ * in-memory comparison on the value already fetched.
23
+ */
24
+ var IndexedDBCacheAdapter = class {
25
+ constructor(options = {}) {
26
+ this.databaseName = options.databaseName ?? "snail-js-api";
27
+ this.storeName = options.storeName ?? "cache";
28
+ this.version = options.version ?? 1;
29
+ }
30
+ /** `false` when the environment has no IndexedDB at all. */
31
+ get available() {
32
+ try {
33
+ return typeof globalThis.indexedDB !== "undefined";
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ async get(key) {
39
+ try {
40
+ const database = await this.open();
41
+ const record = await this.read(database, key);
42
+ if (record === void 0) return void 0;
43
+ if (record.expiresAt !== 0 && record.expiresAt <= Date.now()) {
44
+ await this.delete(key);
45
+ return;
46
+ }
47
+ return record.value;
48
+ } catch {
49
+ return;
50
+ }
51
+ }
52
+ async set(key, value, ttlSeconds) {
53
+ if (!this.available) return;
54
+ const database = await this.open();
55
+ const record = {
56
+ value,
57
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : 0
58
+ };
59
+ await this.transaction(database, "readwrite", (store) => {
60
+ store.put(record, key);
61
+ });
62
+ }
63
+ async delete(key) {
64
+ try {
65
+ const database = await this.open();
66
+ await this.transaction(database, "readwrite", (store) => {
67
+ store.delete(key);
68
+ });
69
+ } catch {}
70
+ }
71
+ async clear() {
72
+ try {
73
+ const database = await this.open();
74
+ await this.transaction(database, "readwrite", (store) => {
75
+ store.clear();
76
+ });
77
+ } catch {}
78
+ }
79
+ async keys() {
80
+ try {
81
+ return (await requestToPromise((await this.open()).transaction(this.storeName, "readonly").objectStore(this.storeName).getAllKeys())).map(String);
82
+ } catch {
83
+ return [];
84
+ }
85
+ }
86
+ /**
87
+ * Open (once) and memoise the database connection.
88
+ *
89
+ * The in-flight promise is stored before it settles, so two concurrent
90
+ * requests cannot open the same database twice and leak a connection.
91
+ */
92
+ open() {
93
+ if (this.database) return Promise.resolve(this.database);
94
+ if (this.opening) return this.opening;
95
+ this.opening = new Promise((resolve, reject) => {
96
+ const factory = globalThis.indexedDB;
97
+ if (!factory) {
98
+ reject(/* @__PURE__ */ new Error("[snail] IndexedDB is not available in this environment"));
99
+ return;
100
+ }
101
+ const request = factory.open(this.databaseName, this.version);
102
+ request.onupgradeneeded = () => {
103
+ const database = request.result;
104
+ if (!database.objectStoreNames.contains(this.storeName)) database.createObjectStore(this.storeName);
105
+ };
106
+ request.onsuccess = () => {
107
+ this.database = request.result;
108
+ resolve(request.result);
109
+ };
110
+ request.onerror = () => reject(request.error);
111
+ request.onblocked = () => reject(/* @__PURE__ */ new Error("[snail] IndexedDB upgrade is blocked"));
112
+ }).finally(() => {
113
+ this.opening = void 0;
114
+ });
115
+ return this.opening;
116
+ }
117
+ async read(database, key) {
118
+ const value = await requestToPromise(database.transaction(this.storeName, "readonly").objectStore(this.storeName).get(key));
119
+ if (value === void 0 || value === null || typeof value !== "object") return void 0;
120
+ if (!("value" in value) || !("expiresAt" in value)) return void 0;
121
+ return value;
122
+ }
123
+ transaction(database, mode, run) {
124
+ return new Promise((resolve, reject) => {
125
+ const transaction = database.transaction(this.storeName, mode);
126
+ run(transaction.objectStore(this.storeName));
127
+ transaction.oncomplete = () => resolve();
128
+ transaction.onerror = () => reject(transaction.error);
129
+ transaction.onabort = () => reject(transaction.error);
130
+ });
131
+ }
132
+ };
133
+ /** Bridge a one-shot `IDBRequest` into a promise. */
134
+ function requestToPromise(request) {
135
+ return new Promise((resolve, reject) => {
136
+ request.onsuccess = () => resolve(request.result);
137
+ request.onerror = () => reject(request.error);
138
+ });
139
+ }
140
+ //#endregion
141
+ //#region src/plugins/cache/adapters/memory.ts
142
+ /** L1 capacity used whenever the configured one is missing or not positive. */
143
+ var DEFAULT_L1_MAX_SIZE = 100;
144
+ /**
145
+ * The L1 store: a `Map` with TTL and LRU capacity.
146
+ *
147
+ * ## Why there is no sweep timer
148
+ *
149
+ * The pre-rewrite memory adapter ran a `setInterval` that (a) kept a Node
150
+ * process alive forever and (b) made tests hang until vitest force-killed them.
151
+ * Expiry is therefore swept *lazily*, at the start of every operation: at most
152
+ * `maxSize` records are inspected, which is cheaper than the timer it replaces
153
+ * for any realistic capacity, and it cannot outlive the store.
154
+ *
155
+ * ## Why a `Map` and not a `WeakMap`
156
+ *
157
+ * LRU needs enumeration and ordering. `Map` preserves insertion order, so
158
+ * "least recently used" is simply "first key" and touching an entry is
159
+ * delete + set — no linked list to keep in sync with the data.
160
+ */
161
+ var MemoryCacheAdapter = class {
162
+ constructor(options = {}) {
163
+ this.records = /* @__PURE__ */ new Map();
164
+ const maxSize = options.maxSize;
165
+ this.maxSize = typeof maxSize === "number" && Number.isFinite(maxSize) && maxSize > 0 ? Math.floor(maxSize) : 100;
166
+ this.onEvict = options.onEvict;
167
+ }
168
+ /** Number of live entries. */
169
+ get size() {
170
+ this.sweep();
171
+ return this.records.size;
172
+ }
173
+ /** `true` when the key is present and still fresh. */
174
+ async has(key) {
175
+ return await this.get(key) !== void 0;
176
+ }
177
+ async get(key) {
178
+ this.sweep();
179
+ const record = this.records.get(key);
180
+ if (!record) return void 0;
181
+ this.records.delete(key);
182
+ this.records.set(key, record);
183
+ return record.value;
184
+ }
185
+ async set(key, value, ttlSeconds) {
186
+ this.sweep();
187
+ if (!this.records.delete(key)) this.evictForCapacity(1);
188
+ this.records.set(key, {
189
+ value,
190
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : 0
191
+ });
192
+ }
193
+ async delete(key) {
194
+ this.records.delete(key);
195
+ }
196
+ async clear() {
197
+ this.records.clear();
198
+ }
199
+ async keys() {
200
+ this.sweep();
201
+ return [...this.records.keys()];
202
+ }
203
+ /** Drop expired records. Called before every operation, never on a timer. */
204
+ sweep() {
205
+ const now = Date.now();
206
+ for (const [key, record] of this.records) if (record.expiresAt !== 0 && record.expiresAt <= now) {
207
+ this.records.delete(key);
208
+ this.onEvict?.(key);
209
+ }
210
+ }
211
+ /** Make room for `incoming` new entries by dropping the oldest ones. */
212
+ evictForCapacity(incoming) {
213
+ while (this.records.size + incoming > this.maxSize) {
214
+ const oldest = this.records.keys().next();
215
+ if (oldest.done) return;
216
+ this.records.delete(oldest.value);
217
+ this.onEvict?.(oldest.value);
218
+ }
219
+ }
220
+ };
221
+ //#endregion
222
+ //#region src/plugins/cache/adapters/web-storage.ts
223
+ /**
224
+ * L2 adapter over any Web Storage area.
225
+ *
226
+ * One class drives both `localStorage` and `sessionStorage`: they share the
227
+ * `Storage` interface, and wrapping them twice would duplicate every guard below.
228
+ *
229
+ * ## Why the area is resolved through a getter
230
+ *
231
+ * `localStorage` does not exist in Node, and in a browser it *throws* on access
232
+ * when the user has blocked site data. Reading it in the constructor would make
233
+ * `import`-time behaviour environment-dependent — and a Node test could not even
234
+ * construct the adapter to assert that it degrades. The getter is therefore
235
+ * called per operation and its failure is swallowed; the store behaves as an
236
+ * always-empty cache, which the manager turns into "L1 only".
237
+ */
238
+ var WebStorageCacheAdapter = class {
239
+ constructor(resolveStorage, options = {}) {
240
+ this.resolveStorage = resolveStorage;
241
+ this.prefix = options.prefix ?? "[snail-cache]";
242
+ this.label = options.label ?? "web storage";
243
+ }
244
+ /** `false` when the backing area is missing — the manager then drops L2. */
245
+ get available() {
246
+ return this.storage() !== void 0;
247
+ }
248
+ async get(key) {
249
+ const storage = this.storage();
250
+ if (!storage) return void 0;
251
+ const raw = storage.getItem(this.key(key));
252
+ if (raw === null) return void 0;
253
+ const record = this.parse(raw);
254
+ if (record === void 0) {
255
+ storage.removeItem(this.key(key));
256
+ return;
257
+ }
258
+ if (record.expiresAt !== 0 && record.expiresAt <= Date.now()) {
259
+ storage.removeItem(this.key(key));
260
+ return;
261
+ }
262
+ return record.value;
263
+ }
264
+ async set(key, value, ttlSeconds) {
265
+ const storage = this.storage();
266
+ if (!storage) return;
267
+ const record = {
268
+ value,
269
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : 0
270
+ };
271
+ storage.setItem(this.key(key), JSON.stringify(record));
272
+ }
273
+ async delete(key) {
274
+ this.storage()?.removeItem(this.key(key));
275
+ }
276
+ async clear() {
277
+ const storage = this.storage();
278
+ if (!storage) return;
279
+ for (const key of this.keysOf(storage)) storage.removeItem(key);
280
+ }
281
+ async keys() {
282
+ const storage = this.storage();
283
+ if (!storage) return [];
284
+ return this.keysOf(storage).map((key) => key.slice(this.prefix.length));
285
+ }
286
+ storage() {
287
+ try {
288
+ return this.resolveStorage();
289
+ } catch {
290
+ return;
291
+ }
292
+ }
293
+ key(key) {
294
+ return `${this.prefix}${key}`;
295
+ }
296
+ /** Every storage key this adapter owns, prefix included. */
297
+ keysOf(storage) {
298
+ const owned = [];
299
+ for (let index = 0; index < storage.length; index++) {
300
+ const key = storage.key(index);
301
+ if (key !== null && key.startsWith(this.prefix)) owned.push(key);
302
+ }
303
+ return owned;
304
+ }
305
+ parse(raw) {
306
+ try {
307
+ const parsed = JSON.parse(raw);
308
+ if (parsed === null || typeof parsed !== "object") return void 0;
309
+ if (!("value" in parsed) || !("expiresAt" in parsed)) return void 0;
310
+ return parsed;
311
+ } catch {
312
+ return;
313
+ }
314
+ }
315
+ };
316
+ //#endregion
317
+ //#region src/plugins/cache/decorators.ts
318
+ /**
319
+ * `@Cacheable()` / `@NoCache()` / `@Invalidates()` / `@HitSource()` — the
320
+ * declarative half of the cache plugin.
321
+ *
322
+ * All four work on a class and on a method. TypeScript distinguishes the two by
323
+ * arity (a class decorator is called with the constructor alone), which is the
324
+ * only signal available now that `emitDecoratorMetadata` emits nothing in TS 7.
325
+ *
326
+ * ## Why `@Invalidates` appends instead of replacing
327
+ *
328
+ * `@Invalidates("users", "orders")` is two decorator applications on one target.
329
+ * Using the merging factory means the reader gets `["users", "orders"]` without
330
+ * the plugin having to flatten a nested array — and two separate
331
+ * `@Invalidates()` lines compose the same way, which is how a method that
332
+ * touches several caches is naturally written.
333
+ */
334
+ var CACHEABLE_KEY = "snail-cache/cacheable";
335
+ var NO_CACHE_KEY = "snail-cache/disabled";
336
+ var INVALIDATES_KEY = "snail-cache/invalidates";
337
+ var addCacheableOnClass = createClassDecorator(CACHEABLE_KEY, false);
338
+ var addCacheableOnMethod = createMethodDecorator(CACHEABLE_KEY, false);
339
+ var addNoCacheOnClass = createClassDecorator(NO_CACHE_KEY, false);
340
+ var addNoCacheOnMethod = createMethodDecorator(NO_CACHE_KEY, false);
341
+ var addInvalidatesOnClass = createClassDecorator(INVALIDATES_KEY);
342
+ var addInvalidatesOnMethod = createMethodDecorator(INVALIDATES_KEY);
343
+ /** Fold a class application and a method application into one decorator. */
344
+ function dualDecorator$1(applyToClass, applyToMethod) {
345
+ return ((target, propertyKey) => {
346
+ if (propertyKey === void 0) {
347
+ applyToClass(target);
348
+ return;
349
+ }
350
+ applyToMethod(target, propertyKey, void 0);
351
+ });
352
+ }
353
+ /** Reject the misuse that would otherwise only surface at request time. */
354
+ function assertCacheableOptions(options) {
355
+ if (options.ttl !== void 0 && !(typeof options.ttl === "number" && options.ttl > 0)) throw new SnailDecoratorError("[snail] @Cacheable({ ttl }) must be a positive number of seconds");
356
+ if (options.key !== void 0 && (typeof options.key !== "string" || options.key.length === 0)) throw new SnailDecoratorError("[snail] @Cacheable({ key }) must be a non-empty string");
357
+ if (options.tags !== void 0) {
358
+ if (!Array.isArray(options.tags)) throw new SnailDecoratorError("[snail] @Cacheable({ tags }) must be an array of strings");
359
+ for (const tag of options.tags) if (typeof tag !== "string" || tag.length === 0) throw new SnailDecoratorError("[snail] @Cacheable({ tags }) must contain non-empty strings");
360
+ }
361
+ }
362
+ /**
363
+ * Mark this method — or every method of this class — as cacheable.
364
+ *
365
+ * Passing the decorator is an explicit opt-in: it caches even a verb that
366
+ * `CacheOptions.cacheFor` does not list. Without it, `cacheFor` decides, and the
367
+ * reason is that "cache every POST of this class" is far too easy to write by
368
+ * accident when the intent was only to attach tags.
369
+ *
370
+ * ```ts
371
+ * @Api("/user")
372
+ * @Cacheable({ tags: ["users"] })
373
+ * class UserApi {
374
+ * @Get("/") list(): Promise<User[]> { return null!; }
375
+ *
376
+ * @Get("/stats")
377
+ * @Cacheable({ ttl: 5 })
378
+ * stats(): Promise<Stats> { return null!; }
379
+ * }
380
+ * ```
381
+ */
382
+ function Cacheable(options = {}) {
383
+ assertCacheableOptions(options);
384
+ const value = {
385
+ ttl: options.ttl,
386
+ key: options.key,
387
+ tags: options.tags === void 0 ? void 0 : [...options.tags]
388
+ };
389
+ return dualDecorator$1(addCacheableOnClass(value), addCacheableOnMethod(value));
390
+ }
391
+ /**
392
+ * Opt this method — or this entire class — out of caching.
393
+ *
394
+ * A method-level `@NoCache()` beats a class-level `@Cacheable()`, and a
395
+ * class-level `@NoCache()` beats the `cacheFor` default. Opting out is the one
396
+ * decision that may never be overridden by a broader rule, because the cost of
397
+ * being wrong is serving stale data.
398
+ */
399
+ function NoCache() {
400
+ return dualDecorator$1(addNoCacheOnClass(true), addNoCacheOnMethod(true));
401
+ }
402
+ /**
403
+ * Purge every cached entry carrying any of `tags` once this request succeeds.
404
+ *
405
+ * "Succeeds" means the HTTP round-trip did: the hook lives on the response path,
406
+ * so a failed or cancelled request invalidates nothing. A method may invalidate a
407
+ * tag it also stores under — the plugin purges before it stores, so the fresh
408
+ * entry survives its own invalidation.
409
+ */
410
+ function Invalidates(...tags) {
411
+ for (const tag of tags) if (typeof tag !== "string" || tag.length === 0) throw new SnailDecoratorError("[snail] @Invalidates(...tags) needs non-empty strings");
412
+ return ((target, propertyKey) => {
413
+ for (const tag of tags) if (propertyKey === void 0) addInvalidatesOnClass(tag)(target);
414
+ else addInvalidatesOnMethod(tag)(target, propertyKey, void 0);
415
+ });
416
+ }
417
+ /**
418
+ * Legacy-compatible alias of {@link Invalidates} for a single source name.
419
+ *
420
+ * The pre-rewrite decorator was named after the *source* of a change rather than
421
+ * the entries it purges; application code written against it keeps working, and
422
+ * new code should prefer `@Invalidates`.
423
+ */
424
+ function HitSource(name) {
425
+ return Invalidates(name);
426
+ }
427
+ /**
428
+ * Read `@Cacheable(...)` from a method (when `methodName` is given) or a class.
429
+ *
430
+ * `undefined` means "not marked", which is different from `{}` ("marked, with no
431
+ * overrides") — the plugin treats the two differently.
432
+ */
433
+ function readCacheable(target, methodName) {
434
+ return methodName === void 0 ? getClassMetadata(CACHEABLE_KEY, target) : getMethodMetadata(CACHEABLE_KEY, target, methodName);
435
+ }
436
+ /** `true` when `@NoCache()` was applied to this method or class. */
437
+ function readNoCache(target, methodName) {
438
+ return (methodName === void 0 ? getClassMetadata(NO_CACHE_KEY, target) : getMethodMetadata(NO_CACHE_KEY, target, methodName)) === true;
439
+ }
440
+ /** Tags listed by `@Invalidates(...)` / `@HitSource(...)`, in application order. */
441
+ function readInvalidates(target, methodName) {
442
+ return (methodName === void 0 ? getClassMetadata(INVALIDATES_KEY, target) : getMethodMetadata(INVALIDATES_KEY, target, methodName)) ?? [];
443
+ }
444
+ //#endregion
445
+ //#region src/plugins/cache/key.ts
446
+ /**
447
+ * Build the key used for one request.
448
+ *
449
+ * The readable part (`prefix:VERB`) is kept outside the hash so a misbehaving
450
+ * cache can be diagnosed from a key alone; only the volatile signature is hashed,
451
+ * which keeps keys short enough for `localStorage`.
452
+ */
453
+ function buildCacheKey(input) {
454
+ const { prefix, request, methodType, explicitKey } = input;
455
+ if (explicitKey !== void 0 && explicitKey.length > 0) return `${prefix}:${explicitKey}`;
456
+ const method = (request.method ?? methodType).toUpperCase();
457
+ const signature = stableStringify({
458
+ method,
459
+ baseURL: request.baseURL ?? "",
460
+ url: request.url ?? "",
461
+ params: request.params ?? null,
462
+ data: request.data ?? null
463
+ });
464
+ return `${prefix}:${method}:${shortHash(signature)}`;
465
+ }
466
+ //#endregion
467
+ //#region src/plugins/cache/manager.ts
468
+ /** Verbs cached when `cacheFor` is not configured. */
469
+ var DEFAULT_CACHE_FOR = ["GET"];
470
+ /**
471
+ * Storage engine behind the cache plugin.
472
+ *
473
+ * ## Layers
474
+ *
475
+ * - **L1** is a {@link MemoryCacheAdapter}: always present unless `l1: false`,
476
+ * bounded by `maxSize`, and the only place an L2 hit is promoted into.
477
+ * - **L2** is any {@link CacheAdapter}, usually a persistent store.
478
+ *
479
+ * ## What the manager owns that the adapters do not
480
+ *
481
+ * TTL, LRU and tags are policy, not storage. Keeping them here means a custom L2
482
+ * adapter only has to answer "get/set/delete" and never has to know what a tag
483
+ * is — which is what makes the adapter interface small enough to implement over
484
+ * anything (IndexedDB, `localStorage`, an HTTP cache).
485
+ *
486
+ * ## Freshness vs. `staleWhileRevalidate`
487
+ *
488
+ * Freshness is always decided by this class, using the `expiresAt` it records on
489
+ * `set`. In stale-while-revalidate mode L1 is told "never expire" so the stale
490
+ * copy survives to be served while the plugin refreshes it; otherwise L1 is
491
+ * given the real TTL and sweeps the entry itself. L2 always receives the real TTL
492
+ * so a second tab (which has no shared in-memory index) never reads a stale one.
493
+ */
494
+ var CacheManager = class {
495
+ constructor(options = {}) {
496
+ this.expiry = /* @__PURE__ */ new Map();
497
+ this.tagIndex = /* @__PURE__ */ new Map();
498
+ this.keyTags = /* @__PURE__ */ new Map();
499
+ this.flight = /* @__PURE__ */ new Map();
500
+ this.logger = options.logger ?? createLogger("silent");
501
+ this.options = resolveOptions(options, this.logger);
502
+ this.l1 = this.options.l1 ? new MemoryCacheAdapter({
503
+ maxSize: this.options.maxSize,
504
+ onEvict: (key) => this.forgetKey(key)
505
+ }) : void 0;
506
+ }
507
+ /** Number of live **L1** entries. L2 is not enumerated, by design. */
508
+ get size() {
509
+ return this.l1?.size ?? 0;
510
+ }
511
+ /**
512
+ * Read a fresh value.
513
+ *
514
+ * A stale entry (kept alive only for stale-while-revalidate) is *not* returned:
515
+ * callers that must serve staleness ask {@link lookup} explicitly, so the
516
+ * common path cannot accidentally serve expired data.
517
+ */
518
+ async get(key) {
519
+ return (await this.lookup(key))?.value;
520
+ }
521
+ /** `true` when a fresh value is stored under `key`. */
522
+ async has(key) {
523
+ return await this.get(key) !== void 0;
524
+ }
525
+ /**
526
+ * Read a value together with its freshness.
527
+ *
528
+ * `allowStale` is the plugin's `staleWhileRevalidate` switch: when the entry is
529
+ * past its TTL but still resident, it is returned with `stale: true` so the
530
+ * caller can serve it now and refresh afterwards.
531
+ */
532
+ async lookup(key, allowStale = false) {
533
+ const fromL1 = await this.readL1(key);
534
+ if (fromL1.found) {
535
+ const stale = this.isStale(key);
536
+ if (!stale || allowStale) return {
537
+ value: fromL1.value,
538
+ stale
539
+ };
540
+ }
541
+ const fromL2 = await this.readL2(key);
542
+ if (!fromL2.found) return void 0;
543
+ this.markExpiry(key, this.options.ttl);
544
+ const l1Ttl = this.options.staleWhileRevalidate ? 0 : this.options.ttl;
545
+ await this.writeL1(key, fromL2.value, l1Ttl);
546
+ return {
547
+ value: fromL2.value,
548
+ stale: false
549
+ };
550
+ }
551
+ /**
552
+ * Store a value in L1 and L2.
553
+ *
554
+ * `ttlSeconds` defaults to the configured TTL and `tags` may be empty; a tag
555
+ * written here is what `@Invalidates("tag")` later purges.
556
+ */
557
+ async set(key, value, ttlSeconds = this.options.ttl, tags = []) {
558
+ this.markExpiry(key, ttlSeconds);
559
+ this.rememberTags(key, tags);
560
+ await this.writeL1(key, value, this.options.staleWhileRevalidate ? 0 : ttlSeconds);
561
+ await this.writeL2(key, value, ttlSeconds);
562
+ }
563
+ /** Remove one entry from every layer. */
564
+ async delete(key) {
565
+ this.forgetKey(key);
566
+ await this.l1?.delete(key);
567
+ if (!this.options.l2) return;
568
+ try {
569
+ await this.options.l2.delete(key);
570
+ } catch (error) {
571
+ this.warnL2("delete", error);
572
+ }
573
+ }
574
+ /** Remove every entry this manager can see. */
575
+ async clear() {
576
+ await this.l1?.clear();
577
+ this.expiry.clear();
578
+ this.tagIndex.clear();
579
+ this.keyTags.clear();
580
+ if (!this.options.l2) return;
581
+ try {
582
+ await this.options.l2.clear();
583
+ } catch (error) {
584
+ this.warnL2("clear", error);
585
+ }
586
+ }
587
+ /**
588
+ * Purge every entry carrying any of `tags`.
589
+ *
590
+ * Used by `@Invalidates(...)` / `@HitSource(...)` after a successful request.
591
+ * Keys are collected first because deleting mutates the tags they came from.
592
+ */
593
+ async invalidateTags(tags) {
594
+ const keys = /* @__PURE__ */ new Set();
595
+ for (const tag of tags) for (const key of this.tagIndex.get(tag) ?? []) keys.add(key);
596
+ for (const key of keys) await this.delete(key);
597
+ }
598
+ /** Alias of {@link clear}, spelled the way `@Invalidates("*")`-style callers expect. */
599
+ async invalidateAll() {
600
+ await this.clear();
601
+ }
602
+ /** The promise of the request currently being sent for `key`, if any. */
603
+ getInFlight(key) {
604
+ return this.flight.get(key);
605
+ }
606
+ /**
607
+ * Publish the promise of the request being sent for `key`.
608
+ *
609
+ * The entry removes itself when the promise settles, so a rejected shared
610
+ * promise cannot be picked up by a later, unrelated request. The rejection is
611
+ * also marked as observed: a leader that fails with no follower waiting would
612
+ * otherwise surface as an unhandled rejection in Node.
613
+ */
614
+ setInFlight(key, promise) {
615
+ const tracked = promise.then((value) => {
616
+ this.flight.delete(key);
617
+ return value;
618
+ }, (error) => {
619
+ this.flight.delete(key);
620
+ throw error;
621
+ });
622
+ tracked.catch(() => void 0);
623
+ this.flight.set(key, tracked);
624
+ }
625
+ async readL1(key) {
626
+ if (!this.l1) return { found: false };
627
+ const value = await this.l1.get(key);
628
+ return value === void 0 ? { found: false } : {
629
+ found: true,
630
+ value
631
+ };
632
+ }
633
+ async readL2(key) {
634
+ const l2 = this.options.l2;
635
+ if (!l2) return { found: false };
636
+ try {
637
+ const value = await l2.get(key);
638
+ return value === void 0 ? { found: false } : {
639
+ found: true,
640
+ value
641
+ };
642
+ } catch (error) {
643
+ this.warnL2("read", error);
644
+ return { found: false };
645
+ }
646
+ }
647
+ async writeL1(key, value, ttlSeconds) {
648
+ if (!this.l1) return;
649
+ await this.l1.set(key, value, ttlSeconds);
650
+ }
651
+ async writeL2(key, value, ttlSeconds) {
652
+ const l2 = this.options.l2;
653
+ if (!l2) return;
654
+ try {
655
+ await l2.set(key, value, ttlSeconds);
656
+ } catch (error) {
657
+ this.warnL2("write", error);
658
+ }
659
+ }
660
+ markExpiry(key, ttlSeconds) {
661
+ this.expiry.set(key, ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : 0);
662
+ }
663
+ isStale(key) {
664
+ const expiresAt = this.expiry.get(key);
665
+ return expiresAt !== void 0 && expiresAt !== 0 && expiresAt <= Date.now();
666
+ }
667
+ rememberTags(key, tags) {
668
+ if (tags.length === 0) return;
669
+ const owned = this.keyTags.get(key) ?? /* @__PURE__ */ new Set();
670
+ for (const tag of tags) {
671
+ owned.add(tag);
672
+ const keys = this.tagIndex.get(tag) ?? /* @__PURE__ */ new Set();
673
+ keys.add(key);
674
+ this.tagIndex.set(tag, keys);
675
+ }
676
+ this.keyTags.set(key, owned);
677
+ }
678
+ forgetKey(key) {
679
+ this.expiry.delete(key);
680
+ const tags = this.keyTags.get(key);
681
+ if (!tags) return;
682
+ for (const tag of tags) {
683
+ const keys = this.tagIndex.get(tag);
684
+ if (!keys) continue;
685
+ keys.delete(key);
686
+ if (keys.size === 0) this.tagIndex.delete(tag);
687
+ }
688
+ this.keyTags.delete(key);
689
+ }
690
+ warnL2(operation, error) {
691
+ this.logger.warn(t("cache.warn.l2.failed", this.options.prefix, `${operation}: ${String(error)}`));
692
+ }
693
+ };
694
+ /** Apply every default and turn the L2 selector into a live adapter. */
695
+ function resolveOptions(options, logger) {
696
+ const prefix = options.prefix ?? "[snail-cache]";
697
+ return {
698
+ ttl: positive(options.ttl, 60),
699
+ maxSize: positive(options.maxSize, 100),
700
+ l1: options.l1 ?? true,
701
+ l2: createL2(options.l2, prefix, logger),
702
+ cacheFor: normalizeCacheFor(options.cacheFor),
703
+ prefix,
704
+ staleWhileRevalidate: options.staleWhileRevalidate ?? false,
705
+ dedupe: options.dedupe ?? true
706
+ };
707
+ }
708
+ /**
709
+ * Turn the `l2` selector into an adapter.
710
+ *
711
+ * A missing environment global becomes "no L2 at all" plus one warning, rather
712
+ * than an adapter that silently misses on every read: the warning is the only
713
+ * signal an application gets that its persistence intent was not honoured.
714
+ */
715
+ function createL2(selector, prefix, logger) {
716
+ if (selector === void 0) return void 0;
717
+ if (typeof selector !== "string") return selector;
718
+ const normalized = selector.toLowerCase();
719
+ if (normalized === "localstorage" || normalized === "sessionstorage") {
720
+ const area = normalized === "localstorage" ? "localStorage" : "sessionStorage";
721
+ const adapter = new WebStorageCacheAdapter(() => globalThis[area], {
722
+ prefix,
723
+ label: area
724
+ });
725
+ if (!adapter.available) {
726
+ logger.warn(t("cache.warn.l2.unavailable", prefix, area));
727
+ return;
728
+ }
729
+ return adapter;
730
+ }
731
+ if (normalized === "indexeddb") {
732
+ const adapter = new IndexedDBCacheAdapter({ storeName: prefix });
733
+ if (!adapter.available) {
734
+ logger.warn(t("cache.warn.l2.unavailable", prefix, "indexedDB"));
735
+ return;
736
+ }
737
+ return adapter;
738
+ }
739
+ throw new SnailPluginError(t("error.plugin.cache.adapter", selector), { pluginName: "cache" });
740
+ }
741
+ /** Normalise `cacheFor` into `"all"` or an uppercase verb list. */
742
+ function normalizeCacheFor(input) {
743
+ if (input === void 0) return DEFAULT_CACHE_FOR;
744
+ if (typeof input === "string") return input.toLowerCase() === "all" ? "all" : [input.toUpperCase()];
745
+ return input.map((verb) => String(verb).toUpperCase());
746
+ }
747
+ /** Coerce an optional count to a positive finite number, else `fallback`. */
748
+ function positive(value, fallback) {
749
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
750
+ }
751
+ //#endregion
752
+ //#region src/plugins/cache/plugin.ts
753
+ /**
754
+ * The cache plugin.
755
+ *
756
+ * ## Where it sits in the pipeline
757
+ *
758
+ * `priority: -100` is the reserved cache band, which buys two things the contract
759
+ * spells out in `docs/guide/plugin-lifecycle.md` §2.2:
760
+ *
761
+ * - **last in forward order** — the interceptor (`100`) and every argument
762
+ * decorator have already run, so `ctx.request` holds the final url, params and
763
+ * body when the key is built. Hashing an earlier shape would key two different
764
+ * requests identically (or one request twice).
765
+ * - **first in unwind order** — the raw envelope is stored before the validation
766
+ * and transformation plugins touch it, so a hit replays exactly what the server
767
+ * sent rather than a processed derivative.
768
+ *
769
+ * ## Stores what the server sent
770
+ *
771
+ * The entry holds `ctx.response.data`, not the `SnailResult`. The result is a
772
+ * projection of the envelope onto the caller's envelope schema; caching it would
773
+ * freeze one server's key names into another's cache.
774
+ */
775
+ /** Plugin name; also the identity used by `Service.use()` / `Service.remove()`. */
776
+ var CACHE_PLUGIN_NAME = "cache";
777
+ /** The reserved cache priority band (see `docs/guide/plugin-lifecycle.md` §2.1). */
778
+ var CACHE_PRIORITY = -100;
779
+ /** `ctx.state` slot holding the {@link CachePlan} of the current send. */
780
+ var CACHE_PLAN_STATE = "snail:cache:plan";
781
+ /**
782
+ * Create the cache plugin.
783
+ *
784
+ * ```ts
785
+ * const cache = Cache({ ttl: 30, l2: "localStorage" });
786
+ * Service.use(cache);
787
+ * await cache.manager?.invalidateAll();
788
+ * ```
789
+ */
790
+ function Cache(options) {
791
+ let manager;
792
+ const base = createPlugin({
793
+ name: CACHE_PLUGIN_NAME,
794
+ priority: CACHE_PRIORITY,
795
+ setup(pluginOptions, api) {
796
+ const instance = new CacheManager({
797
+ ...pluginOptions,
798
+ prefix: pluginOptions?.prefix ?? api.serverName,
799
+ logger: createLogger(api.serverOptions.logLevel)
800
+ });
801
+ manager = instance;
802
+ api.addMessages({
803
+ "cache.warn.l2.failed": "[%s] L2 cache %s failed; continuing without it",
804
+ "cache.warn.l2.unavailable": "[%s] %s is unavailable in this environment; the cache plugin uses L1 only",
805
+ "cache.warn.store.failed": "[%s] the response could not be cached: %s",
806
+ "cache.warn.revalidate.failed": "[%s] background revalidation failed: %s",
807
+ "info.cache.stale": "[%s] served a stale entry; revalidating in the background"
808
+ });
809
+ return {
810
+ beforeRequest: (ctx, next) => serveFromCache(instance, ctx, next),
811
+ afterResponse: (ctx, next) => storeResponse(instance, ctx, next)
812
+ };
813
+ }
814
+ })(options);
815
+ Object.defineProperty(base, "manager", {
816
+ enumerable: true,
817
+ configurable: true,
818
+ get: () => manager
819
+ });
820
+ return base;
821
+ }
822
+ /**
823
+ * Serve a cache hit, or register this request as the one that will fill the gap.
824
+ *
825
+ * Three outcomes, in order of cost:
826
+ *
827
+ * 1. **fresh hit** — `ctx.interrupt(response)` and no `next()`, so axios is never
828
+ * reached;
829
+ * 2. **stale hit** (only with `staleWhileRevalidate`) — the stale body is served
830
+ * immediately and the refresh happens out of band;
831
+ * 3. **miss** — `next()` runs the request, and a follower of an identical
832
+ * in-flight request waits for its leader instead of sending a second one.
833
+ */
834
+ async function serveFromCache(manager, ctx, next) {
835
+ const plan = resolveCachePlan(ctx, manager.options);
836
+ ctx.state.set(CACHE_PLAN_STATE, plan);
837
+ const key = plan.key;
838
+ if (key === void 0) return next();
839
+ const found = await manager.lookup(key, manager.options.staleWhileRevalidate);
840
+ if (found) {
841
+ ctx.markCacheHit();
842
+ ctx.interrupt(makeCachedResponse(found.value, ctx.request));
843
+ ctx.logger.debug(t("info.cache.hit", ctx.fullName));
844
+ if (found.stale) {
845
+ ctx.logger.debug(t("info.cache.stale", ctx.fullName));
846
+ revalidate(manager, ctx, plan, key);
847
+ }
848
+ return;
849
+ }
850
+ if (!manager.options.dedupe) return next();
851
+ const pending = manager.getInFlight(key);
852
+ if (pending) {
853
+ const shared = await pending.catch(() => void 0);
854
+ if (shared !== void 0) {
855
+ ctx.markCacheHit();
856
+ ctx.interrupt(makeCachedResponse(shared, ctx.request));
857
+ return;
858
+ }
859
+ return next();
860
+ }
861
+ const flight = deferred();
862
+ manager.setInFlight(key, flight.promise);
863
+ try {
864
+ await next();
865
+ flight.resolve(ctx.getResponse()?.data);
866
+ } catch (error) {
867
+ flight.reject(error);
868
+ throw error;
869
+ }
870
+ }
871
+ /**
872
+ * Invalidate first, then store, then continue the chain.
873
+ *
874
+ * The order of the first two is load-bearing: a method that is both cacheable
875
+ * and declares `@Invalidates` for one of its own tags stores *after* the purge,
876
+ * so it cannot delete the entry it just wrote. Storing before `next()` is what
877
+ * makes the cached value the raw envelope — later plugins see the response
878
+ * afterwards, and the cache keeps what the server actually sent.
879
+ */
880
+ async function storeResponse(manager, ctx, next) {
881
+ const plan = ctx.state.get(CACHE_PLAN_STATE);
882
+ if (plan && !ctx.isCacheHit) {
883
+ if (plan.invalidate.length > 0) {
884
+ await manager.invalidateTags(plan.invalidate);
885
+ ctx.logger.debug(t("info.cache.invalidate", ctx.fullName, plan.invalidate.join(", ")));
886
+ }
887
+ if (plan.key !== void 0 && ctx.response) try {
888
+ await manager.set(plan.key, snapshot(ctx.response.data), plan.ttl, plan.tags);
889
+ ctx.logger.debug(t("info.cache.set", ctx.fullName));
890
+ } catch (error) {
891
+ ctx.logger.warn(t("cache.warn.store.failed", ctx.fullName, String(error)));
892
+ }
893
+ }
894
+ await next();
895
+ }
896
+ /**
897
+ * Decide what the cache should do with this request, from the decorators and the
898
+ * configured verb list.
899
+ *
900
+ * Precedence, and why:
901
+ *
902
+ * 1. `@NoCache()` on the method — an explicit opt-out is never overridden;
903
+ * 2. `@Cacheable()` on the method — an explicit opt-in beats a class-wide
904
+ * decision, in both directions (this is what "method-level wins" means);
905
+ * 3. `@NoCache()` on the class;
906
+ * 4. `@Cacheable()` on the class — every method of the class;
907
+ * 5. otherwise, the verb must be listed in `cacheFor`, which defaults to `GET`.
908
+ *
909
+ * Tags from both levels are merged, class first, so a method can add its own
910
+ * without losing the class's. `ttl` and `key` are *not* merged: the most specific
911
+ * declaration wins outright.
912
+ */
913
+ function resolveCachePlan(ctx, options) {
914
+ const classCacheable = readCacheable(ctx.apiClass);
915
+ const methodCacheable = readCacheable(ctx.apiClass, ctx.methodName);
916
+ const cacheable = readNoCache(ctx.apiClass, ctx.methodName) ? false : methodCacheable !== void 0 ? true : readNoCache(ctx.apiClass) ? false : classCacheable !== void 0 ? true : matchesCacheFor(ctx.methodType, options.cacheFor);
917
+ const explicitKey = methodCacheable?.key ?? classCacheable?.key;
918
+ return {
919
+ key: cacheable ? buildCacheKey({
920
+ prefix: options.prefix,
921
+ request: ctx.request,
922
+ methodType: ctx.methodType,
923
+ explicitKey
924
+ }) : void 0,
925
+ ttl: methodCacheable?.ttl ?? classCacheable?.ttl ?? options.ttl,
926
+ tags: [.../* @__PURE__ */ new Set([...classCacheable?.tags ?? [], ...methodCacheable?.tags ?? []])],
927
+ invalidate: [.../* @__PURE__ */ new Set([...readInvalidates(ctx.apiClass), ...readInvalidates(ctx.apiClass, ctx.methodName)])]
928
+ };
929
+ }
930
+ /** `true` when `cacheFor` covers this verb. */
931
+ function matchesCacheFor(method, cacheFor) {
932
+ return cacheFor === "all" || cacheFor.includes(method);
933
+ }
934
+ /**
935
+ * Wrap a cached body into the axios response a plugin must hand back.
936
+ *
937
+ * `headers` is empty on purpose: the stored entry is only the body, and inventing
938
+ * headers would make downstream plugins react to values the server never sent.
939
+ * `config` is the live request, so anything reading `response.config` sees the
940
+ * request that produced the hit.
941
+ *
942
+ * The body is **copied**. Without that, the object handed to the caller would be
943
+ * the very object the cache holds, so one `result.data.name = "x"` would silently
944
+ * rewrite the cache entry — and, worse, a response transformer that hydrates the
945
+ * payload in place would corrupt the stored value for every later hit.
946
+ */
947
+ function makeCachedResponse(body, config) {
948
+ return {
949
+ data: snapshot(body),
950
+ status: 200,
951
+ statusText: "Cache Hit (snail)",
952
+ headers: {},
953
+ config
954
+ };
955
+ }
956
+ /**
957
+ * Copy a value so the cache and the caller can never share a mutable reference.
958
+ *
959
+ * `structuredClone` is preferred because it preserves `Date`, `Map`, `Set` and
960
+ * typed arrays, which a JSON round-trip flattens. JSON is the fallback for older
961
+ * engines, and if both fail (a function-valued field, a class instance with
962
+ * private state) the original reference is returned — a shared reference is a
963
+ * far smaller problem than throwing inside a cache read.
964
+ */
965
+ function snapshot(value) {
966
+ if (value === null || typeof value !== "object") return value;
967
+ const cloner = globalThis.structuredClone;
968
+ if (typeof cloner === "function") try {
969
+ return cloner(value);
970
+ } catch {}
971
+ try {
972
+ return JSON.parse(JSON.stringify(value));
973
+ } catch {
974
+ return value;
975
+ }
976
+ }
977
+ /**
978
+ * Refresh an entry after serving stale data.
979
+ *
980
+ * The refresh deliberately does **not** call this request's `next()`: the caller
981
+ * is still reading `ctx.response`, and a detached chain step would write the
982
+ * fresh response into the very same context — corrupting the result the caller is
983
+ * about to receive, or a later `send()` that reused the context. Instead the
984
+ * final config is replayed through the server's own axios instance and the
985
+ * response is normalised the same way `dispatch` normalises it, so the refreshed
986
+ * entry is byte-for-byte what a normal miss would have stored.
987
+ */
988
+ function revalidate(manager, ctx, plan, key) {
989
+ const config = ctx.server.pluginManager.reduce("requestInterceptor", ctx.request, ctx);
990
+ (async () => {
991
+ try {
992
+ let response = await ctx.server.axios.request(config);
993
+ response = coerceJSONStringBody(response, ctx.serverOptions.coerceJSONString);
994
+ response = ctx.server.pluginManager.reduce("responseInterceptor", response, ctx);
995
+ await manager.set(key, response.data, plan.ttl, plan.tags);
996
+ } catch (error) {
997
+ ctx.logger.warn(t("cache.warn.revalidate.failed", ctx.fullName, String(error)));
998
+ }
999
+ })();
1000
+ }
1001
+ //#endregion
1002
+ //#region src/plugins/interceptor/decorators.ts
1003
+ /**
1004
+ * `@BeforeRequest()` / `@AfterResponse()` — the decorator half of the
1005
+ * interceptor plugin.
1006
+ *
1007
+ * ## Storage
1008
+ *
1009
+ * Both targets share one metadata key per phase: a class application lands in the
1010
+ * class slot, a method application in that method's slot
1011
+ * (`src/core/metadata.ts` §storage model). Reading them back is therefore one
1012
+ * `getClassMetadata` plus one `getMethodMetadata`, with no "is this a class or a
1013
+ * proto" branching at the call site.
1014
+ *
1015
+ * ## Why the key name is namespaced
1016
+ *
1017
+ * `createClassDecorator` turns `"snail-interceptor/before"` into
1018
+ * `Symbol.for("@snail-js/api:custom:snail-interceptor/before")`. A `Symbol.for`
1019
+ * key survives the two-copies-of-the-package situation a monorepo creates, so a
1020
+ * decorator applied from the hoisted copy is still visible to a plugin loaded
1021
+ * from the nested one.
1022
+ */
1023
+ var BEFORE_KEY = "snail-interceptor/before";
1024
+ var AFTER_KEY = "snail-interceptor/after";
1025
+ var addBeforeOnClass = createClassDecorator(BEFORE_KEY);
1026
+ var addBeforeOnMethod = createMethodDecorator(BEFORE_KEY);
1027
+ var addAfterOnClass = createClassDecorator(AFTER_KEY);
1028
+ var addAfterOnMethod = createMethodDecorator(AFTER_KEY);
1029
+ /**
1030
+ * Fold a class application and a method application into one decorator.
1031
+ *
1032
+ * TypeScript tells the two apart by arity: a class decorator is called with the
1033
+ * constructor alone, a method decorator with `(prototype, key, descriptor)`.
1034
+ * That is the only reliable signal available without `reflect-metadata`.
1035
+ */
1036
+ function dualDecorator(applyToClass, applyToMethod) {
1037
+ return ((target, propertyKey) => {
1038
+ if (propertyKey === void 0) {
1039
+ applyToClass(target);
1040
+ return;
1041
+ }
1042
+ applyToMethod(target, propertyKey, void 0);
1043
+ });
1044
+ }
1045
+ /** Reject the misuse that would otherwise fail silently at request time. */
1046
+ function assertCallback(value, decorator) {
1047
+ if (typeof value !== "function") throw new SnailDecoratorError(`[snail] @${decorator}() needs a function; got ${typeof value}`);
1048
+ }
1049
+ /**
1050
+ * Run `onFulfilled` on the request config of this class / this method, in series
1051
+ * with every other request interceptor.
1052
+ *
1053
+ * ```ts
1054
+ * @Api("/user")
1055
+ * @BeforeRequest<UserConfig>((config) => { config.headers.set("x-trace", "1"); })
1056
+ * class UserApi {
1057
+ * @Get("/")
1058
+ * @BeforeRequest((config) => { config.timeout = 5000; })
1059
+ * list(): Promise<User[]> { return null!; }
1060
+ * }
1061
+ * ```
1062
+ *
1063
+ * Class-level interceptors run before method-level ones, so a method may refine
1064
+ * what its class established. `onRejected` may recover a failure by returning a
1065
+ * replacement config; returning `undefined` (or throwing) lets the failure
1066
+ * through, and the request is then never sent.
1067
+ */
1068
+ function BeforeRequest(onFulfilled, onRejected) {
1069
+ assertCallback(onFulfilled, "BeforeRequest");
1070
+ if (onRejected !== void 0) assertCallback(onRejected, "BeforeRequest");
1071
+ const entry = {
1072
+ onFulfilled,
1073
+ onRejected
1074
+ };
1075
+ return dualDecorator(addBeforeOnClass(entry), addBeforeOnMethod(entry));
1076
+ }
1077
+ /**
1078
+ * Run `onFulfilled` against the response of this class / this method.
1079
+ *
1080
+ * The callback receives the axios response, **not** a `next`: unlike
1081
+ * `beforeRequest`, this phase is an unwind hook, so the response already exists
1082
+ * and nothing here decides whether the request happens. Return a replacement
1083
+ * response, or `undefined` to keep the one that was passed in.
1084
+ */
1085
+ function AfterResponse(onFulfilled, onRejected) {
1086
+ assertCallback(onFulfilled, "AfterResponse");
1087
+ if (onRejected !== void 0) assertCallback(onRejected, "AfterResponse");
1088
+ const entry = {
1089
+ onFulfilled,
1090
+ onRejected
1091
+ };
1092
+ return dualDecorator(addAfterOnClass(entry), addAfterOnMethod(entry));
1093
+ }
1094
+ /** Class-level `@BeforeRequest` entries, in application order. */
1095
+ function classBeforeEntries(target) {
1096
+ return getClassMetadata(BEFORE_KEY, target) ?? [];
1097
+ }
1098
+ /** Method-level `@BeforeRequest` entries, in application order. */
1099
+ function methodBeforeEntries(target, methodName) {
1100
+ return getMethodMetadata(BEFORE_KEY, target, methodName) ?? [];
1101
+ }
1102
+ /** Class-level `@AfterResponse` entries, in application order. */
1103
+ function classAfterEntries(target) {
1104
+ return getClassMetadata(AFTER_KEY, target) ?? [];
1105
+ }
1106
+ /** Method-level `@AfterResponse` entries, in application order. */
1107
+ function methodAfterEntries(target, methodName) {
1108
+ return getMethodMetadata(AFTER_KEY, target, methodName) ?? [];
1109
+ }
1110
+ //#endregion
1111
+ //#region src/plugins/interceptor/manager.ts
1112
+ /**
1113
+ * Runtime registry of programmatic interceptors.
1114
+ *
1115
+ * Decorators cover interceptors that are known at class-definition time. This
1116
+ * class covers the other half — an application that decides at runtime "from now
1117
+ * on, sign every request" — and it is also what the plugin uses internally to
1118
+ * hold the server-wide entries passed to `Interceptor({ request })`.
1119
+ *
1120
+ * Entries keep insertion order: that order *is* the execution order, since the
1121
+ * interceptor contract is deliberately sequential rather than priority based.
1122
+ */
1123
+ var InterceptorManager = class {
1124
+ constructor() {
1125
+ this.registry = /* @__PURE__ */ new Map();
1126
+ this.sequence = 0;
1127
+ }
1128
+ /** Register an entry. Returns the id {@link eject} needs to remove it again. */
1129
+ use(entry) {
1130
+ if (!entry || typeof entry.onFulfilled !== "function" && typeof entry.onRejected !== "function") throw new SnailPluginError("[snail] InterceptorManager.use() expects an entry with onFulfilled and/or onRejected", { pluginName: "interceptor" });
1131
+ this.sequence += 1;
1132
+ this.registry.set(this.sequence, entry);
1133
+ return this.sequence;
1134
+ }
1135
+ /** Remove an entry by the id returned from {@link use}. Returns whether it existed. */
1136
+ eject(id) {
1137
+ return this.registry.delete(id);
1138
+ }
1139
+ /** Drop every entry. */
1140
+ clear() {
1141
+ this.registry.clear();
1142
+ }
1143
+ /** Registered entries, in execution order. */
1144
+ get entries() {
1145
+ return [...this.registry.values()];
1146
+ }
1147
+ /** Number of registered entries. */
1148
+ get size() {
1149
+ return this.registry.size;
1150
+ }
1151
+ };
1152
+ //#endregion
1153
+ //#region src/plugins/interceptor/plugin.ts
1154
+ /**
1155
+ * The interceptor plugin.
1156
+ *
1157
+ * ## Where it sits in the pipeline
1158
+ *
1159
+ * `priority: 100` is the reserved interceptor band, so this plugin is the first
1160
+ * to see the request in forward order and the last to see the response in unwind
1161
+ * order. That is what lets `@BeforeRequest()` rewrite the config *before* the
1162
+ * cache plugin (`-100`) hashes the final url, params and body into a cache key.
1163
+ *
1164
+ * ## Request vs response
1165
+ *
1166
+ * `beforeRequest` is a chain hook: the interceptors run first, and only then
1167
+ * does the hook hand control on with `next()`. An interceptor that fails
1168
+ * unrecoverably therefore stops the request before anything reaches the network.
1169
+ *
1170
+ * `afterResponse` is an **unwind** hook. The response already exists when it
1171
+ * runs, so calling `next()` *first* would let later plugins observe a response
1172
+ * the interceptors had not rewritten yet. The interceptors run against
1173
+ * `ctx.response` first and the chain is advanced afterwards. The interceptor
1174
+ * callbacks themselves are not chain hooks — they receive the response rather
1175
+ * than a `next` — so nothing inside one of them can advance or stop the chain.
1176
+ */
1177
+ /** Plugin name; also the identity used by `Service.use()` / `Service.remove()`. */
1178
+ var INTERCEPTOR_PLUGIN_NAME = "interceptor";
1179
+ /** The reserved interceptor priority band (see `docs/guide/plugin-lifecycle.md` §2.1). */
1180
+ var INTERCEPTOR_PRIORITY = 100;
1181
+ /**
1182
+ * Create the interceptor plugin.
1183
+ *
1184
+ * ```ts
1185
+ * const interceptors = Interceptor({
1186
+ * request: [{ onFulfilled: (config) => { config.headers.set("x-app", "web"); } }]
1187
+ * });
1188
+ * Service.use(interceptors);
1189
+ * interceptors.request.use({ onFulfilled: (config, ctx) => ctx.logger.debug(ctx.fullName) });
1190
+ * ```
1191
+ */
1192
+ function Interceptor(options) {
1193
+ const request = new InterceptorManager();
1194
+ const response = new InterceptorManager();
1195
+ for (const entry of options?.request ?? []) request.use(entry);
1196
+ for (const entry of options?.response ?? []) response.use(entry);
1197
+ const base = createPlugin({
1198
+ name: INTERCEPTOR_PLUGIN_NAME,
1199
+ priority: 100,
1200
+ setup(_pluginOptions, api) {
1201
+ api.addMessages({
1202
+ "info.interceptor.request": "[%s] %s request interceptor(s) applied",
1203
+ "info.interceptor.response": "[%s] %s response interceptor(s) applied"
1204
+ });
1205
+ return {
1206
+ beforeRequest: (ctx, next) => runRequestInterceptors(request, ctx, next),
1207
+ afterResponse: (ctx, next) => runResponseInterceptors(response, ctx, next)
1208
+ };
1209
+ }
1210
+ })(options);
1211
+ return Object.assign(base, {
1212
+ request,
1213
+ response
1214
+ });
1215
+ }
1216
+ /**
1217
+ * Run every request interceptor, then continue the chain.
1218
+ *
1219
+ * Order is class → method → server-wide, each list in its own application order.
1220
+ * One interceptor's failure is offered to that same interceptor's `onRejected`
1221
+ * before it is allowed to abort the request, which is the only recovery point in
1222
+ * the whole pipeline — `onError` merely observes.
1223
+ */
1224
+ async function runRequestInterceptors(manager, ctx, next) {
1225
+ const entries = [
1226
+ ...classBeforeEntries(ctx.apiClass),
1227
+ ...methodBeforeEntries(ctx.apiClass, ctx.methodName),
1228
+ ...manager.entries
1229
+ ];
1230
+ if (entries.length > 0) ctx.logger.debug(t("info.interceptor.request", ctx.fullName, String(entries.length)));
1231
+ for (const entry of entries) {
1232
+ if (typeof entry.onFulfilled !== "function") continue;
1233
+ try {
1234
+ const replaced = await entry.onFulfilled(ctx.request, ctx);
1235
+ if (replaced !== void 0) ctx.setRequest(replaced);
1236
+ } catch (error) {
1237
+ ctx.setRequest(await recover(entry, error, ctx));
1238
+ }
1239
+ }
1240
+ await next();
1241
+ }
1242
+ /**
1243
+ * Run every response interceptor against the response that already exists.
1244
+ *
1245
+ * The callbacks are not chain hooks: they get the response, not a `next`, so a
1246
+ * response interceptor can rewrite but never interrupt. The plugin's own
1247
+ * `afterResponse` hook still advances the chain at the end, which is what lets
1248
+ * lower-priority plugins (validation, transformation) see the rewritten value.
1249
+ */
1250
+ async function runResponseInterceptors(manager, ctx, next) {
1251
+ const entries = [
1252
+ ...classAfterEntries(ctx.apiClass),
1253
+ ...methodAfterEntries(ctx.apiClass, ctx.methodName),
1254
+ ...manager.entries
1255
+ ];
1256
+ if (entries.length > 0) ctx.logger.debug(t("info.interceptor.response", ctx.fullName, String(entries.length)));
1257
+ for (const entry of entries) {
1258
+ if (typeof entry.onFulfilled !== "function") continue;
1259
+ try {
1260
+ const replaced = await entry.onFulfilled(ctx.requireResponse(), ctx);
1261
+ if (replaced !== void 0) ctx.setResponse(replaced);
1262
+ } catch (error) {
1263
+ ctx.setResponse(await recover(entry, error, ctx));
1264
+ }
1265
+ }
1266
+ await next();
1267
+ }
1268
+ /**
1269
+ * Give an interceptor's `onRejected` a chance to recover from `error`.
1270
+ *
1271
+ * Only a *value* recovers: `undefined` means "I handled the notification but
1272
+ * have nothing to continue with", which must not silently produce an undefined
1273
+ * config. The original error is rethrown in that case, so the caller sees the
1274
+ * real cause rather than a downstream symptom.
1275
+ */
1276
+ async function recover(entry, error, ctx) {
1277
+ if (typeof entry.onRejected !== "function") throw error;
1278
+ const recovered = await entry.onRejected(error, ctx);
1279
+ if (recovered === void 0) throw error;
1280
+ return recovered;
1281
+ }
1282
+ //#endregion
1283
+ //#region src/plugins/pool/type.ts
1284
+ /**
1285
+ * The error codes a request-pool refusal can carry.
1286
+ *
1287
+ * `isPoolError()` is the supported way to test them, but the strings are exported
1288
+ * so an application can switch on one precisely — for example to distinguish "the
1289
+ * queue was full, try again shortly" from "the user navigated away".
1290
+ */
1291
+ var POOL_ERROR_CODES = {
1292
+ /** `maxQueue` was reached; the request was never sent. */
1293
+ queueFull: "SNAIL_POOL_QUEUE_FULL",
1294
+ /** The request waited longer than `queueTimeout` and was dropped. */
1295
+ queueTimeout: "SNAIL_POOL_QUEUE_TIMEOUT",
1296
+ /** The request was cancelled while it was still waiting for a slot. */
1297
+ aborted: "SNAIL_POOL_ABORTED",
1298
+ /** The queue was cleared — usually because the plugin was uninstalled. */
1299
+ cleared: "SNAIL_POOL_CLEARED"
1300
+ };
1301
+ /**
1302
+ * Raised when the pool refuses or drops a request.
1303
+ *
1304
+ * A dedicated class rather than a reuse of `SnailRequestError`: a pool refusal
1305
+ * means the request **never reached the network**, which is a materially different
1306
+ * situation from a transport failure. Retrying is safe and usually correct, the
1307
+ * payload is untouched, and no server state changed — a caller that cannot tell the
1308
+ * two apart will either retry a request the server already processed, or give up on
1309
+ * one that was merely queued behind a burst.
1310
+ */
1311
+ var SnailPoolError = class extends SnailError {
1312
+ constructor(message, code, options = {}) {
1313
+ super(message, {
1314
+ code,
1315
+ cause: options.cause
1316
+ });
1317
+ }
1318
+ };
1319
+ //#endregion
1320
+ //#region src/plugins/pool/scheduler.ts
1321
+ /**
1322
+ * The scheduling core of the request pool.
1323
+ *
1324
+ * Deterministic and free of any request or plugin concept: it hands out `release`
1325
+ * callbacks and knows nothing about what they guard. That makes the hard part —
1326
+ * ordering, fairness and the guarantee that a slot is always returned — testable
1327
+ * on its own, without a server or a network.
1328
+ *
1329
+ * ## Messages come from the plugin
1330
+ *
1331
+ * The refusal messages are contributed by `RequestPool`'s `setup`, following the
1332
+ * plugin-owned-strings convention. Using this class **standalone** therefore yields
1333
+ * the raw message keys rather than translated text — the `code` on each
1334
+ * {@link SnailPoolError} is always correct, and that is what callers should branch
1335
+ * on. Registering a catalogue yourself is enough if you want the text too.
1336
+ *
1337
+ * ## The invariant that matters
1338
+ *
1339
+ * `active` must never drift from the number of outstanding tickets. Every failure
1340
+ * path (a queue that is full, a wait that times out, a request that is aborted,
1341
+ * a waiter dropped by `clear`) either never increments `active` or has already
1342
+ * been counted, and `release()` is idempotent so a double release from a `finally`
1343
+ * plus an abort listener cannot leak a slot. A leaked slot is invisible until the
1344
+ * pool permanently deadlocks at its ceiling, at which point every later request
1345
+ * hangs — which is why it is enforced here rather than at the call site.
1346
+ */
1347
+ var RequestPoolScheduler = class {
1348
+ constructor(options = {}) {
1349
+ this.active = 0;
1350
+ this.sequence = 0;
1351
+ this.waiters = [];
1352
+ const concurrency = Number.isFinite(options.concurrency) ? Math.floor(options.concurrency) : 6;
1353
+ this.concurrency = Math.max(1, concurrency);
1354
+ this.maxQueue = Number.isFinite(options.maxQueue) ? Math.max(0, Math.floor(options.maxQueue)) : Number.POSITIVE_INFINITY;
1355
+ this.queueTimeout = Math.max(0, options.queueTimeout ?? 0);
1356
+ this.priorityOf = options.priority;
1357
+ }
1358
+ /** Current counters. */
1359
+ get stats() {
1360
+ return {
1361
+ active: this.active,
1362
+ queued: this.waiters.length,
1363
+ concurrency: this.concurrency
1364
+ };
1365
+ }
1366
+ /** Change the ceiling at runtime; raising it immediately admits queued work. */
1367
+ setConcurrency(value) {
1368
+ if (!Number.isFinite(value)) return;
1369
+ this.concurrency = Math.max(1, Math.floor(value));
1370
+ this.pump();
1371
+ }
1372
+ /**
1373
+ * Take a slot, or wait for one.
1374
+ *
1375
+ * Resolves with a ticket whose `release()` returns the slot. Rejects when the
1376
+ * queue is full or the wait exceeds `queueTimeout`. `signal` lets an abandoned
1377
+ * request leave the queue instead of holding a place it will never use.
1378
+ */
1379
+ acquire(ctx, signal) {
1380
+ if (signal?.aborted) return Promise.reject(this.abortError());
1381
+ if (this.active < this.concurrency) return Promise.resolve(this.issue());
1382
+ if (this.waiters.length >= this.maxQueue) return Promise.reject(new SnailPoolError(t("error.pool.queueFull", this.waiters.length), POOL_ERROR_CODES.queueFull));
1383
+ return new Promise((resolve, reject) => {
1384
+ let timer;
1385
+ let onAbort;
1386
+ let done = false;
1387
+ const waiter = {
1388
+ priority: this.priorityOfFor(ctx),
1389
+ sequence: this.sequence++,
1390
+ settle: (ticket) => {
1391
+ if (done) return;
1392
+ done = true;
1393
+ waiter.dispose();
1394
+ resolve(ticket);
1395
+ },
1396
+ fail: (error) => {
1397
+ if (done) return;
1398
+ done = true;
1399
+ waiter.dispose();
1400
+ reject(error);
1401
+ },
1402
+ dispose: () => {
1403
+ if (timer) clearTimeout(timer);
1404
+ if (onAbort) signal?.removeEventListener?.("abort", onAbort);
1405
+ }
1406
+ };
1407
+ if (this.queueTimeout > 0) {
1408
+ timer = setTimeout(() => {
1409
+ remove(this.waiters, waiter);
1410
+ waiter.fail(new SnailPoolError(t("error.pool.queueTimeout", this.queueTimeout), POOL_ERROR_CODES.queueTimeout));
1411
+ }, this.queueTimeout);
1412
+ timer.unref?.();
1413
+ }
1414
+ if (signal && typeof signal.addEventListener === "function") {
1415
+ onAbort = () => {
1416
+ remove(this.waiters, waiter);
1417
+ waiter.fail(this.abortError());
1418
+ };
1419
+ signal.addEventListener("abort", onAbort, { once: true });
1420
+ }
1421
+ this.waiters.push(waiter);
1422
+ this.sort();
1423
+ });
1424
+ }
1425
+ /**
1426
+ * Drop every waiter, rejecting each with `reason`.
1427
+ *
1428
+ * Used on uninstall: a queued request must not sit forever waiting for a pool
1429
+ * that will never admit it.
1430
+ */
1431
+ clear(reason) {
1432
+ const pending = this.waiters.splice(0, this.waiters.length);
1433
+ for (const waiter of pending) waiter.fail(reason ?? new SnailPoolError(t("error.pool.cleared"), POOL_ERROR_CODES.cleared));
1434
+ }
1435
+ /** Hand out one slot. */
1436
+ issue() {
1437
+ this.active += 1;
1438
+ let released = false;
1439
+ return { release: () => {
1440
+ if (released) return;
1441
+ released = true;
1442
+ this.active = Math.max(0, this.active - 1);
1443
+ this.pump();
1444
+ } };
1445
+ }
1446
+ /** Admit as many waiters as there are free slots. */
1447
+ pump() {
1448
+ while (this.active < this.concurrency && this.waiters.length > 0) this.waiters.shift().settle(this.issue());
1449
+ }
1450
+ /** Lowest priority number first, then arrival order. */
1451
+ sort() {
1452
+ this.waiters.sort((a, b) => a.priority === b.priority ? a.sequence - b.sequence : a.priority - b.priority);
1453
+ }
1454
+ priorityOfFor(ctx) {
1455
+ if (!this.priorityOf) return 0;
1456
+ try {
1457
+ const value = this.priorityOf(ctx);
1458
+ return Number.isFinite(value) ? value : 0;
1459
+ } catch {
1460
+ return 0;
1461
+ }
1462
+ }
1463
+ abortError() {
1464
+ return new SnailPoolError(t("error.pool.aborted"), POOL_ERROR_CODES.aborted);
1465
+ }
1466
+ };
1467
+ /** Remove one waiter by identity, wherever it sits in the queue. */
1468
+ function remove(waiters, waiter) {
1469
+ const index = waiters.indexOf(waiter);
1470
+ if (index !== -1) waiters.splice(index, 1);
1471
+ }
1472
+ //#endregion
1473
+ //#region src/plugins/pool/plugin.ts
1474
+ /** Plugin name; also the identity used by `Service.use()` / `Service.remove()`. */
1475
+ var POOL_PLUGIN_NAME = "pool";
1476
+ /**
1477
+ * Priority of the pool band.
1478
+ *
1479
+ * **Below** the cache (`-100`), which is the whole point: forward hooks run
1480
+ * highest-priority first, so the pool is the very last thing to run before the
1481
+ * transport. A request that the cache can answer never reaches the pool and never
1482
+ * consumes a slot. Putting the pool above the cache would let a handful of cached
1483
+ * reads occupy the entire pool and starve the real requests behind them.
1484
+ */
1485
+ var POOL_PRIORITY = -150;
1486
+ /**
1487
+ * Bound how many requests are in flight at once.
1488
+ *
1489
+ * ```ts
1490
+ * Service.use(RequestPool({ concurrency: 4, maxQueue: 50, queueTimeout: 10_000 }));
1491
+ *
1492
+ * // `ctx.state` is readable from the priority callback, so an interactive request
1493
+ * // can jump ahead of a background prefetch.
1494
+ * Service.use(RequestPool({ concurrency: 4, priority: (ctx) => (ctx.state.get("prefetch") ? 100 : 0) }));
1495
+ * ```
1496
+ *
1497
+ * ## What problem this solves
1498
+ *
1499
+ * See {@link RequestPoolOptions} for why the browser's own queue is not enough.
1500
+ * In short: the built-in one is FIFO, invisible and unprioritised, so a burst from
1501
+ * one screen can starve the request the user is actually waiting for, and nothing
1502
+ * fails fast when the backlog grows without bound.
1503
+ *
1504
+ * ## Where it sits
1505
+ *
1506
+ * Last in forward order, immediately before the network call, and it holds its slot
1507
+ * only for the transport — `next()` resolves once the response has been received,
1508
+ * so validation, transformation and the caller's reactive updates happen after the
1509
+ * slot is already back in the pool.
1510
+ *
1511
+ * A cache hit short-circuits `beforeRequest` upstream of this plugin, so cached
1512
+ * reads cost no concurrency.
1513
+ *
1514
+ * ## One path it does not cover
1515
+ *
1516
+ * `useTokenAuth` replays a request that came back `401` by re-running the transport
1517
+ * directly, deliberately without re-entering `beforeRequest` (re-entering would
1518
+ * loop). Those replays therefore **do not pass through this pool** and are not
1519
+ * counted against `concurrency`.
1520
+ *
1521
+ * The practical impact is small — replays are bounded by the number of requests
1522
+ * that were in flight when the token expired, which were already uncapped — but it
1523
+ * is worth knowing before reaching for the pool to protect a backend from an auth
1524
+ * storm. If that matters, cap it at the source: use `useRequest`'s own concurrency
1525
+ * control, or let `useTokenAuth` refresh proactively rather than on the first 401.
1526
+ */
1527
+ function RequestPool(options) {
1528
+ let scheduler;
1529
+ const base = createPlugin({
1530
+ name: POOL_PLUGIN_NAME,
1531
+ priority: POOL_PRIORITY,
1532
+ setup(_pluginOptions, api) {
1533
+ const instance = new RequestPoolScheduler(options);
1534
+ scheduler = instance;
1535
+ api.addMessages({
1536
+ "error.pool.queueFull": "请求池队列已满(等待中 %s 个),请稍后重试",
1537
+ "error.pool.queueTimeout": "请求池排队超时(已等待 %sms)",
1538
+ "error.pool.cleared": "请求池已清空,排队的请求被取消",
1539
+ "error.pool.aborted": "请求在排队期间被取消"
1540
+ });
1541
+ api.onDispose(() => instance.clear());
1542
+ return { async beforeRequest(ctx, next) {
1543
+ const signal = ctx.request.signal ?? void 0;
1544
+ const ticket = await instance.acquire(ctx, signal);
1545
+ try {
1546
+ await next();
1547
+ } finally {
1548
+ ticket.release();
1549
+ }
1550
+ } };
1551
+ }
1552
+ })(options);
1553
+ return Object.defineProperty(base, "scheduler", {
1554
+ enumerable: true,
1555
+ configurable: true,
1556
+ get: () => scheduler
1557
+ });
1558
+ }
1559
+ /** Read the live counters of an installed pool plugin, or `undefined`. */
1560
+ function poolStats(plugin) {
1561
+ return plugin.scheduler?.stats;
1562
+ }
1563
+ /** Drop every queued request of an installed pool plugin. */
1564
+ function clearPool(plugin, reason) {
1565
+ plugin.scheduler?.clear(reason);
1566
+ }
1567
+ /**
1568
+ * `true` when `error` came from the pool rather than from the transport.
1569
+ *
1570
+ * Lets a caller tell "refused before it was ever sent" — safe to retry later —
1571
+ * apart from a real network failure. Covers all four refusal paths: a full queue,
1572
+ * a queue wait that timed out, a request abandoned while waiting, and a queue
1573
+ * cleared by an uninstall.
1574
+ */
1575
+ function isPoolError(error) {
1576
+ return error instanceof SnailPoolError;
1577
+ }
1578
+ //#endregion
1579
+ //#region src/plugins/transform/decorators.ts
1580
+ /**
1581
+ * Transform decorators.
1582
+ *
1583
+ * `@Transform(DtoClass)` chooses the class a response is hydrated into;
1584
+ * `@PropertyType()` and `@ExposeName()` describe the shape of that class. All
1585
+ * three are metadata-only — they never touch a request or a server, so a DTO
1586
+ * shared between two api classes cannot leak state from one to the other.
1587
+ */
1588
+ /** Metadata slot written by `@Transform`. */
1589
+ var TRANSFORM_KEY = customMetadataKey("transform/dto");
1590
+ /** Metadata slot written by `@PropertyType`. */
1591
+ var PROPERTY_TYPE_KEY = customMetadataKey("transform/property-type");
1592
+ /** Metadata slot written by `@ExposeName`. */
1593
+ var EXPOSE_NAME_KEY = customMetadataKey("transform/expose-name");
1594
+ /**
1595
+ * Choose the DTO a response payload is hydrated into.
1596
+ *
1597
+ * Applies to an api class and to a single method; the method wins, so one
1598
+ * endpoint can answer with a different shape than its siblings.
1599
+ *
1600
+ * ```ts
1601
+ * @Api("/user")
1602
+ * @Transform(UserDto)
1603
+ * class UserApi {
1604
+ * @Get("/:id")
1605
+ * getUser(@Params("id") id: string): Promise<UserDto> { return null!; }
1606
+ * }
1607
+ * ```
1608
+ */
1609
+ function Transform$1(dto) {
1610
+ if (typeof dto !== "function") throw new SnailDecoratorError("[snail] @Transform() expects a DTO class, not an instance or a plain object");
1611
+ return ((target, propertyKey) => {
1612
+ defineMetadata(TRANSFORM_KEY, dto, target, propertyKey);
1613
+ });
1614
+ }
1615
+ var definePropertyType = createPropertyDecorator("transform/property-type");
1616
+ /**
1617
+ * Declare the runtime type of one DTO property.
1618
+ *
1619
+ * ```ts
1620
+ * class OrderDto {
1621
+ * @PropertyType(() => UserDto) user!: UserDto;
1622
+ * @PropertyType(() => ItemDto, { array: true }) items!: ItemDto[];
1623
+ * @PropertyType(() => Date) createdAt!: Date;
1624
+ * }
1625
+ * ```
1626
+ *
1627
+ * A property without `@PropertyType` is assigned as-is, so primitives, plain
1628
+ * objects and arrays of primitives need no declaration — but they *do* need
1629
+ * something that makes the property known, otherwise the unknown-key rule drops
1630
+ * them.
1631
+ */
1632
+ function PropertyType(type, options) {
1633
+ if (typeof type !== "function") throw new SnailDecoratorError("[snail] @PropertyType() expects a lazy type resolver, e.g. @PropertyType(() => ChildDto)");
1634
+ return definePropertyType(options ? {
1635
+ type,
1636
+ options
1637
+ } : { type });
1638
+ }
1639
+ var defineExposeName = createPropertyDecorator("transform/expose-name");
1640
+ /**
1641
+ * Read a property from a differently-named JSON key.
1642
+ *
1643
+ * ```ts
1644
+ * class UserDto {
1645
+ * @ExposeName("user_name") userName!: string;
1646
+ * }
1647
+ * // { "user_name": "ada" } → new UserDto().userName === "ada"
1648
+ * ```
1649
+ *
1650
+ * Excluding a key needs no decorator: undeclared JSON keys are already dropped.
1651
+ */
1652
+ function ExposeName(jsonKey) {
1653
+ if (typeof jsonKey !== "string" || jsonKey.length === 0) throw new SnailDecoratorError("[snail] @ExposeName() requires a non-empty JSON key");
1654
+ return defineExposeName(jsonKey);
1655
+ }
1656
+ /** Read the DTO of one method: method first, then the api class. */
1657
+ function resolveDto(apiClass, methodName) {
1658
+ return getMetadata(TRANSFORM_KEY, apiClass, methodName) ?? getMetadata(TRANSFORM_KEY, apiClass);
1659
+ }
1660
+ /** `true` for an own (not inherited) property. */
1661
+ function hasOwn(target, key) {
1662
+ return Object.prototype.hasOwnProperty.call(target, key);
1663
+ }
1664
+ /**
1665
+ * Revive a JSON date.
1666
+ *
1667
+ * A string that `new Date()` rejects is returned unchanged: an `Invalid Date` is
1668
+ * truthy, serialises to `null` and fails every later check somewhere far from the
1669
+ * cause, so keeping the original string at least leaves the bug where it is.
1670
+ */
1671
+ function reviveDate(value) {
1672
+ if (value instanceof Date) return value;
1673
+ if (typeof value !== "string" && typeof value !== "number") return value;
1674
+ const date = new Date(value);
1675
+ return Number.isNaN(date.getTime()) ? value : date;
1676
+ }
1677
+ /**
1678
+ * The property names a DTO declares.
1679
+ *
1680
+ * Two sources, and both are needed:
1681
+ * - the property decorators, which are the only way to see a `field!: T`
1682
+ * declaration (TypeScript erases those at compile time);
1683
+ * - the instance's own keys, which cover initialised fields and constructor
1684
+ * assignments and let a partially decorated DTO keep working.
1685
+ *
1686
+ * An empty result means the class declares nothing at all — see
1687
+ * {@link buildInstance}.
1688
+ */
1689
+ function knownPropertyNames(DtoClass, instance) {
1690
+ const names = /* @__PURE__ */ new Set([...collectMethodKeys(PROPERTY_TYPE_KEY, DtoClass), ...collectMethodKeys(EXPOSE_NAME_KEY, DtoClass)]);
1691
+ for (const key of Object.keys(instance)) names.add(key);
1692
+ return names;
1693
+ }
1694
+ /** Shallow-copy every own key of the JSON onto the instance. */
1695
+ function copyOwnKeys(target, source) {
1696
+ for (const key of Object.keys(source)) target[key] = source[key];
1697
+ }
1698
+ /**
1699
+ * Hydrate one property value using the type declared for it.
1700
+ *
1701
+ * `childDepth` is the depth of `value` itself, so the depth guard in
1702
+ * {@link hydrateInto} can stop the descent before the stack does.
1703
+ */
1704
+ function hydrateProperty(value, spec, options, childDepth, seen) {
1705
+ if (!spec || value === null || value === void 0) return value;
1706
+ const resolved = spec.type();
1707
+ if (spec.options?.array === true) {
1708
+ if (!Array.isArray(value)) return value;
1709
+ return value.map((item) => hydrateValue(item, resolved, options, childDepth, seen));
1710
+ }
1711
+ return hydrateValue(value, resolved, options, childDepth, seen);
1712
+ }
1713
+ /**
1714
+ * Hydrate a single value against a resolved constructor.
1715
+ *
1716
+ * Primitives need no work — `JSON.parse` already produced the right JavaScript
1717
+ * type — and a non-function resolver is treated as "no type declared" rather than
1718
+ * a crash, because a typo in a decorator must not break every response.
1719
+ */
1720
+ function hydrateValue(value, resolved, options, depth, seen) {
1721
+ if (value === null || value === void 0) return value;
1722
+ if (resolved === Date) return reviveDate(value);
1723
+ if (resolved === String || resolved === Number || resolved === Boolean) return value;
1724
+ if (typeof resolved !== "function") return value;
1725
+ return hydrateInto(value, resolved, options, depth, seen);
1726
+ }
1727
+ /**
1728
+ * Build one instance, honouring `fromJSON`, the declared properties and
1729
+ * `keepUnknown`.
1730
+ */
1731
+ function buildInstance(raw, DtoClass, options, depth, seen) {
1732
+ if (typeof DtoClass.fromJSON === "function") return DtoClass.fromJSON(raw, options.ctx);
1733
+ const instance = new DtoClass();
1734
+ const known = knownPropertyNames(DtoClass, instance);
1735
+ if (known.size === 0) {
1736
+ copyOwnKeys(instance, raw);
1737
+ return instance;
1738
+ }
1739
+ const consumed = /* @__PURE__ */ new Set();
1740
+ for (const property of known) {
1741
+ const jsonKey = getMetadata(EXPOSE_NAME_KEY, DtoClass, property) ?? property;
1742
+ if (!hasOwn(raw, jsonKey)) continue;
1743
+ consumed.add(jsonKey);
1744
+ const spec = getMetadata(PROPERTY_TYPE_KEY, DtoClass, property);
1745
+ instance[property] = hydrateProperty(raw[jsonKey], spec, options, depth + 1, seen);
1746
+ }
1747
+ if (options.keepUnknown) for (const key of Object.keys(raw)) {
1748
+ if (consumed.has(key)) continue;
1749
+ instance[key] = raw[key];
1750
+ }
1751
+ return instance;
1752
+ }
1753
+ /**
1754
+ * Recursive worker behind {@link hydrate}.
1755
+ *
1756
+ * Three guards, each fixing a way this could hang or lie:
1757
+ * - a non-object is returned unchanged, so a primitive payload passes through;
1758
+ * - `depth > maxDepth` stops a self-referencing `@PropertyType` chain;
1759
+ * - `seen` stops a *cyclic* JSON graph, which depth alone would only delay
1760
+ * (branching recursion is exponential, not linear).
1761
+ */
1762
+ function hydrateInto(raw, DtoClass, options, depth, seen) {
1763
+ if (raw === null || typeof raw !== "object") return raw;
1764
+ if (depth > options.maxDepth) return raw;
1765
+ if (Array.isArray(raw)) return raw.map((item) => hydrateInto(item, DtoClass, options, depth + 1, seen));
1766
+ if (seen.has(raw)) return raw;
1767
+ seen.add(raw);
1768
+ try {
1769
+ return buildInstance(raw, DtoClass, options, depth, seen);
1770
+ } finally {
1771
+ seen.delete(raw);
1772
+ }
1773
+ }
1774
+ /**
1775
+ * Turn a plain JSON payload into an instance of `DtoClass`.
1776
+ *
1777
+ * A primitive, `null`, an unknown class or an over-deep value is returned
1778
+ * unchanged rather than wrapped: the caller asked for a class, but a response that
1779
+ * does not look like one is more useful as-is than as an empty instance.
1780
+ *
1781
+ * ```ts
1782
+ * const user = hydrate(raw, UserDto);
1783
+ * user instanceof UserDto; // true
1784
+ * ```
1785
+ */
1786
+ function hydrate(raw, DtoClass, options = {}) {
1787
+ return hydrateInto(raw, DtoClass, {
1788
+ keepUnknown: options.keepUnknown ?? false,
1789
+ maxDepth: options.maxDepth ?? 32,
1790
+ ctx: options.ctx
1791
+ }, 0, /* @__PURE__ */ new WeakSet());
1792
+ }
1793
+ //#endregion
1794
+ //#region src/plugins/transform/plugin.ts
1795
+ /**
1796
+ * Hydrate the live response payload in place (by replacing the response body).
1797
+ *
1798
+ * A hydration failure must not corrupt the response: a throwing DTO constructor or
1799
+ * a broken `fromJSON` is reported and the raw JSON is left exactly as it arrived,
1800
+ * because a partially hydrated body is worse than an unhydrated one.
1801
+ */
1802
+ function transformResponse(ctx, Dto, options) {
1803
+ const response = ctx.getResponse();
1804
+ if (!response) return;
1805
+ const { dataKey } = ctx.serverOptions;
1806
+ try {
1807
+ const envelope = response.data;
1808
+ const hydrated = hydrate(unwrapEnvelope(envelope, dataKey), Dto, {
1809
+ keepUnknown: options.keepUnknown,
1810
+ maxDepth: options.maxDepth,
1811
+ ctx
1812
+ });
1813
+ ctx.setResponse({
1814
+ ...response,
1815
+ data: looksLikeEnvelope(envelope, dataKey) ? {
1816
+ ...envelope,
1817
+ [dataKey]: hydrated
1818
+ } : hydrated
1819
+ });
1820
+ } catch (error) {
1821
+ ctx.logger.warn(t("error.plugin.transform", ctx.fullName, String(error)));
1822
+ }
1823
+ }
1824
+ /**
1825
+ * Create the transform plugin.
1826
+ *
1827
+ * ```ts
1828
+ * Service.use(Transform());
1829
+ *
1830
+ * @Api("/user")
1831
+ * @Transform(UserDto)
1832
+ * class UserApi {
1833
+ * @Get("/:id")
1834
+ * getUser(@Params("id") id: string): Promise<UserDto> { return null!; }
1835
+ * }
1836
+ * ```
1837
+ *
1838
+ * Without a DTO — no decorator and no `dto` option — the plugin is a no-op and the
1839
+ * payload stays the plain object JSON.parse produced.
1840
+ */
1841
+ var transformPlugin = createPlugin({
1842
+ name: "transform",
1843
+ priority: 0,
1844
+ setup(options) {
1845
+ const fallbackDto = options?.dto;
1846
+ const resolved = {
1847
+ keepUnknown: options?.keepUnknown ?? false,
1848
+ maxDepth: options?.maxDepth ?? 32
1849
+ };
1850
+ return { afterResponse(ctx, next) {
1851
+ const Dto = resolveDto(ctx.apiClass, ctx.methodName) ?? fallbackDto;
1852
+ if (Dto) transformResponse(ctx, Dto, resolved);
1853
+ return next();
1854
+ } };
1855
+ }
1856
+ });
1857
+ //#endregion
1858
+ //#region src/plugins/transform/index.ts
1859
+ function Transform(input) {
1860
+ return typeof input === "function" ? Transform$1(input) : transformPlugin(input);
1861
+ }
1862
+ //#endregion
1863
+ //#region src/plugins/validate/decorators.ts
1864
+ /**
1865
+ * `@Validate(schema)` / `@ValidateResponse(schema)` — attach a zod schema to a
1866
+ * request or to a response body.
1867
+ *
1868
+ * Both work on an api class and on a single method. A class-level schema applies
1869
+ * to every method of the class; a method-level one overrides it, which is the only
1870
+ * ordering that lets one endpoint deviate without redeclaring the class default on
1871
+ * every sibling.
1872
+ */
1873
+ /** Metadata slot written by `@Validate`. */
1874
+ var REQUEST_SCHEMA_KEY = customMetadataKey("validate/request");
1875
+ /** Metadata slot written by `@ValidateResponse`. */
1876
+ var RESPONSE_SCHEMA_KEY = customMetadataKey("validate/response");
1877
+ /**
1878
+ * Build a class/method decorator that stores one schema.
1879
+ *
1880
+ * The value is checked at decoration time, not at request time: a typo like
1881
+ * `@Validate({})` would otherwise stay silent until a request ran, and then read
1882
+ * as "the payload is malformed" instead of "the decorator is wrong".
1883
+ */
1884
+ function schemaDecorator(key, label) {
1885
+ return (schema) => {
1886
+ if (!schema || typeof schema.safeParse !== "function") throw new SnailDecoratorError(`[snail] @${label}() expects a zod schema with a safeParse() method`);
1887
+ return ((target, propertyKey) => {
1888
+ defineMetadata(key, schema, target, propertyKey);
1889
+ });
1890
+ };
1891
+ }
1892
+ var defineRequestSchema = schemaDecorator(REQUEST_SCHEMA_KEY, "Validate");
1893
+ var defineResponseSchema = schemaDecorator(RESPONSE_SCHEMA_KEY, "ValidateResponse");
1894
+ /**
1895
+ * Validate the outgoing request body (or query) against a zod schema.
1896
+ *
1897
+ * ```ts
1898
+ * @Post("/")
1899
+ * @Validate(z.object({ name: z.string().min(1) }))
1900
+ * create(@Data() body: CreateUser): Promise<User> { return null!; }
1901
+ * ```
1902
+ */
1903
+ function Validate$1(schema) {
1904
+ return defineRequestSchema(schema);
1905
+ }
1906
+ /**
1907
+ * Validate the response payload against a zod schema.
1908
+ *
1909
+ * Response validation only ever warns: the backend, not the caller, decides what
1910
+ * it sends, and throwing away a usable payload because one field is unexpected
1911
+ * turns a cosmetic backend drift into a broken page.
1912
+ */
1913
+ function ValidateResponse(schema) {
1914
+ return defineResponseSchema(schema);
1915
+ }
1916
+ /** Read the request schema of one method: method first, then the api class. */
1917
+ function resolveRequestSchema(apiClass, methodName) {
1918
+ return getMetadata(REQUEST_SCHEMA_KEY, apiClass, methodName) ?? getMetadata(REQUEST_SCHEMA_KEY, apiClass);
1919
+ }
1920
+ /** Read the response schema of one method: method first, then the api class. */
1921
+ function resolveResponseSchema(apiClass, methodName) {
1922
+ return getMetadata(RESPONSE_SCHEMA_KEY, apiClass, methodName) ?? getMetadata(RESPONSE_SCHEMA_KEY, apiClass);
1923
+ }
1924
+ //#endregion
1925
+ //#region src/plugins/validate/type.ts
1926
+ /**
1927
+ * Thrown when the request payload does not satisfy its schema.
1928
+ *
1929
+ * The request is abandoned **before** anything reaches the network, which is the
1930
+ * whole point of validating a request: an invalid body that is still sent produces
1931
+ * a backend error that looks like a server bug, and a `4xx` that pollutes the
1932
+ * application's error handling for what is really a programmer mistake.
1933
+ *
1934
+ * `issues` is zod's own list, kept verbatim so the application can render field
1935
+ * paths and messages instead of parsing a string.
1936
+ */
1937
+ var SnailValidationError = class extends SnailError {
1938
+ constructor(message, options) {
1939
+ super(message, {
1940
+ code: "SNAIL_VALIDATION_ERROR",
1941
+ cause: options.cause
1942
+ });
1943
+ this.issues = options.issues;
1944
+ }
1945
+ };
1946
+ //#endregion
1947
+ //#region src/plugins/validate/plugin.ts
1948
+ /**
1949
+ * Zod validation plugin.
1950
+ *
1951
+ * ## The asymmetry, and why it is deliberate
1952
+ *
1953
+ * **An invalid request aborts the call.** The request never reaches the network:
1954
+ * a body that fails its own schema is a programmer mistake, and the backend error
1955
+ * it would produce points at the wrong layer.
1956
+ *
1957
+ * **An invalid response only warns.** The response is already here and the caller
1958
+ * asked for it; throwing it away because the backend added, renamed or retyped a
1959
+ * field turns a cosmetic drift into a broken page. The warning carries zod's
1960
+ * issues so the drift is still visible in the console.
1961
+ *
1962
+ * ## Priority
1963
+ *
1964
+ * `-50` — the reserved validate band. In the forward phase it runs after the
1965
+ * version (`50`) and the adapters (`0`) have finished shaping the request, and
1966
+ * before the cache (`-100`) is asked for a key. On the unwind side it runs after
1967
+ * the cache and before the adapters, so the response is validated *before* the
1968
+ * transform plugin replaces the payload with class instances.
1969
+ */
1970
+ /**
1971
+ * The value a request schema validates.
1972
+ *
1973
+ * Write verbs carry a body, read verbs carry query params. Validating the body
1974
+ * when there is one and the query otherwise means one decorator covers both, and
1975
+ * a request that carries neither (`undefined`) is left to the backend rather than
1976
+ * failing against an empty object.
1977
+ */
1978
+ function requestTarget(ctx) {
1979
+ return ctx.request.data !== void 0 ? ctx.request.data : ctx.request.params;
1980
+ }
1981
+ /**
1982
+ * Validate the outgoing request, throwing when `strict`.
1983
+ *
1984
+ * Never calls `next()` on failure: the chain ends, `send()` rejects with the
1985
+ * {@link SnailValidationError}, and no axios adapter is ever reached.
1986
+ */
1987
+ function validateRequest(ctx, schema, strict, next) {
1988
+ const target = requestTarget(ctx);
1989
+ if (target === void 0) return next();
1990
+ const parsed = schema.safeParse(target);
1991
+ if (parsed.success) return next();
1992
+ const message = t("error.plugin.validate.request", ctx.fullName);
1993
+ if (!strict) {
1994
+ ctx.logger.warn(message, parsed.error.issues);
1995
+ return next();
1996
+ }
1997
+ throw new SnailValidationError(message, { issues: parsed.error.issues });
1998
+ }
1999
+ /**
2000
+ * Validate the response payload and warn about it.
2001
+ *
2002
+ * A throwing logger or a malformed schema must not fail the request, so the whole
2003
+ * check is defensive: the caller still receives the payload no matter what.
2004
+ */
2005
+ function warnOnInvalidResponse(ctx, schema) {
2006
+ try {
2007
+ const response = ctx.getResponse();
2008
+ if (!response) return;
2009
+ const payload = unwrapEnvelope(response.data, ctx.serverOptions.dataKey);
2010
+ const parsed = schema.safeParse(payload);
2011
+ if (parsed.success) return;
2012
+ ctx.logger.warn(t("error.plugin.validate.response", ctx.fullName), parsed.error.issues);
2013
+ } catch (error) {
2014
+ ctx.logger.warn(t("error.plugin.validate.response", ctx.fullName), error);
2015
+ }
2016
+ }
2017
+ /**
2018
+ * Create the zod validation plugin.
2019
+ *
2020
+ * ```ts
2021
+ * Service.use(Validate({ strict: false }));
2022
+ * ```
2023
+ *
2024
+ * It is normally used without options, together with the decorators:
2025
+ * `Service.use(Validate())`.
2026
+ */
2027
+ var validatePlugin = createPlugin({
2028
+ name: "validate",
2029
+ priority: -50,
2030
+ setup(options) {
2031
+ const fallbackRequest = options?.request;
2032
+ const fallbackResponse = options?.response;
2033
+ const strict = options?.strict ?? true;
2034
+ return {
2035
+ beforeRequest(ctx, next) {
2036
+ const schema = resolveRequestSchema(ctx.apiClass, ctx.methodName) ?? fallbackRequest;
2037
+ if (!schema) return next();
2038
+ return validateRequest(ctx, schema, strict, next);
2039
+ },
2040
+ afterResponse(ctx, next) {
2041
+ const schema = resolveResponseSchema(ctx.apiClass, ctx.methodName) ?? fallbackResponse;
2042
+ if (schema) warnOnInvalidResponse(ctx, schema);
2043
+ return next();
2044
+ }
2045
+ };
2046
+ }
2047
+ });
2048
+ //#endregion
2049
+ //#region src/plugins/validate/index.ts
2050
+ function Validate(input) {
2051
+ return isZodSchema(input) ? Validate$1(input) : validatePlugin(input);
2052
+ }
2053
+ /** `true` for a zod schema — the only shape the decorator overload accepts. */
2054
+ function isZodSchema(value) {
2055
+ return typeof value?.safeParse === "function";
2056
+ }
2057
+ //#endregion
2058
+ //#region src/plugins/version/decorators.ts
2059
+ /**
2060
+ * `@Version(...)` — declare the api version of a class or of one method.
2061
+ *
2062
+ * Method wins over class, class wins over the plugin's `defaultVersion`, which is
2063
+ * the only ordering that lets one endpoint of a class deviate without forcing
2064
+ * every sibling to restate the class version.
2065
+ *
2066
+ * The decorator is intentionally metadata-only: it never touches the request or
2067
+ * the server, so decorating a class from a shared module cannot leak a version
2068
+ * into another server instance.
2069
+ */
2070
+ /**
2071
+ * Metadata slot written by `@Version`.
2072
+ *
2073
+ * One key covers both levels: a class decorator stores under the class slot and a
2074
+ * method decorator under the method name, so a reader never has to know which
2075
+ * decorator wrote the value.
2076
+ */
2077
+ var VERSION_KEY = customMetadataKey("versioning/version");
2078
+ /**
2079
+ * Declare the version of an api class or of a single request method.
2080
+ *
2081
+ * ```ts
2082
+ * @Api("/user")
2083
+ * @Version("1.2.0")
2084
+ * class UserApi {
2085
+ * @Get("/legacy")
2086
+ * @Version("0.9.0")
2087
+ * legacy(): Promise<void> { return null!; }
2088
+ * }
2089
+ * ```
2090
+ */
2091
+ function Version(version) {
2092
+ if (typeof version !== "string" || version.length === 0) throw new SnailDecoratorError("[snail] @Version() requires a non-empty version string");
2093
+ return ((target, propertyKey) => {
2094
+ defineMetadata(VERSION_KEY, version, target, propertyKey);
2095
+ });
2096
+ }
2097
+ /**
2098
+ * Read the version declared for one method.
2099
+ *
2100
+ * Reads the method slot first, then the class slot, and both walk the prototype
2101
+ * chain so a subclass inherits the version of its base api class.
2102
+ */
2103
+ function resolveDeclaredVersion(apiClass, methodName) {
2104
+ return getMetadata(VERSION_KEY, apiClass, methodName) ?? getMetadata(VERSION_KEY, apiClass);
2105
+ }
2106
+ //#endregion
2107
+ //#region src/plugins/version/plugin.ts
2108
+ /**
2109
+ * Version management plugin.
2110
+ *
2111
+ * ## What it rewrites
2112
+ *
2113
+ * Only the live request on the context — never `server.defaults.baseURL`, never
2114
+ * the shared axios instance.
2115
+ *
2116
+ * The pre-rewrite implementation baked the version into the server's `baseURL`
2117
+ * the first time any request ran. That leaked: the first method to run decided
2118
+ * the version of every later request on that server, so calling a `v2` endpoint
2119
+ * once silently moved the whole application to `v2`. Resolving the version per
2120
+ * request from the method/class metadata makes that unrepresentable.
2121
+ *
2122
+ * ## Priority
2123
+ *
2124
+ * `50` — inside the reserved version band. It runs after an interceptor (`100`)
2125
+ * so it sees the url the interceptor produced, and before the cache (`-100`) so
2126
+ * the cache hashes the versioned url instead of a url that changes under it.
2127
+ */
2128
+ /**
2129
+ * Default key or url segment name per transport.
2130
+ *
2131
+ * `url` keeps the conventional `v` marker, so a bare
2132
+ * `Versioning({ type: "url", defaultVersion: "1.0.0" })` turns `/user/1` into
2133
+ * `/v1.0.0/user/1`. Pass `key: ""` for a bare `/1.0.0/user/1` segment.
2134
+ */
2135
+ var DEFAULT_KEYS = {
2136
+ url: "v",
2137
+ header: "x-api-version",
2138
+ query: "v"
2139
+ };
2140
+ /**
2141
+ * Validate and normalise the factory options.
2142
+ *
2143
+ * Runs once per server, inside `setup`. Throwing here rolls the registration back,
2144
+ * so a misconfigured plugin is never half-installed and silently doing nothing.
2145
+ */
2146
+ function resolveVersioningOptions(options) {
2147
+ const type = options?.type ?? "url";
2148
+ if (type !== "url" && type !== "header" && type !== "query" && type !== "custom") throw new SnailPluginError(`[snail] Versioning() received an unknown type "${String(type)}"`, { pluginName: "versioning" });
2149
+ const defaultVersion = options?.defaultVersion;
2150
+ if (typeof defaultVersion !== "string" || defaultVersion.length === 0) throw new SnailPluginError("[snail] Versioning() requires a non-empty `defaultVersion`", { pluginName: "versioning" });
2151
+ if (type === "custom" && typeof options?.extractor !== "function") throw new SnailPluginError("[snail] Versioning({ type: \"custom\" }) requires an `extractor` function", { pluginName: "versioning" });
2152
+ return {
2153
+ type,
2154
+ defaultVersion,
2155
+ key: type === "custom" ? options?.key ?? "" : options?.key ?? DEFAULT_KEYS[type],
2156
+ extractor: options?.extractor
2157
+ };
2158
+ }
2159
+ /**
2160
+ * The mutable header bag of the request, created when a plugin or a decorator
2161
+ * has not produced an `AxiosHeaders` yet.
2162
+ */
2163
+ function headerBag(ctx) {
2164
+ if (!(ctx.request.headers instanceof AxiosHeaders)) ctx.request.headers = AxiosHeaders.from(ctx.request.headers ?? {});
2165
+ return ctx.request.headers;
2166
+ }
2167
+ /** `true` when `segment` already is one of the url's path segments. */
2168
+ function hasSegment(url, segment) {
2169
+ return (url.split(/[?#]/, 1)[0] ?? "").split("/").includes(segment);
2170
+ }
2171
+ /**
2172
+ * Prepend `<key><version>` as the first path segment.
2173
+ *
2174
+ * Absolute urls are left untouched — prefixing them would corrupt the host — and
2175
+ * a url that already carries the segment is returned as-is, so a method whose path
2176
+ * hard-codes the version is not rewritten twice.
2177
+ */
2178
+ function applyURLVersion(url, version, key) {
2179
+ const segment = `${key}${version}`;
2180
+ if (isAbsoluteURL(url) || hasSegment(url, segment)) return url;
2181
+ if (url.length === 0) return `/${segment}`;
2182
+ return `/${segment}${url.startsWith("/") ? "" : "/"}${url}`;
2183
+ }
2184
+ /** Write the version header, replacing any value the caller already set. */
2185
+ function applyHeaderVersion(ctx, version, key) {
2186
+ headerBag(ctx).set(key, version);
2187
+ }
2188
+ /** Merge the version into the query params without dropping the existing ones. */
2189
+ function applyQueryVersion(ctx, version, key) {
2190
+ const current = ctx.request.params;
2191
+ ctx.request.params = {
2192
+ ...isPlainObject(current) ? current : {},
2193
+ [key]: version
2194
+ };
2195
+ }
2196
+ /** Delegate to the caller's extractor and merge whatever patch it returns. */
2197
+ function applyCustomVersion(ctx, version, extractor) {
2198
+ const patch = extractor(version, ctx);
2199
+ if (!patch) return;
2200
+ if (typeof patch.url === "string") ctx.request.url = patch.url;
2201
+ if (patch.headers) {
2202
+ const headers = headerBag(ctx);
2203
+ for (const [key, value] of Object.entries(patch.headers)) headers.set(key, value);
2204
+ }
2205
+ if (patch.params) {
2206
+ const current = ctx.request.params;
2207
+ ctx.request.params = {
2208
+ ...isPlainObject(current) ? current : {},
2209
+ ...patch.params
2210
+ };
2211
+ }
2212
+ }
2213
+ /**
2214
+ * Resolve the effective version and rewrite the live request.
2215
+ *
2216
+ * The default version is applied too, not skipped: the backend wants `/v1.0.0`
2217
+ * for an undeclared method, and nothing else in the pipeline knows that. Only the
2218
+ * *logging* distinguishes an explicit version from the default.
2219
+ */
2220
+ function applyVersion(ctx, options) {
2221
+ const version = resolveDeclaredVersion(ctx.apiClass, ctx.methodName) ?? options.defaultVersion;
2222
+ if (version !== options.defaultVersion) {
2223
+ ctx.logger.info(t("info.version.change", ctx.fullName, version));
2224
+ ctx.logger.warn(t("warn.version.change", ctx.fullName, options.defaultVersion, version));
2225
+ }
2226
+ switch (options.type) {
2227
+ case "url":
2228
+ ctx.request.url = applyURLVersion(ctx.request.url ?? ctx.route, version, options.key);
2229
+ return;
2230
+ case "header":
2231
+ applyHeaderVersion(ctx, version, options.key);
2232
+ return;
2233
+ case "query":
2234
+ applyQueryVersion(ctx, version, options.key);
2235
+ return;
2236
+ case "custom":
2237
+ applyCustomVersion(ctx, version, options.extractor);
2238
+ return;
2239
+ }
2240
+ }
2241
+ /**
2242
+ * Create the version management plugin.
2243
+ *
2244
+ * ```ts
2245
+ * Service.use(Versioning({ type: "url", defaultVersion: "1.0.0" }));
2246
+ * ```
2247
+ *
2248
+ * The rewrite happens in `beforeRequest`, so it is the last thing that can change
2249
+ * the url before the request is hashed, cached and sent.
2250
+ */
2251
+ var Versioning = createPlugin({
2252
+ name: "versioning",
2253
+ priority: 50,
2254
+ setup(options) {
2255
+ const resolved = resolveVersioningOptions(options);
2256
+ return { beforeRequest(ctx, next) {
2257
+ applyVersion(ctx, resolved);
2258
+ return next();
2259
+ } };
2260
+ }
2261
+ });
2262
+ //#endregion
2263
+ export { AfterResponse, BeforeRequest, CACHE_PLUGIN_NAME, CACHE_PRIORITY, Cache, CacheManager, Cacheable, DEFAULT_L1_MAX_SIZE, ExposeName, HitSource, INTERCEPTOR_PLUGIN_NAME, INTERCEPTOR_PRIORITY, IndexedDBCacheAdapter, Interceptor, InterceptorManager, Invalidates, MemoryCacheAdapter, NoCache, POOL_ERROR_CODES, POOL_PLUGIN_NAME, POOL_PRIORITY, PropertyType, RequestPool, RequestPoolScheduler, SnailPoolError, SnailValidationError, Transform, Validate, ValidateResponse, Version, Versioning, WebStorageCacheAdapter, buildCacheKey, classAfterEntries, classBeforeEntries, clearPool, hydrate, isPoolError, makeCachedResponse, methodAfterEntries, methodBeforeEntries, poolStats, readCacheable, readInvalidates, readNoCache };
2264
+
2265
+ //# sourceMappingURL=index.js.map