@snail-js/api 0.1.28 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -605
  3. package/dist/adapter/plain.d.ts +12 -0
  4. package/dist/adapter/plain.d.ts.map +1 -0
  5. package/dist/adapter/react.d.ts +14 -0
  6. package/dist/adapter/react.d.ts.map +1 -0
  7. package/dist/adapter/registry.d.ts +16 -0
  8. package/dist/adapter/registry.d.ts.map +1 -0
  9. package/dist/adapter/vue.d.ts +15 -0
  10. package/dist/adapter/vue.d.ts.map +1 -0
  11. package/dist/chunks/custom-UchOSg_s.js +466 -0
  12. package/dist/chunks/custom-UchOSg_s.js.map +1 -0
  13. package/dist/chunks/download-lBC7oOTT.js +160 -0
  14. package/dist/chunks/download-lBC7oOTT.js.map +1 -0
  15. package/dist/chunks/object-Ct0w-h4i.js +74 -0
  16. package/dist/chunks/object-Ct0w-h4i.js.map +1 -0
  17. package/dist/chunks/plugin-D3RDffDG.js +742 -0
  18. package/dist/chunks/plugin-D3RDffDG.js.map +1 -0
  19. package/dist/chunks/react-C1em-gxy.js +62 -0
  20. package/dist/chunks/react-C1em-gxy.js.map +1 -0
  21. package/dist/chunks/request-qeRhihEF.js +40 -0
  22. package/dist/chunks/request-qeRhihEF.js.map +1 -0
  23. package/dist/chunks/response-D-ubkAFl.js +127 -0
  24. package/dist/chunks/response-D-ubkAFl.js.map +1 -0
  25. package/dist/chunks/use-download-FAjtdJ58.js +1816 -0
  26. package/dist/chunks/use-download-FAjtdJ58.js.map +1 -0
  27. package/dist/chunks/vue-Dd9EhkQ3.js +34 -0
  28. package/dist/chunks/vue-Dd9EhkQ3.js.map +1 -0
  29. package/dist/core/args.d.ts +54 -0
  30. package/dist/core/args.d.ts.map +1 -0
  31. package/dist/core/connection.d.ts +15 -0
  32. package/dist/core/connection.d.ts.map +1 -0
  33. package/dist/core/context.d.ts +124 -0
  34. package/dist/core/context.d.ts.map +1 -0
  35. package/dist/core/http-stream.d.ts +28 -0
  36. package/dist/core/http-stream.d.ts.map +1 -0
  37. package/dist/core/logger.d.ts +22 -0
  38. package/dist/core/logger.d.ts.map +1 -0
  39. package/dist/core/metadata.d.ts +92 -0
  40. package/dist/core/metadata.d.ts.map +1 -0
  41. package/dist/core/metadata.keys.d.ts +41 -0
  42. package/dist/core/metadata.keys.d.ts.map +1 -0
  43. package/dist/core/method.d.ts +177 -0
  44. package/dist/core/method.d.ts.map +1 -0
  45. package/dist/core/plugin-manager.d.ts +106 -0
  46. package/dist/core/plugin-manager.d.ts.map +1 -0
  47. package/dist/core/plugin.d.ts +113 -0
  48. package/dist/core/plugin.d.ts.map +1 -0
  49. package/dist/core/reconnect.d.ts +22 -0
  50. package/dist/core/reconnect.d.ts.map +1 -0
  51. package/dist/core/resolve-stream.d.ts +45 -0
  52. package/dist/core/resolve-stream.d.ts.map +1 -0
  53. package/dist/core/resolve.d.ts +50 -0
  54. package/dist/core/resolve.d.ts.map +1 -0
  55. package/dist/core/response.d.ts +60 -0
  56. package/dist/core/response.d.ts.map +1 -0
  57. package/dist/core/server.d.ts +132 -0
  58. package/dist/core/server.d.ts.map +1 -0
  59. package/dist/core/sse.d.ts +27 -0
  60. package/dist/core/sse.d.ts.map +1 -0
  61. package/dist/core/state-bag.d.ts +36 -0
  62. package/dist/core/state-bag.d.ts.map +1 -0
  63. package/dist/core/websocket.d.ts +29 -0
  64. package/dist/core/websocket.d.ts.map +1 -0
  65. package/dist/decorators/api.d.ts +24 -4
  66. package/dist/decorators/api.d.ts.map +1 -0
  67. package/dist/decorators/args.d.ts +77 -14
  68. package/dist/decorators/args.d.ts.map +1 -0
  69. package/dist/decorators/custom.d.ts +81 -0
  70. package/dist/decorators/custom.d.ts.map +1 -0
  71. package/dist/decorators/header.d.ts +22 -0
  72. package/dist/decorators/header.d.ts.map +1 -0
  73. package/dist/decorators/index.d.ts +17 -0
  74. package/dist/decorators/index.d.ts.map +1 -0
  75. package/dist/decorators/methods.d.ts +33 -0
  76. package/dist/decorators/methods.d.ts.map +1 -0
  77. package/dist/decorators/progress.d.ts +24 -6
  78. package/dist/decorators/progress.d.ts.map +1 -0
  79. package/dist/decorators/server.d.ts +28 -7
  80. package/dist/decorators/server.d.ts.map +1 -0
  81. package/dist/decorators/stream.d.ts +113 -0
  82. package/dist/decorators/stream.d.ts.map +1 -0
  83. package/dist/default/options.d.ts +19 -0
  84. package/dist/default/options.d.ts.map +1 -0
  85. package/dist/error/base.d.ts +20 -0
  86. package/dist/error/base.d.ts.map +1 -0
  87. package/dist/error/decorator.d.ts +14 -0
  88. package/dist/error/decorator.d.ts.map +1 -0
  89. package/dist/error/hook.d.ts +14 -0
  90. package/dist/error/hook.d.ts.map +1 -0
  91. package/dist/error/index.d.ts +8 -0
  92. package/dist/error/index.d.ts.map +1 -0
  93. package/dist/error/options.d.ts +12 -0
  94. package/dist/error/options.d.ts.map +1 -0
  95. package/dist/error/plugin.d.ts +11 -0
  96. package/dist/error/plugin.d.ts.map +1 -0
  97. package/dist/error/request.d.ts +29 -0
  98. package/dist/error/request.d.ts.map +1 -0
  99. package/dist/error/response.d.ts +36 -0
  100. package/dist/error/response.d.ts.map +1 -0
  101. package/dist/index.d.ts +56 -11
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +2034 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/locale/en.d.ts +5 -0
  106. package/dist/locale/en.d.ts.map +1 -0
  107. package/dist/locale/index.d.ts +57 -0
  108. package/dist/locale/index.d.ts.map +1 -0
  109. package/dist/locale/types.d.ts +11 -0
  110. package/dist/locale/types.d.ts.map +1 -0
  111. package/dist/locale/zh.d.ts +5 -0
  112. package/dist/locale/zh.d.ts.map +1 -0
  113. package/dist/plugins/cache/adapters/indexeddb.d.ts +52 -0
  114. package/dist/plugins/cache/adapters/indexeddb.d.ts.map +1 -0
  115. package/dist/plugins/cache/adapters/memory.d.ts +60 -0
  116. package/dist/plugins/cache/adapters/memory.d.ts.map +1 -0
  117. package/dist/plugins/cache/adapters/web-storage.d.ts +43 -0
  118. package/dist/plugins/cache/adapters/web-storage.d.ts.map +1 -0
  119. package/dist/plugins/cache/decorators.d.ts +62 -0
  120. package/dist/plugins/cache/decorators.d.ts.map +1 -0
  121. package/dist/plugins/cache/index.d.ts +41 -0
  122. package/dist/plugins/cache/index.d.ts.map +1 -0
  123. package/dist/plugins/cache/key.d.ts +48 -0
  124. package/dist/plugins/cache/key.d.ts.map +1 -0
  125. package/dist/plugins/cache/manager.d.ts +124 -0
  126. package/dist/plugins/cache/manager.d.ts.map +1 -0
  127. package/dist/plugins/cache/plugin.d.ts +64 -0
  128. package/dist/plugins/cache/plugin.d.ts.map +1 -0
  129. package/dist/plugins/cache/type.d.ts +116 -0
  130. package/dist/plugins/cache/type.d.ts.map +1 -0
  131. package/dist/plugins/index.d.ts +75 -0
  132. package/dist/plugins/index.d.ts.map +1 -0
  133. package/dist/plugins/index.js +2265 -0
  134. package/dist/plugins/index.js.map +1 -0
  135. package/dist/plugins/interceptor/decorators.d.ts +44 -0
  136. package/dist/plugins/interceptor/decorators.d.ts.map +1 -0
  137. package/dist/plugins/interceptor/index.d.ts +31 -0
  138. package/dist/plugins/interceptor/index.d.ts.map +1 -0
  139. package/dist/plugins/interceptor/manager.d.ts +27 -0
  140. package/dist/plugins/interceptor/manager.d.ts.map +1 -0
  141. package/dist/plugins/interceptor/plugin.d.ts +65 -0
  142. package/dist/plugins/interceptor/plugin.d.ts.map +1 -0
  143. package/dist/plugins/interceptor/type.d.ts +45 -0
  144. package/dist/plugins/interceptor/type.d.ts.map +1 -0
  145. package/dist/plugins/pool/index.d.ts +26 -0
  146. package/dist/plugins/pool/index.d.ts.map +1 -0
  147. package/dist/plugins/pool/plugin.d.ts +83 -0
  148. package/dist/plugins/pool/plugin.d.ts.map +1 -0
  149. package/dist/plugins/pool/scheduler.d.ts +153 -0
  150. package/dist/plugins/pool/scheduler.d.ts.map +1 -0
  151. package/dist/plugins/pool/type.d.ts +38 -0
  152. package/dist/plugins/pool/type.d.ts.map +1 -0
  153. package/dist/plugins/react/index.d.ts +20 -0
  154. package/dist/plugins/react/index.d.ts.map +1 -0
  155. package/dist/plugins/react/index.js +145 -0
  156. package/dist/plugins/react/index.js.map +1 -0
  157. package/dist/plugins/react/plugin.d.ts +33 -0
  158. package/dist/plugins/react/plugin.d.ts.map +1 -0
  159. package/dist/plugins/react/type.d.ts +32 -0
  160. package/dist/plugins/react/type.d.ts.map +1 -0
  161. package/dist/plugins/transform/decorators.d.ts +54 -0
  162. package/dist/plugins/transform/decorators.d.ts.map +1 -0
  163. package/dist/plugins/transform/hydrate.d.ts +47 -0
  164. package/dist/plugins/transform/hydrate.d.ts.map +1 -0
  165. package/dist/plugins/transform/index.d.ts +41 -0
  166. package/dist/plugins/transform/index.d.ts.map +1 -0
  167. package/dist/plugins/transform/plugin.d.ts +20 -0
  168. package/dist/plugins/transform/plugin.d.ts.map +1 -0
  169. package/dist/plugins/transform/type.d.ts +67 -0
  170. package/dist/plugins/transform/type.d.ts.map +1 -0
  171. package/dist/plugins/validate/decorators.d.ts +24 -0
  172. package/dist/plugins/validate/decorators.d.ts.map +1 -0
  173. package/dist/plugins/validate/index.d.ts +40 -0
  174. package/dist/plugins/validate/index.d.ts.map +1 -0
  175. package/dist/plugins/validate/plugin.d.ts +13 -0
  176. package/dist/plugins/validate/plugin.d.ts.map +1 -0
  177. package/dist/plugins/validate/type.d.ts +54 -0
  178. package/dist/plugins/validate/type.d.ts.map +1 -0
  179. package/dist/plugins/version/decorators.d.ts +22 -0
  180. package/dist/plugins/version/decorators.d.ts.map +1 -0
  181. package/dist/plugins/version/index.d.ts +23 -0
  182. package/dist/plugins/version/index.d.ts.map +1 -0
  183. package/dist/plugins/version/plugin.d.ts +13 -0
  184. package/dist/plugins/version/plugin.d.ts.map +1 -0
  185. package/dist/plugins/version/type.d.ts +47 -0
  186. package/dist/plugins/version/type.d.ts.map +1 -0
  187. package/dist/plugins/vue/index.d.ts +21 -0
  188. package/dist/plugins/vue/index.d.ts.map +1 -0
  189. package/dist/plugins/vue/index.js +114 -0
  190. package/dist/plugins/vue/index.js.map +1 -0
  191. package/dist/plugins/vue/plugin.d.ts +15 -0
  192. package/dist/plugins/vue/plugin.d.ts.map +1 -0
  193. package/dist/plugins/vue/type.d.ts +34 -0
  194. package/dist/plugins/vue/type.d.ts.map +1 -0
  195. package/dist/strategies/index.d.ts +2 -0
  196. package/dist/strategies/index.d.ts.map +1 -0
  197. package/dist/strategies/index.js +29 -0
  198. package/dist/strategies/index.js.map +1 -0
  199. package/dist/strategies/plain.d.ts +2 -0
  200. package/dist/strategies/plain.d.ts.map +1 -0
  201. package/dist/strategies/plain.js +23 -0
  202. package/dist/strategies/plain.js.map +1 -0
  203. package/dist/strategies/react.d.ts +2 -0
  204. package/dist/strategies/react.d.ts.map +1 -0
  205. package/dist/strategies/react.js +30 -0
  206. package/dist/strategies/react.js.map +1 -0
  207. package/dist/strategies/shared/adapter.d.ts +23 -0
  208. package/dist/strategies/shared/adapter.d.ts.map +1 -0
  209. package/dist/strategies/shared/dom.d.ts +53 -0
  210. package/dist/strategies/shared/dom.d.ts.map +1 -0
  211. package/dist/strategies/shared/error.d.ts +30 -0
  212. package/dist/strategies/shared/error.d.ts.map +1 -0
  213. package/dist/strategies/shared/method.d.ts +51 -0
  214. package/dist/strategies/shared/method.d.ts.map +1 -0
  215. package/dist/strategies/shared/public.d.ts +33 -0
  216. package/dist/strategies/shared/public.d.ts.map +1 -0
  217. package/dist/strategies/shared/queue.d.ts +30 -0
  218. package/dist/strategies/shared/queue.d.ts.map +1 -0
  219. package/dist/strategies/shared/retry.d.ts +51 -0
  220. package/dist/strategies/shared/retry.d.ts.map +1 -0
  221. package/dist/strategies/shared/state.d.ts +117 -0
  222. package/dist/strategies/shared/state.d.ts.map +1 -0
  223. package/dist/strategies/shared/timing.d.ts +53 -0
  224. package/dist/strategies/shared/timing.d.ts.map +1 -0
  225. package/dist/strategies/shared/watcher.d.ts +37 -0
  226. package/dist/strategies/shared/watcher.d.ts.map +1 -0
  227. package/dist/strategies/use-auto-request.d.ts +77 -0
  228. package/dist/strategies/use-auto-request.d.ts.map +1 -0
  229. package/dist/strategies/use-download.d.ts +103 -0
  230. package/dist/strategies/use-download.d.ts.map +1 -0
  231. package/dist/strategies/use-fetcher.d.ts +52 -0
  232. package/dist/strategies/use-fetcher.d.ts.map +1 -0
  233. package/dist/strategies/use-pagination.d.ts +101 -0
  234. package/dist/strategies/use-pagination.d.ts.map +1 -0
  235. package/dist/strategies/use-request.d.ts +50 -0
  236. package/dist/strategies/use-request.d.ts.map +1 -0
  237. package/dist/strategies/use-retriable-request.d.ts +46 -0
  238. package/dist/strategies/use-retriable-request.d.ts.map +1 -0
  239. package/dist/strategies/use-sse.d.ts +103 -0
  240. package/dist/strategies/use-sse.d.ts.map +1 -0
  241. package/dist/strategies/use-token-auth.d.ts +88 -0
  242. package/dist/strategies/use-token-auth.d.ts.map +1 -0
  243. package/dist/strategies/use-uploader.d.ts +88 -0
  244. package/dist/strategies/use-uploader.d.ts.map +1 -0
  245. package/dist/strategies/use-watcher.d.ts +69 -0
  246. package/dist/strategies/use-watcher.d.ts.map +1 -0
  247. package/dist/typings/adapter.d.ts +61 -0
  248. package/dist/typings/adapter.d.ts.map +1 -0
  249. package/dist/typings/api.d.ts +122 -0
  250. package/dist/typings/api.d.ts.map +1 -0
  251. package/dist/typings/args.d.ts +51 -0
  252. package/dist/typings/args.d.ts.map +1 -0
  253. package/dist/typings/index.d.ts +8 -12
  254. package/dist/typings/index.d.ts.map +1 -0
  255. package/dist/typings/meta.d.ts +44 -0
  256. package/dist/typings/meta.d.ts.map +1 -0
  257. package/dist/typings/plugin.d.ts +114 -0
  258. package/dist/typings/plugin.d.ts.map +1 -0
  259. package/dist/typings/response.d.ts +118 -0
  260. package/dist/typings/response.d.ts.map +1 -0
  261. package/dist/typings/server.d.ts +83 -0
  262. package/dist/typings/server.d.ts.map +1 -0
  263. package/dist/typings/stream.d.ts +150 -0
  264. package/dist/typings/stream.d.ts.map +1 -0
  265. package/dist/utils/download.d.ts +97 -0
  266. package/dist/utils/download.d.ts.map +1 -0
  267. package/dist/utils/emitter.d.ts +36 -0
  268. package/dist/utils/emitter.d.ts.map +1 -0
  269. package/dist/utils/index.d.ts +6 -1
  270. package/dist/utils/index.d.ts.map +1 -0
  271. package/dist/utils/is.d.ts +22 -0
  272. package/dist/utils/is.d.ts.map +1 -0
  273. package/dist/utils/object.d.ts +44 -0
  274. package/dist/utils/object.d.ts.map +1 -0
  275. package/dist/utils/url.d.ts +44 -0
  276. package/dist/utils/url.d.ts.map +1 -0
  277. package/package.json +90 -27
  278. package/README_EN.md +0 -607
  279. package/dist/cache/index.d.ts +0 -9
  280. package/dist/cache/indexDBCache.d.ts +0 -13
  281. package/dist/cache/localstorageCache.d.ts +0 -10
  282. package/dist/cache/memoryCache.d.ts +0 -11
  283. package/dist/core/index.d.ts +0 -4
  284. package/dist/core/snailApi.d.ts +0 -22
  285. package/dist/core/snailMethod.d.ts +0 -49
  286. package/dist/core/snailServer.d.ts +0 -35
  287. package/dist/core/snailSse.d.ts +0 -20
  288. package/dist/decorators/cache.d.ts +0 -15
  289. package/dist/decorators/method.d.ts +0 -38
  290. package/dist/decorators/sse.d.ts +0 -30
  291. package/dist/decorators/strategy.d.ts +0 -10
  292. package/dist/decorators/versioning.d.ts +0 -16
  293. package/dist/eventEmmit/index.d.ts +0 -1
  294. package/dist/eventEmmit/snail.event.d.ts +0 -10
  295. package/dist/snail-api.js +0 -4871
  296. package/dist/snail-api.umd.cjs +0 -4875
  297. package/dist/strategies/jwtStrategy.d.ts +0 -4
  298. package/dist/typings/api.option.d.ts +0 -10
  299. package/dist/typings/apiProxy.d.ts +0 -8
  300. package/dist/typings/cache.management.option.d.ts +0 -25
  301. package/dist/typings/cache.type.d.ts +0 -23
  302. package/dist/typings/content.type.d.ts +0 -5
  303. package/dist/typings/request.body.d.ts +0 -3
  304. package/dist/typings/response.data.d.ts +0 -13
  305. package/dist/typings/snail.event.d.ts +0 -6
  306. package/dist/typings/snail.method.d.ts +0 -14
  307. package/dist/typings/snail.option.d.ts +0 -16
  308. package/dist/typings/sse.d.ts +0 -15
  309. package/dist/typings/strategy.d.ts +0 -5
  310. package/dist/typings/versioning.option.d.ts +0 -30
  311. package/dist/utils/function.d.ts +0 -31
  312. package/dist/versioning/index.d.ts +0 -1
  313. package/dist/versioning/versioning.d.ts +0 -12
@@ -0,0 +1,1816 @@
1
+ import { a as SnailHttpError, n as createPlugin, o as SnailResponseError } from "./plugin-D3RDffDG.js";
2
+ import { t as SnailCancelledError } from "./request-qeRhihEF.js";
3
+ import { n as deferred, r as noop } from "./object-Ct0w-h4i.js";
4
+ import { i as Emitter, r as triggerDownload } from "./download-lBC7oOTT.js";
5
+ import { isCancel } from "axios";
6
+ //#region src/adapter/plain.ts
7
+ /**
8
+ * Framework-free state adapter.
9
+ *
10
+ * A plain mutable box. Values update correctly, they just do not *trigger*
11
+ * anything — which is exactly right for a test, an SSR pass or a script, and is
12
+ * the safe default when no UI framework is present.
13
+ *
14
+ * A framework adapter registered with `setStateAdapter` replaces it.
15
+ */
16
+ var plainStateAdapter = {
17
+ name: "plain",
18
+ create(initial) {
19
+ return { value: initial };
20
+ },
21
+ read(ref) {
22
+ return ref.value;
23
+ },
24
+ write(ref, value) {
25
+ ref.value = value;
26
+ }
27
+ };
28
+ //#endregion
29
+ //#region src/adapter/registry.ts
30
+ /**
31
+ * The active state adapter.
32
+ *
33
+ * Strategies never touch `ref()` or `useState()` directly — they ask this
34
+ * registry. That is what lets one `useRequest` implementation serve Vue, React
35
+ * and framework-free code without the core importing any of them.
36
+ */
37
+ var active = plainStateAdapter;
38
+ /** Replace the active adapter. Called once by a framework entry point. */
39
+ function setStateAdapter(adapter) {
40
+ if (!adapter || typeof adapter.create !== "function") throw new TypeError("[snail] setStateAdapter() expects an object with a create() method");
41
+ active = adapter;
42
+ }
43
+ /** The active adapter. */
44
+ function getStateAdapter() {
45
+ return active;
46
+ }
47
+ //#endregion
48
+ //#region src/strategies/shared/adapter.ts
49
+ /**
50
+ * Resolve the state adapter one strategy instance should use.
51
+ *
52
+ * The adapter is resolved **once per hook** rather than per read. A hook that
53
+ * asked the registry on every write could observe two different adapters if an
54
+ * application called `setStateAdapter()` between two requests, and would then mix
55
+ * Vue refs with plain boxes in one set of handles.
56
+ */
57
+ function resolveStateAdapter(options = {}) {
58
+ return options.adapter ?? getStateAdapter();
59
+ }
60
+ /**
61
+ * Read a handle for the current render.
62
+ *
63
+ * `useBind` exists for frameworks that only re-render on an explicit
64
+ * subscription (React's `useSyncExternalStore`); Vue tracks the `.value` access
65
+ * itself, so its adapter omits the method and the read is the whole story. The
66
+ * `??` fallback — rather than a truthiness check — keeps a legitimate `false`,
67
+ * `0` or `""` from being replaced by a second read.
68
+ */
69
+ function bindRef(adapter, ref) {
70
+ return adapter.useBind?.(ref) ?? adapter.read(ref);
71
+ }
72
+ //#endregion
73
+ //#region src/strategies/shared/error.ts
74
+ /**
75
+ * `true` when an error means "this request was deliberately stopped".
76
+ *
77
+ * Cancellation is **expected control flow**, not a failure: `method.abort()` and
78
+ * a strategy discarding a stale response both produce it. Every strategy must ask
79
+ * this question before writing `error` state or firing `onError`, otherwise a
80
+ * user aborting a request would see a spurious error toast.
81
+ *
82
+ * The duck-typed `code` check is what catches an `AbortError` produced by a
83
+ * caller-supplied `AbortSignal`, which axios does not always wrap in its own
84
+ * cancel class.
85
+ */
86
+ function isCancellation(error) {
87
+ if (error instanceof SnailCancelledError) return true;
88
+ if (isCancel(error)) return true;
89
+ const code = error?.code;
90
+ return code === "ERR_CANCELED" || code === "ABORT_ERR";
91
+ }
92
+ /**
93
+ * Best-effort business/HTTP code of a failure, for the `code` state handle.
94
+ *
95
+ * Reading it from the error rather than from a response keeps `code` meaningful
96
+ * on the failure path too — a `401` should be visible to the UI even though no
97
+ * envelope ever passed validation.
98
+ */
99
+ function readErrorCode(error) {
100
+ if (error instanceof SnailResponseError) return error.businessCode;
101
+ if (error instanceof SnailHttpError) return error.status;
102
+ const candidate = error;
103
+ const value = candidate?.response?.status ?? candidate?.status;
104
+ return typeof value === "number" || typeof value === "string" ? value : void 0;
105
+ }
106
+ /**
107
+ * Best-effort human readable message of a failure, for the `message` handle.
108
+ *
109
+ * The backend envelope is preferred over the `Error.message` because an axios
110
+ * transport message ("Request failed with status code 401") is useless to show a
111
+ * user while `{ message: "token expired" }` is not.
112
+ */
113
+ function readErrorMessage(error) {
114
+ if (error instanceof SnailResponseError) {
115
+ const payload = error.payload;
116
+ if (payload && typeof payload.message === "string") return payload.message;
117
+ }
118
+ const message = error?.message;
119
+ return typeof message === "string" ? message : void 0;
120
+ }
121
+ //#endregion
122
+ //#region src/strategies/shared/state.ts
123
+ /** `true` when `key` was explicitly provided, even with an `undefined` value. */
124
+ function hasKey(source, key) {
125
+ return Object.prototype.hasOwnProperty.call(source, key);
126
+ }
127
+ /**
128
+ * Build the state handles, the listeners and the writers for one hook.
129
+ *
130
+ * All five refs are created here, even for a hook that will not use them: a
131
+ * `useFetcher({ withState: false })` still has to answer `abort()` and
132
+ * `onFinish()`, and creating state lazily would hand the UI handles that appear
133
+ * only after the first request.
134
+ */
135
+ function createStrategyState(options = {}) {
136
+ const adapter = resolveStateAdapter(options);
137
+ const loading = adapter.create(false);
138
+ const data = adapter.create(options.initialData);
139
+ const error = adapter.create(void 0);
140
+ const code = adapter.create(void 0);
141
+ const message = adapter.create(void 0);
142
+ const events = new Emitter();
143
+ return {
144
+ adapter,
145
+ state: {
146
+ loading,
147
+ data,
148
+ error,
149
+ code,
150
+ message,
151
+ abort() {
152
+ options.onAbort?.();
153
+ },
154
+ update(patch) {
155
+ if (hasKey(patch, "data")) adapter.write(data, patch.data);
156
+ if (hasKey(patch, "loading")) adapter.write(loading, patch.loading);
157
+ if (hasKey(patch, "error")) adapter.write(error, patch.error);
158
+ if (hasKey(patch, "code")) adapter.write(code, patch.code);
159
+ if (hasKey(patch, "message")) adapter.write(message, patch.message);
160
+ },
161
+ bind() {
162
+ return {
163
+ loading: bindRef(adapter, loading),
164
+ data: bindRef(adapter, data),
165
+ error: bindRef(adapter, error),
166
+ code: bindRef(adapter, code),
167
+ message: bindRef(adapter, message)
168
+ };
169
+ },
170
+ onSuccess(callback) {
171
+ return events.on("success", callback);
172
+ },
173
+ onError(callback) {
174
+ return events.on("error", callback);
175
+ },
176
+ onFinish(callback) {
177
+ return events.on("finish", callback);
178
+ }
179
+ },
180
+ setLoading(value) {
181
+ adapter.write(loading, value);
182
+ },
183
+ setData(value) {
184
+ adapter.write(data, value);
185
+ },
186
+ setError(value) {
187
+ adapter.write(error, value);
188
+ },
189
+ setCode(value) {
190
+ adapter.write(code, value);
191
+ },
192
+ setMessage(value) {
193
+ adapter.write(message, value);
194
+ },
195
+ resetForSend() {
196
+ adapter.write(error, void 0);
197
+ adapter.write(code, void 0);
198
+ adapter.write(message, void 0);
199
+ },
200
+ applySuccess(result) {
201
+ adapter.write(data, result.data);
202
+ adapter.write(code, result.code);
203
+ adapter.write(message, result.message);
204
+ adapter.write(error, void 0);
205
+ return result.data;
206
+ },
207
+ applyFailure(failure) {
208
+ adapter.write(error, failure);
209
+ adapter.write(code, readErrorCode(failure));
210
+ adapter.write(message, readErrorMessage(failure));
211
+ },
212
+ emitSuccess(value) {
213
+ events.emit("success", value);
214
+ },
215
+ emitError(failure) {
216
+ events.emit("error", failure);
217
+ },
218
+ emitFinish() {
219
+ events.emit("finish", void 0);
220
+ },
221
+ dispose() {
222
+ events.clear();
223
+ adapter.dispose?.(loading);
224
+ adapter.dispose?.(data);
225
+ adapter.dispose?.(error);
226
+ adapter.dispose?.(code);
227
+ adapter.dispose?.(message);
228
+ }
229
+ };
230
+ }
231
+ //#endregion
232
+ //#region src/strategies/shared/method.ts
233
+ /** Create a {@link MethodHolder} around a proxied api method. */
234
+ function createMethodHolder(method) {
235
+ let instance;
236
+ return {
237
+ get instance() {
238
+ return instance;
239
+ },
240
+ get pending() {
241
+ return instance?.pending ?? false;
242
+ },
243
+ resolve(args) {
244
+ instance ??= method(...args);
245
+ return instance;
246
+ },
247
+ abort() {
248
+ instance?.abort();
249
+ }
250
+ };
251
+ }
252
+ /**
253
+ * Attach a per-request upload progress callback.
254
+ *
255
+ * The core resolves `onUploadProgress` from `@UploadProgress(...)` metadata when
256
+ * the `SnailMethod` is built, which is once per method — not once per file. To
257
+ * vary it per request this writes onto the **live** axios config instead.
258
+ *
259
+ * The timing is load-bearing: `send()` calls `begin()` synchronously before its
260
+ * first `await`, so the config read here is the one axios will send. Calling this
261
+ * before `send()` would be silently overwritten by `begin()`, and calling it
262
+ * after an `await` would be too late.
263
+ *
264
+ * @returns `false` when there was no live config to attach to.
265
+ */
266
+ function attachUploadProgress(snail, listener) {
267
+ const config = snail.context?.request;
268
+ if (!config) return false;
269
+ const previous = config.onUploadProgress;
270
+ config.onUploadProgress = (event) => {
271
+ if (typeof previous === "function") previous(event);
272
+ listener(event);
273
+ };
274
+ return true;
275
+ }
276
+ //#endregion
277
+ //#region src/strategies/use-request.ts
278
+ /**
279
+ * Drive one api method from component state.
280
+ *
281
+ * ```ts
282
+ * const user = useRequest(userApi.getUser);
283
+ * await user.send("1"); // → payload
284
+ * user.data.value; // → the same payload
285
+ * ```
286
+ *
287
+ * The hook owns exactly one `SnailMethod`, built from the arguments of the first
288
+ * send and reused afterwards. That is what keeps the reactive handles stable
289
+ * across re-sends — see `shared/method.ts` for why a second instance would break
290
+ * the UI — while `send("2")` still overrides the arguments per call.
291
+ *
292
+ * One instance also means one request at a time: `SnailMethod` resets its context
293
+ * at the start of every `send()`, so a second `send()` issued while the first is
294
+ * still in flight would leave the first reading a context that belongs to the
295
+ * second. Call `abort()` first, or use `useWatcher`/`useAutoRequest`, which
296
+ * collapse bursts for exactly this reason.
297
+ */
298
+ function useRequest(method, options = {}) {
299
+ const holder = createMethodHolder(method);
300
+ const controller = createStrategyState({
301
+ adapter: options.adapter,
302
+ initialData: options.initialData,
303
+ onAbort: () => holder.abort()
304
+ });
305
+ const { state } = controller;
306
+ if (options.onSuccess) state.onSuccess(options.onSuccess);
307
+ if (options.onError) state.onError(options.onError);
308
+ if (options.onFinish) state.onFinish(options.onFinish);
309
+ async function send(...args) {
310
+ const snail = holder.resolve(args);
311
+ controller.resetForSend();
312
+ if (options.resetOnSend) controller.setData(options.initialData);
313
+ controller.setLoading(true);
314
+ try {
315
+ const result = await snail.send(...args);
316
+ const payload = controller.applySuccess(result);
317
+ controller.emitSuccess(payload);
318
+ return payload;
319
+ } catch (error) {
320
+ if (isCancellation(error)) throw error;
321
+ controller.applyFailure(error);
322
+ controller.emitError(error);
323
+ throw error;
324
+ } finally {
325
+ controller.setLoading(false);
326
+ controller.emitFinish();
327
+ }
328
+ }
329
+ const result = {
330
+ ...state,
331
+ send
332
+ };
333
+ if (options.immediate) send(...[]).catch(noop);
334
+ return result;
335
+ }
336
+ //#endregion
337
+ //#region src/strategies/shared/timing.ts
338
+ /**
339
+ * Sleep for `ms`, rejecting with a {@link SnailCancelledError} the moment the
340
+ * signal aborts.
341
+ *
342
+ * A plain `setTimeout` promise is the wrong primitive for a backoff: `abort()`
343
+ * during a 30 second retry delay would leave the loop asleep and the caller's
344
+ * promise pending long after it cancelled. The listener is `{ once: true }` and
345
+ * removed on either outcome so a long-lived signal cannot accumulate handlers.
346
+ */
347
+ function cancellableDelay(ms, signal) {
348
+ return new Promise((resolve, reject) => {
349
+ if (signal?.aborted) {
350
+ reject(new SnailCancelledError("wait cancelled before it started"));
351
+ return;
352
+ }
353
+ let timer;
354
+ const onAbort = () => {
355
+ if (timer !== void 0) clearTimeout(timer);
356
+ signal?.removeEventListener("abort", onAbort);
357
+ reject(new SnailCancelledError("wait cancelled"));
358
+ };
359
+ timer = setTimeout(() => {
360
+ signal?.removeEventListener("abort", onAbort);
361
+ resolve();
362
+ }, Math.max(0, ms));
363
+ signal?.addEventListener("abort", onAbort, { once: true });
364
+ });
365
+ }
366
+ /**
367
+ * Detach a Node timer from the event loop, when it has an `unref`.
368
+ *
369
+ * A polling strategy that is never stopped would otherwise keep a Node process
370
+ * (or a test worker) alive forever. In a browser `setTimeout` returns a number,
371
+ * which has no `unref` — hence the optional call rather than a feature test.
372
+ */
373
+ function unrefTimer(timer) {
374
+ timer?.unref?.();
375
+ }
376
+ /**
377
+ * Collapse a burst of `schedule()` calls into at most one request.
378
+ *
379
+ * Two modes, one rule — **the most recent task wins**, because a request must be
380
+ * sent with the latest arguments, never with the ones captured when the burst
381
+ * started:
382
+ *
383
+ * - `debounce` waits for `debounce` ms of quiet. Every new call pushes the run
384
+ * back, so ten keystrokes produce one request, not ten.
385
+ * - `throttle` runs on the leading edge and then books exactly one trailing run
386
+ * per window. Without the trailing run the last change in a burst would be
387
+ * silently dropped — a search box that ignores the final character.
388
+ *
389
+ * `debounce` is preferred when both are configured (see `useWatcher`): the two
390
+ * express contradictory intents, and picking one deterministically beats a
391
+ * half-debounced, half-throttled hybrid nobody can reason about.
392
+ */
393
+ function createRequestScheduler(options = {}) {
394
+ const debounce = options.debounce && options.debounce > 0 ? options.debounce : 0;
395
+ const throttle = options.throttle && options.throttle > 0 ? options.throttle : 0;
396
+ let timer;
397
+ let latest;
398
+ let lastRunAt = 0;
399
+ let ran = false;
400
+ const clear = () => {
401
+ if (timer !== void 0) {
402
+ clearTimeout(timer);
403
+ timer = void 0;
404
+ }
405
+ };
406
+ const runNow = () => {
407
+ const task = latest;
408
+ latest = void 0;
409
+ clear();
410
+ lastRunAt = Date.now();
411
+ ran = true;
412
+ task?.();
413
+ };
414
+ return {
415
+ get pending() {
416
+ return latest !== void 0;
417
+ },
418
+ schedule(task) {
419
+ latest = task;
420
+ if (debounce > 0) {
421
+ clear();
422
+ timer = setTimeout(runNow, debounce);
423
+ return;
424
+ }
425
+ if (throttle > 0) {
426
+ if (timer !== void 0) return;
427
+ const elapsed = Date.now() - lastRunAt;
428
+ if (!ran || elapsed >= throttle) {
429
+ runNow();
430
+ return;
431
+ }
432
+ timer = setTimeout(runNow, throttle - elapsed);
433
+ return;
434
+ }
435
+ runNow();
436
+ },
437
+ cancel() {
438
+ clear();
439
+ latest = void 0;
440
+ }
441
+ };
442
+ }
443
+ //#endregion
444
+ //#region src/strategies/shared/watcher.ts
445
+ /**
446
+ * Read one entry of a `watching()` result.
447
+ *
448
+ * `docs/guide/plugin-lifecycle.md`'s adapter contract exposes no way to ask "is this a state
449
+ * handle?" portably: the Vue adapter has `isState`, React's has none, and the
450
+ * plain adapter's boxes are bare `{ value }` objects. So three cases are handled,
451
+ * in order of confidence:
452
+ *
453
+ * 1. `adapter.isState(value)` says yes — unwrap it (Vue).
454
+ * 2. the value is a plain object whose *only* own key is `value` — the exact shape
455
+ * the plain and React adapters allocate — unwrap it.
456
+ * 3. anything else is the value itself.
457
+ *
458
+ * Case 2 is a heuristic, and it is the reason `watching: () => [{ value: 1 }]`
459
+ * should be written as `() => [{ value: 1 }.value]` if the object is genuine
460
+ * data rather than a handle. Without it, `() => [pageRef]` — the natural thing to
461
+ * write — would compare the ref object itself and never detect a change.
462
+ */
463
+ function unwrapWatchedValue(adapter, value) {
464
+ if (adapter.isState?.(value)) return adapter.read(value);
465
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
466
+ const keys = Object.keys(value);
467
+ if (keys.length === 1 && keys[0] === "value") return value.value;
468
+ }
469
+ return value;
470
+ }
471
+ /**
472
+ * Evaluate a `watching()` function into the plain values that get compared.
473
+ *
474
+ * A watcher is user code, so a non-array return is normalised to a one-element
475
+ * list rather than iterated blindly — spreading a string would compare its
476
+ * characters, and spreading `undefined` would throw inside the hook.
477
+ */
478
+ function readWatchedValues(adapter, watching) {
479
+ const produced = watching();
480
+ return (Array.isArray(produced) ? produced : [produced]).map((value) => unwrapWatchedValue(adapter, value));
481
+ }
482
+ /**
483
+ * Compare two watched snapshots with `Object.is`.
484
+ *
485
+ * `Object.is` rather than `===` so `NaN` does not look like a change on every
486
+ * render — a watcher over a numeric field that happens to be `NaN` would
487
+ * otherwise re-send forever.
488
+ */
489
+ function shallowEqual(a, b) {
490
+ if (a === b) return true;
491
+ if (a.length !== b.length) return false;
492
+ for (let index = 0; index < a.length; index += 1) if (!Object.is(a[index], b[index])) return false;
493
+ return true;
494
+ }
495
+ //#endregion
496
+ //#region src/strategies/use-watcher.ts
497
+ /**
498
+ * Re-send a request when watched values move.
499
+ *
500
+ * ```ts
501
+ * const keyword = createState("");
502
+ * const search = useWatcher(searchApi.find, {
503
+ * watching: () => [keyword.value],
504
+ * debounce: 200
505
+ * });
506
+ *
507
+ * keyword.value = "a"; await search.send(); // sends
508
+ * await search.send(); // same values → no request
509
+ * keyword.value = "ab"; await search.send(); // sends again
510
+ * ```
511
+ *
512
+ * ## Why `send()` is the evaluation trigger
513
+ *
514
+ * `SnailStateAdapter` exposes `create`/`read`/`write` and an optional
515
+ * `subscribe`; it has no `watch` or `effect`, and the core deliberately keeps
516
+ * framework reactivity out of the strategy layer. So there is nothing to fire at
517
+ * the *moment* a value changes. The hook therefore evaluates `watching()` when
518
+ * `send()` is called and treats an unchanged snapshot as a no-op, which keeps the
519
+ * behaviour deterministic and framework-free: a Vue/React integration can call
520
+ * `send()` from its own reactive effect, and a script calls it directly.
521
+ *
522
+ * `debounce`/`throttle` then collapse the *bursts* of `send()` calls a reactive
523
+ * effect produces. Every collapsed caller's promise settles with the single
524
+ * request's outcome, so no `await` is ever left hanging.
525
+ */
526
+ function useWatcher(method, options) {
527
+ const holder = createMethodHolder(method);
528
+ const scheduler = createRequestScheduler({
529
+ debounce: options.debounce,
530
+ throttle: options.throttle
531
+ });
532
+ let settleWaiters = [];
533
+ const rejectWaiters = (error) => {
534
+ const waiters = settleWaiters;
535
+ settleWaiters = [];
536
+ for (const waiter of waiters) waiter.reject(error);
537
+ };
538
+ const controller = createStrategyState({
539
+ adapter: options.adapter,
540
+ initialData: options.initialData,
541
+ onAbort: () => {
542
+ scheduler.cancel();
543
+ rejectWaiters(new SnailCancelledError("watched request cancelled before it was sent"));
544
+ holder.abort();
545
+ }
546
+ });
547
+ const { state } = controller;
548
+ const watching = controller.adapter.create(true);
549
+ let snapshot;
550
+ let queuedArgs;
551
+ if (options.onSuccess) state.onSuccess(options.onSuccess);
552
+ if (options.onError) state.onError(options.onError);
553
+ if (options.onFinish) state.onFinish(options.onFinish);
554
+ async function perform() {
555
+ const waiters = settleWaiters;
556
+ settleWaiters = [];
557
+ if (waiters.length === 0) return;
558
+ const args = queuedArgs ?? [];
559
+ const snail = holder.resolve(args);
560
+ controller.resetForSend();
561
+ if (options.resetOnSend) controller.setData(options.initialData);
562
+ controller.setLoading(true);
563
+ try {
564
+ const result = await snail.send(...args);
565
+ const payload = controller.applySuccess(result);
566
+ controller.emitSuccess(payload);
567
+ for (const waiter of waiters) waiter.resolve(payload);
568
+ } catch (error) {
569
+ if (!isCancellation(error)) {
570
+ controller.applyFailure(error);
571
+ controller.emitError(error);
572
+ }
573
+ for (const waiter of waiters) waiter.reject(error);
574
+ } finally {
575
+ controller.setLoading(false);
576
+ controller.emitFinish();
577
+ }
578
+ }
579
+ function send(...args) {
580
+ let values;
581
+ try {
582
+ values = readWatchedValues(controller.adapter, options.watching);
583
+ } catch (error) {
584
+ controller.setError(error);
585
+ controller.emitError(error);
586
+ return Promise.reject(error);
587
+ }
588
+ const changed = snapshot === void 0 || !shallowEqual(values, snapshot);
589
+ snapshot = values;
590
+ queuedArgs = args;
591
+ if (controller.adapter.read(watching) && !changed) return Promise.resolve(controller.adapter.read(state.data));
592
+ const promise = new Promise((resolve, reject) => {
593
+ settleWaiters.push({
594
+ resolve,
595
+ reject
596
+ });
597
+ });
598
+ scheduler.schedule(() => {
599
+ perform();
600
+ });
601
+ return promise;
602
+ }
603
+ const result = {
604
+ ...state,
605
+ watching,
606
+ send
607
+ };
608
+ if (options.immediate) send(...[]).catch(noop);
609
+ return result;
610
+ }
611
+ //#endregion
612
+ //#region src/strategies/use-fetcher.ts
613
+ function useFetcher(method, options = {}) {
614
+ const track = options.withState === true;
615
+ const holder = createMethodHolder(method);
616
+ const controller = createStrategyState({
617
+ adapter: options.adapter,
618
+ onAbort: () => holder.abort()
619
+ });
620
+ const { state } = controller;
621
+ if (options.onSuccess) state.onSuccess(options.onSuccess);
622
+ if (options.onError) state.onError(options.onError);
623
+ if (options.onFinish) state.onFinish(options.onFinish);
624
+ async function fetch(...args) {
625
+ const snail = holder.resolve(args);
626
+ if (track) {
627
+ controller.resetForSend();
628
+ controller.setLoading(true);
629
+ }
630
+ try {
631
+ const result = await snail.send(...args);
632
+ const payload = result.data;
633
+ if (track) controller.applySuccess(result);
634
+ controller.emitSuccess(payload);
635
+ return payload;
636
+ } catch (error) {
637
+ if (!isCancellation(error)) {
638
+ if (track) controller.applyFailure(error);
639
+ controller.emitError(error);
640
+ }
641
+ throw error;
642
+ } finally {
643
+ if (track) controller.setLoading(false);
644
+ controller.emitFinish();
645
+ }
646
+ }
647
+ const core = {
648
+ fetch,
649
+ abort: () => state.abort(),
650
+ onSuccess: (callback) => state.onSuccess(callback),
651
+ onError: (callback) => state.onError(callback),
652
+ onFinish: (callback) => state.onFinish(callback)
653
+ };
654
+ if (options.immediate) fetch(...[]).catch(noop);
655
+ if (!track) return core;
656
+ return {
657
+ ...core,
658
+ ...state
659
+ };
660
+ }
661
+ //#endregion
662
+ //#region src/strategies/use-pagination.ts
663
+ /** Default row extractor — array payloads, then the two common wrapper keys. */
664
+ function defaultList(payload) {
665
+ if (Array.isArray(payload)) return payload;
666
+ const candidate = payload;
667
+ if (Array.isArray(candidate?.list)) return candidate.list;
668
+ if (Array.isArray(candidate?.items)) return candidate.items;
669
+ return [];
670
+ }
671
+ /** Default total extractor — `total`, then `count`, then the current page size. */
672
+ function defaultTotal(payload) {
673
+ if (Array.isArray(payload)) return payload.length;
674
+ const candidate = payload;
675
+ if (typeof candidate?.total === "number") return candidate.total;
676
+ if (typeof candidate?.count === "number") return candidate.count;
677
+ return defaultList(payload).length;
678
+ }
679
+ /**
680
+ * Page through one api method.
681
+ *
682
+ * ```ts
683
+ * const users = usePagination(userApi.list, {
684
+ * total: (payload) => payload.total,
685
+ * list: (payload) => payload.rows
686
+ * });
687
+ * await users.reload();
688
+ * await users.next();
689
+ * ```
690
+ *
691
+ * `next()`/`prev()` are **no-ops at the bounds and fire no request**: a user
692
+ * holding down the "next" button at the last page must not hammer the server, and
693
+ * the promise still resolves (with `undefined`) so an `await` never hangs.
694
+ */
695
+ function usePagination(method, options = {}) {
696
+ const initialPage = Number.isFinite(options.initialPage) ? Math.max(1, Math.floor(options.initialPage)) : 1;
697
+ const initialPageSize = Number.isFinite(options.initialPageSize) ? Math.max(1, Math.floor(options.initialPageSize)) : 10;
698
+ const append = options.append === true;
699
+ const extractList = options.list ?? defaultList;
700
+ const extractTotal = options.total ?? defaultTotal;
701
+ /** The preloaded page, ready to be served without a request. */
702
+ let buffered;
703
+ let preloading = false;
704
+ const holder = createMethodHolder(method);
705
+ const controller = createStrategyState({
706
+ adapter: options.adapter,
707
+ onAbort: () => holder.abort()
708
+ });
709
+ const { state } = controller;
710
+ const adapter = controller.adapter;
711
+ const page = adapter.create(initialPage);
712
+ const pageSize = adapter.create(initialPageSize);
713
+ const total = adapter.create(0);
714
+ const list = adapter.create([]);
715
+ const isLastPage = adapter.create(false);
716
+ if (options.onSuccess) state.onSuccess(options.onSuccess);
717
+ if (options.onError) state.onError(options.onError);
718
+ if (options.onFinish) state.onFinish(options.onFinish);
719
+ /** Last page reachable, or `Infinity` while the total is still unknown. */
720
+ function lastPage() {
721
+ const size = Math.max(1, adapter.read(pageSize));
722
+ const known = adapter.read(total);
723
+ if (known > 0) return Math.max(1, Math.ceil(known / size));
724
+ return adapter.read(isLastPage) ? adapter.read(page) : Number.POSITIVE_INFINITY;
725
+ }
726
+ /** Write the paging derived from one payload. */
727
+ function applyPage(payload, target, size, accumulate) {
728
+ const rows = extractList(payload);
729
+ const known = extractTotal(payload);
730
+ adapter.write(page, target);
731
+ adapter.write(total, known);
732
+ adapter.write(list, accumulate ? [...adapter.read(list), ...rows] : rows);
733
+ adapter.write(isLastPage, known > 0 ? target * size >= known : rows.length < size);
734
+ }
735
+ async function request(target, size, accumulate) {
736
+ const snail = holder.resolve([{
737
+ page: target,
738
+ pageSize: size
739
+ }]);
740
+ controller.resetForSend();
741
+ controller.setLoading(true);
742
+ try {
743
+ const result = await snail.send({
744
+ page: target,
745
+ pageSize: size
746
+ });
747
+ const payload = controller.applySuccess(result);
748
+ applyPage(payload, target, size, accumulate);
749
+ controller.emitSuccess(payload);
750
+ if (options.preloadNext) startPreload(target + 1);
751
+ return payload;
752
+ } catch (error) {
753
+ if (!isCancellation(error)) {
754
+ controller.applyFailure(error);
755
+ controller.emitError(error);
756
+ }
757
+ throw error;
758
+ } finally {
759
+ controller.setLoading(false);
760
+ controller.emitFinish();
761
+ }
762
+ }
763
+ /**
764
+ * Warm the buffer with `target`, using a throwaway `SnailMethod`.
765
+ *
766
+ * A second instance is what keeps the preload invisible: it has its own context,
767
+ * so its `loading`/`data` never reach the caller's handles. A failed preload is
768
+ * swallowed on purpose — `next()` simply falls back to a real request.
769
+ */
770
+ async function startPreload(target) {
771
+ if (preloading) return;
772
+ if (target > lastPage()) return;
773
+ preloading = true;
774
+ try {
775
+ const size = Math.max(1, adapter.read(pageSize));
776
+ buffered = {
777
+ page: target,
778
+ payload: (await method({
779
+ page: target,
780
+ pageSize: size
781
+ }).send({
782
+ page: target,
783
+ pageSize: size
784
+ })).data
785
+ };
786
+ } catch {
787
+ buffered = void 0;
788
+ } finally {
789
+ preloading = false;
790
+ }
791
+ }
792
+ function clamp(target) {
793
+ const bound = lastPage();
794
+ return Math.min(Math.max(1, Number.isFinite(target) ? Math.floor(target) : 1), bound);
795
+ }
796
+ async function next() {
797
+ const current = adapter.read(page);
798
+ if (current >= lastPage()) return void 0;
799
+ const target = current + 1;
800
+ const size = Math.max(1, adapter.read(pageSize));
801
+ if (buffered && buffered.page === target) {
802
+ const ready = buffered;
803
+ buffered = void 0;
804
+ applyPage(ready.payload, target, size, append);
805
+ if (options.preloadNext) startPreload(target + 1);
806
+ return ready.payload;
807
+ }
808
+ return request(target, size, append);
809
+ }
810
+ async function prev() {
811
+ const current = adapter.read(page);
812
+ if (current <= 1) return void 0;
813
+ return request(current - 1, Math.max(1, adapter.read(pageSize)), append);
814
+ }
815
+ async function goTo(target) {
816
+ const resolved = clamp(target);
817
+ if (resolved === adapter.read(page)) return void 0;
818
+ return request(resolved, Math.max(1, adapter.read(pageSize)), false);
819
+ }
820
+ async function reload() {
821
+ const size = Math.max(1, adapter.read(pageSize));
822
+ adapter.write(list, []);
823
+ adapter.write(isLastPage, false);
824
+ buffered = void 0;
825
+ return request(initialPage, size, append);
826
+ }
827
+ async function changePageSize(size) {
828
+ if (!Number.isFinite(size) || size < 1) return void 0;
829
+ const resolved = Math.max(1, Math.floor(size));
830
+ adapter.write(pageSize, resolved);
831
+ adapter.write(list, []);
832
+ adapter.write(isLastPage, false);
833
+ buffered = void 0;
834
+ return request(initialPage, resolved, false);
835
+ }
836
+ return {
837
+ ...state,
838
+ page,
839
+ pageSize,
840
+ total,
841
+ list,
842
+ isLastPage,
843
+ next,
844
+ prev,
845
+ goTo,
846
+ reload,
847
+ changePageSize
848
+ };
849
+ }
850
+ //#endregion
851
+ //#region src/strategies/shared/dom.ts
852
+ /** The global `window`, or `undefined` outside a DOM environment. */
853
+ function getWindowTarget() {
854
+ if (typeof window === "undefined") return void 0;
855
+ return window;
856
+ }
857
+ /** The global `document`, or `undefined` outside a DOM environment. */
858
+ function getDocumentTarget() {
859
+ if (typeof document === "undefined") return void 0;
860
+ return document;
861
+ }
862
+ /**
863
+ * `true` when the page is on screen.
864
+ *
865
+ * Node reports `true` so a `visibilitychange`-driven strategy still works in a
866
+ * script; there is no hidden page to miss.
867
+ */
868
+ function isDocumentVisible() {
869
+ if (typeof document === "undefined") return true;
870
+ return document.visibilityState !== "hidden";
871
+ }
872
+ /**
873
+ * Create a listener bookkeeping scope.
874
+ *
875
+ * `stop()` and `dispose()` are called from event handlers, from `finally` blocks
876
+ * and sometimes twice; without one central registry it is far too easy to remove a
877
+ * listener with a *different* function reference than the one added, which leaks
878
+ * silently and keeps the component from being collected.
879
+ */
880
+ function createListenerScope() {
881
+ const registered = [];
882
+ return {
883
+ get size() {
884
+ return registered.length;
885
+ },
886
+ add(target, type, listener) {
887
+ if (!target) return false;
888
+ target.addEventListener(type, listener);
889
+ registered.push({
890
+ target,
891
+ type,
892
+ listener
893
+ });
894
+ return true;
895
+ },
896
+ removeAll() {
897
+ while (registered.length > 0) {
898
+ const entry = registered.pop();
899
+ entry.target.removeEventListener(entry.type, entry.listener);
900
+ }
901
+ }
902
+ };
903
+ }
904
+ //#endregion
905
+ //#region src/strategies/use-auto-request.ts
906
+ /**
907
+ * Keep a request fresh by itself: polling plus the three "the user is back"
908
+ * signals a dashboard actually needs.
909
+ *
910
+ * ```ts
911
+ * const stats = useAutoRequest(statsApi.get, {
912
+ * pollingInterval: 5000,
913
+ * refreshOnVisible: true
914
+ * });
915
+ * stats.start();
916
+ * stats.dispose(); // on unmount
917
+ * ```
918
+ *
919
+ * ## Lifecycle
920
+ *
921
+ * `stop()` is the single release point: it halts the polling timer **and** removes
922
+ * every listener, so an unmounted view cannot be kept alive by a focus handler.
923
+ * `start()` re-arms both. The refresh listeners are attached at creation rather
924
+ * than in `start()` — a hook whose only job is "refresh when the user comes back"
925
+ * should not need an extra call — while polling itself only runs between `start()`
926
+ * and `stop()`.
927
+ *
928
+ * ## Node safety
929
+ *
930
+ * `window`/`document` are looked up lazily and may be absent, so creating the hook
931
+ * during SSR is harmless. Every listener goes through one listener scope, which is
932
+ * what makes `stop()` remove exactly what was added; a listener removed with a
933
+ * fresh closure reference would leak silently. The polling timer is `unref`'d on
934
+ * Node so it never holds the process (or a test worker) open.
935
+ */
936
+ function useAutoRequest(method, options = {}) {
937
+ const request = useRequest(method, {
938
+ ...options,
939
+ immediate: false
940
+ });
941
+ const running = resolveStateAdapter(options).create(false);
942
+ const scope = createListenerScope();
943
+ const interval = Number.isFinite(options.pollingInterval) ? Math.max(0, options.pollingInterval) : 0;
944
+ let timer;
945
+ let disposed = false;
946
+ function clearTimer() {
947
+ if (timer !== void 0) {
948
+ clearTimeout(timer);
949
+ timer = void 0;
950
+ }
951
+ }
952
+ function refresh() {
953
+ return request.send(...[]);
954
+ }
955
+ /** Run one refresh without letting a rejection escape into a timer/listener. */
956
+ async function refreshQuietly() {
957
+ try {
958
+ await refresh();
959
+ } catch {}
960
+ }
961
+ function schedule() {
962
+ if (disposed || !running.value || interval <= 0) return;
963
+ timer = setTimeout(() => {
964
+ timer = void 0;
965
+ loop();
966
+ }, interval);
967
+ unrefTimer(timer);
968
+ }
969
+ async function loop() {
970
+ await refreshQuietly();
971
+ schedule();
972
+ }
973
+ function attach() {
974
+ scope.removeAll();
975
+ if (options.enableFocusRefresh) scope.add(getWindowTarget(), "focus", () => {
976
+ refreshQuietly();
977
+ });
978
+ if (options.enableReconnectRefresh) scope.add(getWindowTarget(), "online", () => {
979
+ refreshQuietly();
980
+ });
981
+ if (options.refreshOnVisible) scope.add(getDocumentTarget(), "visibilitychange", () => {
982
+ if (isDocumentVisible()) refreshQuietly();
983
+ });
984
+ }
985
+ function start() {
986
+ if (disposed || running.value) return;
987
+ running.value = true;
988
+ attach();
989
+ loop();
990
+ }
991
+ function stop() {
992
+ running.value = false;
993
+ clearTimer();
994
+ scope.removeAll();
995
+ }
996
+ function dispose() {
997
+ stop();
998
+ disposed = true;
999
+ }
1000
+ attach();
1001
+ if (options.immediate) start();
1002
+ return {
1003
+ ...request,
1004
+ running,
1005
+ start,
1006
+ stop,
1007
+ refresh,
1008
+ dispose
1009
+ };
1010
+ }
1011
+ //#endregion
1012
+ //#region src/strategies/shared/retry.ts
1013
+ /** Defaults are deliberately conservative: three tries, exponential, jittered. */
1014
+ var DEFAULT_RETRY_POLICY = {
1015
+ retries: 3,
1016
+ delayMs: 1e3,
1017
+ maxDelayMs: 3e4,
1018
+ factor: 2,
1019
+ jitter: true
1020
+ };
1021
+ /**
1022
+ * Fill in the retry defaults.
1023
+ *
1024
+ * `retries` is floored and clamped to `>= 0`: a negative value would make the
1025
+ * attempt loop run zero times and resolve without ever sending, which is a
1026
+ * silent no-op rather than a validation error the caller can see.
1027
+ */
1028
+ function resolveRetryPolicy(options = {}) {
1029
+ return {
1030
+ retries: Number.isFinite(options.retries) ? Math.max(0, Math.floor(options.retries)) : DEFAULT_RETRY_POLICY.retries,
1031
+ delayMs: Number.isFinite(options.delayMs) ? Math.max(0, options.delayMs) : DEFAULT_RETRY_POLICY.delayMs,
1032
+ maxDelayMs: Number.isFinite(options.maxDelayMs) ? Math.max(0, options.maxDelayMs) : DEFAULT_RETRY_POLICY.maxDelayMs,
1033
+ factor: Number.isFinite(options.factor) ? Math.max(1, options.factor) : DEFAULT_RETRY_POLICY.factor,
1034
+ jitter: options.jitter ?? DEFAULT_RETRY_POLICY.jitter
1035
+ };
1036
+ }
1037
+ /**
1038
+ * Delay before attempt `attempt` (1-based), with exponential growth and an
1039
+ * optional jitter.
1040
+ *
1041
+ * Jitter is *half* the computed delay plus a random share, not a fresh random
1042
+ * pick: full jitter can produce a near-zero delay that turns a retry storm into a
1043
+ * hammering loop, while half jitter still spreads a fleet of clients out.
1044
+ */
1045
+ function computeBackoffDelay(attempt, policy) {
1046
+ const exponent = Math.max(0, attempt - 1);
1047
+ const base = Math.min(policy.delayMs * policy.factor ** exponent, policy.maxDelayMs);
1048
+ if (!policy.jitter) return base;
1049
+ return base / 2 + Math.random() * (base / 2);
1050
+ }
1051
+ /**
1052
+ * The default {@link RetryPredicate}: retry everything except a cancellation.
1053
+ *
1054
+ * Retrying a cancellation is the one case that is always wrong — the caller asked
1055
+ * for the request to stop, so a retry ignores an explicit instruction and keeps
1056
+ * the socket busy.
1057
+ */
1058
+ var defaultRetryPredicate = (error) => !isCancellation(error);
1059
+ //#endregion
1060
+ //#region src/strategies/use-retriable-request.ts
1061
+ /**
1062
+ * A request that heals itself.
1063
+ *
1064
+ * ```ts
1065
+ * const save = useRetriableRequest(api.save, { retries: 3, delayMs: 200 });
1066
+ * await save.send(payload); // may hit the network up to 4 times
1067
+ * save.attempts.value; // how many tries it actually took
1068
+ * ```
1069
+ *
1070
+ * ## Cancellation is the hard part
1071
+ *
1072
+ * The hook keeps its own `AbortController` for the backoff *between* attempts,
1073
+ * because at that moment there is no in-flight request to cancel. `abort()` trips
1074
+ * both, so a `abort()` during a 30 second delay rejects immediately instead of
1075
+ * leaving the caller's promise pending until the timer fires. A cancellation is
1076
+ * never counted as a failed attempt and never written to `error`.
1077
+ *
1078
+ * `attempts` is exposed because it is genuinely useful for telemetry ("this call
1079
+ * needed three tries") and because it makes the retry loop testable without
1080
+ * counting requests.
1081
+ */
1082
+ function useRetriableRequest(method, options = {}) {
1083
+ const policy = resolveRetryPolicy(options);
1084
+ const retryAllowed = options.retryOn ?? defaultRetryPredicate;
1085
+ const holder = createMethodHolder(method);
1086
+ let backoff;
1087
+ const controller = createStrategyState({
1088
+ adapter: options.adapter,
1089
+ initialData: options.initialData,
1090
+ onAbort: () => {
1091
+ backoff?.abort();
1092
+ holder.abort();
1093
+ }
1094
+ });
1095
+ const { state } = controller;
1096
+ const attempts = controller.adapter.create(0);
1097
+ if (options.onSuccess) state.onSuccess(options.onSuccess);
1098
+ if (options.onError) state.onError(options.onError);
1099
+ if (options.onFinish) state.onFinish(options.onFinish);
1100
+ async function send(...args) {
1101
+ const snail = holder.resolve(args);
1102
+ const signal = new AbortController();
1103
+ backoff = signal;
1104
+ controller.resetForSend();
1105
+ if (options.resetOnSend) controller.setData(options.initialData);
1106
+ controller.setLoading(true);
1107
+ controller.adapter.write(attempts, 0);
1108
+ try {
1109
+ for (let attempt = 0; attempt <= policy.retries; attempt += 1) {
1110
+ controller.adapter.write(attempts, attempt + 1);
1111
+ try {
1112
+ const result = await snail.send(...args);
1113
+ const payload = controller.applySuccess(result);
1114
+ controller.emitSuccess(payload);
1115
+ return payload;
1116
+ } catch (error) {
1117
+ if (isCancellation(error)) throw error;
1118
+ if (!(attempt < policy.retries && retryAllowed(error, attempt + 1))) throw error;
1119
+ controller.applyFailure(error);
1120
+ await cancellableDelay(computeBackoffDelay(attempt + 1, policy), signal.signal);
1121
+ }
1122
+ }
1123
+ throw new SnailCancelledError("retry loop ended without a result");
1124
+ } catch (error) {
1125
+ if (!isCancellation(error)) {
1126
+ controller.applyFailure(error);
1127
+ controller.emitError(error);
1128
+ }
1129
+ throw error;
1130
+ } finally {
1131
+ if (backoff === signal) backoff = void 0;
1132
+ controller.setLoading(false);
1133
+ controller.emitFinish();
1134
+ }
1135
+ }
1136
+ return {
1137
+ ...state,
1138
+ send,
1139
+ attempts
1140
+ };
1141
+ }
1142
+ //#endregion
1143
+ //#region src/strategies/shared/queue.ts
1144
+ /**
1145
+ * Create a queue that runs at most `concurrency` tasks at a time.
1146
+ *
1147
+ * A task that rejects is swallowed rather than failing the queue: one broken
1148
+ * upload must not stall the remaining ones, and the caller observes the failure
1149
+ * through per-file state anyway. `concurrency` is clamped to at least 1, because a
1150
+ * queue with zero slots would never drain and `drain()` would never resolve.
1151
+ */
1152
+ function createTaskQueue(concurrency = 1) {
1153
+ const limit = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 1;
1154
+ const waiting = [];
1155
+ let active = 0;
1156
+ let idleWaiters = [];
1157
+ const settleIdle = () => {
1158
+ if (active > 0 || waiting.length > 0) return;
1159
+ const waiters = idleWaiters;
1160
+ idleWaiters = [];
1161
+ for (const waiter of waiters) waiter();
1162
+ };
1163
+ const pump = () => {
1164
+ while (active < limit && waiting.length > 0) {
1165
+ const task = waiting.shift();
1166
+ active += 1;
1167
+ Promise.resolve().then(task).catch(noop).then(() => {
1168
+ active -= 1;
1169
+ pump();
1170
+ settleIdle();
1171
+ });
1172
+ }
1173
+ settleIdle();
1174
+ };
1175
+ return {
1176
+ get active() {
1177
+ return active;
1178
+ },
1179
+ get pending() {
1180
+ return waiting.length;
1181
+ },
1182
+ add(task) {
1183
+ waiting.push(task);
1184
+ queueMicrotask(pump);
1185
+ },
1186
+ drain() {
1187
+ if (active === 0 && waiting.length === 0) return Promise.resolve();
1188
+ return new Promise((resolve) => {
1189
+ idleWaiters.push(resolve);
1190
+ });
1191
+ },
1192
+ clear() {
1193
+ waiting.length = 0;
1194
+ settleIdle();
1195
+ }
1196
+ };
1197
+ }
1198
+ //#endregion
1199
+ //#region src/strategies/use-uploader.ts
1200
+ /** Normalise the several shapes a file input hands out. */
1201
+ function normalizeFiles(input) {
1202
+ if (!input) return [];
1203
+ if (Array.isArray(input)) return input;
1204
+ if (typeof input.length === "number") return Array.from(input);
1205
+ return [input];
1206
+ }
1207
+ /** Keep a fraction inside `0`–`1`; a transport can report `loaded > total`. */
1208
+ function clamp01(value) {
1209
+ if (!Number.isFinite(value)) return 0;
1210
+ return Math.min(1, Math.max(0, value));
1211
+ }
1212
+ /**
1213
+ * Upload files with a bounded number of parallel requests.
1214
+ *
1215
+ * ```ts
1216
+ * const uploader = useUploader(api.upload, { concurrency: 2 });
1217
+ * await uploader.upload(input.files);
1218
+ * uploader.files.value; // per-file status, progress, error, response
1219
+ * uploader.retry(id); // re-queue one failure
1220
+ * ```
1221
+ *
1222
+ * ## Progress
1223
+ *
1224
+ * Per-file progress comes from the transport via axios' `onUploadProgress`, which
1225
+ * the hook attaches to the **live** request config of each file (see
1226
+ * `shared/method.ts` for why that has to happen right after `send()` starts). The
1227
+ * aggregate is the mean of the per-file values, and a finished file counts as `1`
1228
+ * regardless of what the transport reported — a mocked or `fetch`-based adapter
1229
+ * reports nothing at all, and without that rule the bar would stop at `0`.
1230
+ *
1231
+ * ## `data` and `code`
1232
+ *
1233
+ * The state handles are shared by the whole batch, so `data` holds the payload of
1234
+ * the most recently finished file. Use `files[i].response` for anything
1235
+ * per-file. `error` mirrors the file list: it holds the first failed file's error
1236
+ * while any file is in the `error` state, and clears itself once the last failure
1237
+ * has been retried successfully.
1238
+ */
1239
+ function useUploader(method, options = {}) {
1240
+ const fieldName = options.fieldName ?? "file";
1241
+ const queue = createTaskQueue(options.concurrency ?? 3);
1242
+ const inFlight = /* @__PURE__ */ new Set();
1243
+ let states = [];
1244
+ let sequence = 0;
1245
+ const controller = createStrategyState({
1246
+ adapter: options.adapter,
1247
+ onAbort: () => {
1248
+ for (const snail of [...inFlight]) snail.abort();
1249
+ inFlight.clear();
1250
+ queue.clear();
1251
+ for (const entry of states) {
1252
+ if (entry.status === "success") continue;
1253
+ patch(entry.id, {
1254
+ status: "pending",
1255
+ progress: 0,
1256
+ error: void 0
1257
+ });
1258
+ }
1259
+ }
1260
+ });
1261
+ const { state } = controller;
1262
+ const adapter = controller.adapter;
1263
+ const files = adapter.create([]);
1264
+ const progress = adapter.create(0);
1265
+ if (options.onSuccess) state.onSuccess(options.onSuccess);
1266
+ if (options.onError) state.onError(options.onError);
1267
+ if (options.onFinish) state.onFinish(options.onFinish);
1268
+ function writeStates() {
1269
+ adapter.write(files, states.slice());
1270
+ }
1271
+ function recalc() {
1272
+ const aggregate = states.length === 0 ? 0 : states.reduce((sum, entry) => sum + entry.progress, 0) / states.length;
1273
+ adapter.write(progress, aggregate);
1274
+ const failure = states.find((entry) => entry.status === "error");
1275
+ controller.setError(failure ? failure.error : void 0);
1276
+ options.onProgress?.({
1277
+ progress: aggregate,
1278
+ files: states.slice()
1279
+ });
1280
+ }
1281
+ function patch(id, changes) {
1282
+ states = states.map((entry) => entry.id === id ? {
1283
+ ...entry,
1284
+ ...changes
1285
+ } : entry);
1286
+ writeStates();
1287
+ recalc();
1288
+ }
1289
+ async function runFile(id) {
1290
+ const entry = states.find((candidate) => candidate.id === id);
1291
+ if (!entry || entry.status === "success" || entry.status === "uploading") return;
1292
+ patch(id, {
1293
+ status: "uploading",
1294
+ error: void 0
1295
+ });
1296
+ const form = new FormData();
1297
+ form.append(fieldName, entry.file, entry.file.name);
1298
+ const snail = method(form);
1299
+ inFlight.add(snail);
1300
+ try {
1301
+ const sending = snail.send(form);
1302
+ attachUploadProgress(snail, (event) => {
1303
+ if (!event.total || event.total <= 0) return;
1304
+ patch(id, { progress: clamp01(event.loaded / event.total) });
1305
+ });
1306
+ const result = await sending;
1307
+ patch(id, {
1308
+ status: "success",
1309
+ progress: 1,
1310
+ response: result.data,
1311
+ error: void 0
1312
+ });
1313
+ controller.setData(result.data);
1314
+ controller.setCode(result.code);
1315
+ controller.setMessage(result.message);
1316
+ controller.emitSuccess(result.data);
1317
+ } catch (error) {
1318
+ if (isCancellation(error)) {
1319
+ patch(id, {
1320
+ status: "pending",
1321
+ progress: 0,
1322
+ error: void 0
1323
+ });
1324
+ return;
1325
+ }
1326
+ patch(id, {
1327
+ status: "error",
1328
+ error
1329
+ });
1330
+ controller.applyFailure(error);
1331
+ controller.emitError(error);
1332
+ } finally {
1333
+ inFlight.delete(snail);
1334
+ recalc();
1335
+ }
1336
+ }
1337
+ async function upload(input) {
1338
+ const incoming = normalizeFiles(input);
1339
+ const accepted = options.multiple === false ? incoming.slice(0, 1) : incoming;
1340
+ if (accepted.length === 0) return;
1341
+ const queued = accepted.map((file) => {
1342
+ sequence += 1;
1343
+ return {
1344
+ id: `upload-${sequence}`,
1345
+ file,
1346
+ status: "pending",
1347
+ progress: 0,
1348
+ error: void 0,
1349
+ response: void 0
1350
+ };
1351
+ });
1352
+ controller.resetForSend();
1353
+ states = [...states, ...queued];
1354
+ writeStates();
1355
+ recalc();
1356
+ for (const entry of queued) queue.add(() => runFile(entry.id));
1357
+ await queue.drain();
1358
+ }
1359
+ function retry(id) {
1360
+ const entry = states.find((candidate) => candidate.id === id);
1361
+ if (!entry || entry.status === "success" || entry.status === "uploading") return;
1362
+ patch(id, {
1363
+ status: "pending",
1364
+ progress: 0,
1365
+ error: void 0
1366
+ });
1367
+ queue.add(() => runFile(id));
1368
+ }
1369
+ return {
1370
+ ...state,
1371
+ upload,
1372
+ files,
1373
+ progress,
1374
+ retry
1375
+ };
1376
+ }
1377
+ //#endregion
1378
+ //#region src/strategies/use-token-auth.ts
1379
+ /** `ctx.state` key holding the token one request was actually sent with. */
1380
+ var USED_TOKEN = "token-auth:used";
1381
+ /**
1382
+ * Read the HTTP status a failure carried, without assuming an axios error class.
1383
+ *
1384
+ * Only a transport-level 401 is recoverable here. A backend that answers HTTP 200
1385
+ * with `{ code: 401 }` produces its `SnailResponseError` in `SnailMethod.finalize`,
1386
+ * *after* the `beforeRequest` chain has already returned — and no hook can recover
1387
+ * a post-chain failure (`onError` is observe-only by design). Such a response is
1388
+ * left to the caller.
1389
+ */
1390
+ function statusOf(error) {
1391
+ const candidate = error;
1392
+ const status = candidate?.response?.status ?? candidate?.status;
1393
+ return typeof status === "number" ? status : void 0;
1394
+ }
1395
+ /**
1396
+ * Bearer-token authentication with single-flight refresh, as a plugin.
1397
+ *
1398
+ * ```ts
1399
+ * const auth = useTokenAuth({
1400
+ * token: () => localStorage.getItem("token"),
1401
+ * refresh: async () => (await api.refresh().send()).token,
1402
+ * onUnauthorized: () => router.push("/login")
1403
+ * });
1404
+ *
1405
+ * Service.use(auth.plugin);
1406
+ * auth.setToken("...");
1407
+ * ```
1408
+ *
1409
+ * ## Why a plugin and not a hook
1410
+ *
1411
+ * A token is global: every request of the service has to carry it, and a 401 on
1412
+ * any one of them has to invalidate the token for all of them. A per-method hook
1413
+ * could not see the other requests, so refresh coordination would be impossible.
1414
+ *
1415
+ * ## Exactly one refresh per wave of 401s
1416
+ *
1417
+ * Three parallel requests, one expired token, three 401s. Refreshing per request
1418
+ * would fire three refreshes and, with a rotating refresh token, two of them would
1419
+ * fail and log the user out. Two mechanisms prevent that:
1420
+ *
1421
+ * 1. a request that 401s while a refresh is already in flight **joins** it instead
1422
+ * of starting a second one;
1423
+ * 2. a request whose 401 was produced by a token that has since been replaced
1424
+ * replays directly, without refreshing again.
1425
+ *
1426
+ * The token each request was sent with is remembered in `ctx.state` (which the core
1427
+ * clears per send), and that is what makes rule 2 decidable without keeping
1428
+ * timers around.
1429
+ *
1430
+ * ## Replaying
1431
+ *
1432
+ * `onError` cannot recover a failure — the core rethrows after every `onError`
1433
+ * hook — so the recovery lives in `beforeRequest`, around `await next()`. The
1434
+ * replay re-runs the `requestInterceptor` reduce, the transport and the
1435
+ * `afterResponse` chain, so headers a plugin signs are recomputed for the retried
1436
+ * call. `beforeRequest` hooks are deliberately *not* re-entered: the refresh
1437
+ * decision has already been made, and re-entering could loop.
1438
+ *
1439
+ * ## What this cannot recover
1440
+ *
1441
+ * Only a **transport** 401 (HTTP 401). A backend that reports `{ code: 401 }` with
1442
+ * HTTP 200 fails in `finalize`, after the chain, where no hook can intervene; that
1443
+ * response reaches the caller as a `SnailResponseError`.
1444
+ */
1445
+ function useTokenAuth(options) {
1446
+ const header = options.header ?? "authorization";
1447
+ const scheme = options.scheme ?? "Bearer";
1448
+ let cached;
1449
+ let refreshing;
1450
+ let disposed = false;
1451
+ /** Resolves when the plugin is uninstalled; lets queued requests bail out. */
1452
+ const disposal = deferred();
1453
+ async function readToken() {
1454
+ if (cached !== void 0) return cached;
1455
+ const value = await options.token();
1456
+ if (typeof value === "string" && value.length > 0) cached = value;
1457
+ return cached;
1458
+ }
1459
+ function inject(ctx, token) {
1460
+ ctx.request.headers.set(header, scheme ? `${scheme} ${token}` : token);
1461
+ }
1462
+ /** Start the one refresh this wave gets, or return the one already running. */
1463
+ function startRefresh() {
1464
+ if (refreshing) return refreshing;
1465
+ const tracked = Promise.resolve().then(() => options.refresh()).then((token) => {
1466
+ if (typeof token === "string" && token.length > 0) cached = token;
1467
+ return cached ?? token;
1468
+ }).finally(() => {
1469
+ if (refreshing === tracked) refreshing = void 0;
1470
+ });
1471
+ refreshing = tracked;
1472
+ tracked.catch(noop);
1473
+ return tracked;
1474
+ }
1475
+ /**
1476
+ * Await a refresh, giving up early when the plugin is uninstalled.
1477
+ *
1478
+ * @returns `true` when a fresh token is available, `false` when the refresh
1479
+ * failed or the plugin went away — in both cases the caller rethrows the
1480
+ * **original** 401, because the refresh error is an implementation detail the
1481
+ * application cannot act on.
1482
+ */
1483
+ async function settleRefresh(pending) {
1484
+ try {
1485
+ await Promise.race([pending, disposal.promise]);
1486
+ } catch {
1487
+ return false;
1488
+ }
1489
+ return !disposed;
1490
+ }
1491
+ /**
1492
+ * Re-send the request that 401'd, with the refreshed token.
1493
+ *
1494
+ * The replay reproduces `SnailMethod.dispatch` using public plugin-manager
1495
+ * surface. It intentionally skips the `beforeRequest` chain — see the hook
1496
+ * documentation.
1497
+ */
1498
+ async function replay(ctx) {
1499
+ const manager = ctx.server.pluginManager;
1500
+ const config = manager.reduce("requestInterceptor", ctx.request, ctx);
1501
+ ctx.request = config;
1502
+ const response = await ctx.server.axios.request(config);
1503
+ ctx.setResponse(response);
1504
+ await manager.runChain("afterResponse", ctx);
1505
+ }
1506
+ return {
1507
+ plugin: createPlugin({
1508
+ name: "token-auth",
1509
+ priority: 20,
1510
+ setup(_pluginOptions, api) {
1511
+ api.onDispose(() => {
1512
+ disposed = true;
1513
+ disposal.resolve();
1514
+ });
1515
+ return { async beforeRequest(ctx, next) {
1516
+ if (disposed) {
1517
+ await next();
1518
+ return;
1519
+ }
1520
+ if (refreshing) await settleRefresh(refreshing);
1521
+ const token = await readToken();
1522
+ if (token) inject(ctx, token);
1523
+ ctx.state.set(USED_TOKEN, token);
1524
+ try {
1525
+ await next();
1526
+ } catch (error) {
1527
+ if (disposed || statusOf(error) !== 401) throw error;
1528
+ const used = ctx.state.get(USED_TOKEN);
1529
+ if (!(cached !== void 0 && used !== void 0 && cached !== used)) {
1530
+ if (!await settleRefresh(refreshing ?? startRefresh())) {
1531
+ options.onUnauthorized?.(error);
1532
+ throw error;
1533
+ }
1534
+ }
1535
+ const refreshed = await readToken();
1536
+ if (refreshed) inject(ctx, refreshed);
1537
+ try {
1538
+ await replay(ctx);
1539
+ } catch (replayError) {
1540
+ if (statusOf(replayError) === 401) options.onUnauthorized?.(replayError);
1541
+ throw replayError;
1542
+ }
1543
+ }
1544
+ } };
1545
+ }
1546
+ })(options),
1547
+ setToken(token) {
1548
+ cached = typeof token === "string" && token.length > 0 ? token : void 0;
1549
+ },
1550
+ getToken() {
1551
+ return cached;
1552
+ },
1553
+ clearToken() {
1554
+ cached = void 0;
1555
+ }
1556
+ };
1557
+ }
1558
+ //#endregion
1559
+ //#region src/strategies/use-sse.ts
1560
+ /** Find the message tap an endpoint or its connection offers, if any. */
1561
+ function attachTap(endpoint, connection, listener) {
1562
+ if (typeof endpoint.subscribe === "function") return endpoint.subscribe(listener) ?? noop;
1563
+ const onMessage = connection.onMessage;
1564
+ if (typeof onMessage === "function") return onMessage.call(connection, listener) ?? noop;
1565
+ return noop;
1566
+ }
1567
+ /**
1568
+ * Consume a Server-Sent Events endpoint as reactive state.
1569
+ *
1570
+ * ```ts
1571
+ * const events = Service.createSse(Events);
1572
+ * const feed = useSSE(events, { maxMessages: 50 });
1573
+ * feed.open();
1574
+ * feed.messages.value; // SnailSseMessage[]
1575
+ * feed.close();
1576
+ * ```
1577
+ *
1578
+ * ## The message tap
1579
+ *
1580
+ * `Service.createSse()` returns an object whose only member is `open()`, and the
1581
+ * messages it parses are dispatched to the decorated class's methods
1582
+ * (`@SseEvent()`), not to the caller. `useSSE` therefore accepts an endpoint that
1583
+ * additionally implements `subscribe(listener)`, or a connection that implements
1584
+ * `onMessage(listener)`. Both extend the core contract without changing it; an
1585
+ * endpoint that implements neither still reports `connected`/`error` faithfully,
1586
+ * it just never fills `messages`.
1587
+ *
1588
+ * ## Never an unhandled rejection
1589
+ *
1590
+ * A rejected `opened` promise is exactly how a failed SSE connect is reported, and
1591
+ * nothing else in the application will ever await it. Both `opened` and `closed`
1592
+ * are handled here, so a dead server cannot take the process down with an
1593
+ * unhandled rejection.
1594
+ */
1595
+ function useSSE(endpoint, options = {}) {
1596
+ const adapter = resolveStateAdapter(options);
1597
+ const maxMessages = Number.isFinite(options.maxMessages) ? Math.max(1, Math.floor(options.maxMessages)) : 100;
1598
+ const messages = adapter.create([]);
1599
+ const lastMessage = adapter.create(void 0);
1600
+ const connected = adapter.create(false);
1601
+ const error = adapter.create(void 0);
1602
+ let connection;
1603
+ let detach;
1604
+ let listening = false;
1605
+ function handle(message) {
1606
+ try {
1607
+ if (options.filter && !options.filter(message)) return;
1608
+ const buffered = [...adapter.read(messages), message];
1609
+ if (buffered.length > maxMessages) buffered.splice(0, buffered.length - maxMessages);
1610
+ adapter.write(messages, buffered);
1611
+ adapter.write(lastMessage, message);
1612
+ options.onMessage?.(message);
1613
+ } catch (cause) {
1614
+ adapter.write(error, cause);
1615
+ }
1616
+ adapter.write(connected, connection?.connected ?? adapter.read(connected));
1617
+ }
1618
+ function open() {
1619
+ if (listening) return;
1620
+ detach?.();
1621
+ detach = void 0;
1622
+ const next = endpoint.open();
1623
+ connection = next;
1624
+ listening = true;
1625
+ detach = attachTap(endpoint, next, handle);
1626
+ adapter.write(connected, next.connected);
1627
+ adapter.write(error, void 0);
1628
+ next.opened.then(() => {
1629
+ if (connection !== next) return;
1630
+ adapter.write(connected, true);
1631
+ adapter.write(error, void 0);
1632
+ }, (cause) => {
1633
+ if (connection !== next) return;
1634
+ adapter.write(connected, false);
1635
+ adapter.write(error, cause);
1636
+ });
1637
+ next.closed.then(() => {
1638
+ if (connection !== next) return;
1639
+ listening = false;
1640
+ adapter.write(connected, false);
1641
+ }, () => {
1642
+ if (connection !== next) return;
1643
+ listening = false;
1644
+ adapter.write(connected, false);
1645
+ });
1646
+ }
1647
+ function close() {
1648
+ const current = connection;
1649
+ connection = void 0;
1650
+ listening = false;
1651
+ adapter.write(connected, false);
1652
+ detach?.();
1653
+ detach = void 0;
1654
+ current?.close();
1655
+ }
1656
+ function clear() {
1657
+ adapter.write(messages, []);
1658
+ adapter.write(lastMessage, void 0);
1659
+ }
1660
+ if (options.immediate) open();
1661
+ return {
1662
+ messages,
1663
+ lastMessage,
1664
+ connected,
1665
+ error,
1666
+ open,
1667
+ close,
1668
+ clear,
1669
+ bind() {
1670
+ return {
1671
+ messages: bindRef(adapter, messages),
1672
+ lastMessage: bindRef(adapter, lastMessage),
1673
+ connected: bindRef(adapter, connected),
1674
+ error: bindRef(adapter, error)
1675
+ };
1676
+ }
1677
+ };
1678
+ }
1679
+ //#endregion
1680
+ //#region src/strategies/use-download.ts
1681
+ /**
1682
+ * Drive a server-prepared download.
1683
+ *
1684
+ * ```ts
1685
+ * @Api("/report")
1686
+ * class ReportApi {
1687
+ * /** Prepares the export server-side and answers with a temp url. *\/
1688
+ * @Post("/export")
1689
+ * create(@Data() query: ReportQuery): Promise<{ url: string; filename: string }> {
1690
+ * return null!;
1691
+ * }
1692
+ * }
1693
+ *
1694
+ * const { download, loading, error } = useDownload(reportApi.create);
1695
+ * await download({ from: "2026-01-01" });
1696
+ * ```
1697
+ *
1698
+ * ## Why it does not fetch the file
1699
+ *
1700
+ * `useDownload` awaits the request that *mints* a download URL, and only that. It
1701
+ * never fetches the file itself. Buffering a response into a `Blob` costs the whole
1702
+ * file in JavaScript memory — twice, once for the body and once for the object URL
1703
+ * — with no progress and no resume. Letting the browser fetch the URL instead gives
1704
+ * you the native download manager, streaming to disk, resume support and a download
1705
+ * that survives navigation.
1706
+ *
1707
+ * ## Why this is a strategy and not a plugin
1708
+ *
1709
+ * A plugin is for a cross-cutting concern that applies to requests an application
1710
+ * did not write specially: caching, interceptors, validation. A download is the
1711
+ * opposite — one explicit user action, with its own visible state and its own
1712
+ * failure modes, invoked from a click handler. That is the definition of a request
1713
+ * strategy. The reusable half that is *not* about reactivity — clicking a temporary
1714
+ * anchor — lives in `triggerDownload()`, which is exported from the package root so
1715
+ * a non-hook caller can use it directly.
1716
+ */
1717
+ function useDownload(method, options = {}) {
1718
+ const holder = createMethodHolder(method);
1719
+ const controller = createStrategyState({
1720
+ adapter: options.adapter,
1721
+ onAbort: () => holder.abort()
1722
+ });
1723
+ const { state, adapter } = controller;
1724
+ const info = adapter.create(void 0);
1725
+ const downloadListeners = /* @__PURE__ */ new Set();
1726
+ if (options.onSuccess) state.onSuccess(options.onSuccess);
1727
+ if (options.onError) state.onError(options.onError);
1728
+ if (options.onFinish) state.onFinish(options.onFinish);
1729
+ const triggerOptions = {
1730
+ openInNewTab: options.openInNewTab,
1731
+ container: options.container,
1732
+ referrerPolicy: options.referrerPolicy
1733
+ };
1734
+ /** Fire the browser download, surfacing a throwing callback as a state error. */
1735
+ function dispatch(descriptor) {
1736
+ const result = triggerDownload(descriptor.url, {
1737
+ ...triggerOptions,
1738
+ filename: options.filename ?? descriptor.filename
1739
+ });
1740
+ adapter.write(info, descriptor);
1741
+ for (const listener of [...downloadListeners]) try {
1742
+ listener(descriptor);
1743
+ } catch {}
1744
+ return result;
1745
+ }
1746
+ async function download(...args) {
1747
+ controller.resetForSend();
1748
+ controller.setLoading(true);
1749
+ try {
1750
+ const result = await holder.resolve(args).send(...args);
1751
+ const descriptor = resolveDescriptor(result.data, options);
1752
+ let triggered;
1753
+ if (options.autoTrigger === false) {
1754
+ adapter.write(info, descriptor);
1755
+ triggered = {
1756
+ url: descriptor.url,
1757
+ filename: descriptor.filename
1758
+ };
1759
+ } else triggered = dispatch(descriptor);
1760
+ controller.applySuccess(result);
1761
+ controller.emitSuccess(result.data);
1762
+ return triggered;
1763
+ } catch (error) {
1764
+ if (!isCancellation(error)) {
1765
+ controller.applyFailure(error);
1766
+ controller.emitError(error);
1767
+ }
1768
+ throw error;
1769
+ } finally {
1770
+ controller.setLoading(false);
1771
+ controller.emitFinish();
1772
+ }
1773
+ }
1774
+ return {
1775
+ ...state,
1776
+ download,
1777
+ info,
1778
+ onDownload(callback) {
1779
+ downloadListeners.add(callback);
1780
+ return () => {
1781
+ downloadListeners.delete(callback);
1782
+ };
1783
+ }
1784
+ };
1785
+ }
1786
+ /**
1787
+ * Turn a payload into a descriptor.
1788
+ *
1789
+ * Accepting several spellings is not sloppiness — it removes a naming argument from
1790
+ * every integration. A backend that answers `{ fileUrl }` should not force a `pick`
1791
+ * implementation for the sake of one word.
1792
+ */
1793
+ function resolveDescriptor(payload, options) {
1794
+ if (options.pick) {
1795
+ const descriptor = options.pick(payload);
1796
+ if (!descriptor || typeof descriptor.url !== "string" || descriptor.url.length === 0) throw new TypeError("[snail] useDownload: `pick` must return a descriptor with a non-empty `url`");
1797
+ return descriptor;
1798
+ }
1799
+ if (typeof payload === "string" && payload.length > 0) return { url: payload };
1800
+ const candidate = payload;
1801
+ if (candidate && typeof candidate === "object") {
1802
+ const url = candidate.url ?? candidate.downloadUrl ?? candidate.fileUrl;
1803
+ if (typeof url === "string" && url.length > 0) {
1804
+ const filename = candidate.filename ?? candidate.name;
1805
+ return filename ? {
1806
+ url,
1807
+ filename
1808
+ } : { url };
1809
+ }
1810
+ }
1811
+ throw new TypeError("[snail] useDownload: the response carried no download url. Expected a string, or an object with `url` / `downloadUrl` / `fileUrl`. Pass `pick` if your server names it differently.");
1812
+ }
1813
+ //#endregion
1814
+ export { useRetriableRequest as a, useFetcher as c, createStrategyState as d, setStateAdapter as f, useUploader as i, useWatcher as l, useSSE as n, useAutoRequest as o, plainStateAdapter as p, useTokenAuth as r, usePagination as s, useDownload as t, useRequest as u };
1815
+
1816
+ //# sourceMappingURL=use-download-FAjtdJ58.js.map