decantr 0.9.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 (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,839 @@
1
+ /**
2
+ * Server state / query cache module for Decantr.
3
+ *
4
+ * Exports: createQuery, createInfiniteQuery, createMutation, queryClient
5
+ *
6
+ * Zero dependencies beyond Decantr's own reactive primitives.
7
+ * @module data/query
8
+ */
9
+
10
+ import { createSignal, createEffect, createMemo, batch, untrack } from '../state/index.js';
11
+ import { _pendingQueries } from '../core/index.js';
12
+
13
+ // ─── Request Middleware ──────────────────────────────────────────
14
+
15
+ /**
16
+ * @typedef {Object} MiddlewareContext
17
+ * @property {string} url
18
+ * @property {string} method
19
+ * @property {Object} headers
20
+ * @property {*} body
21
+ * @property {*} [response] — populated on the way back through middleware
22
+ */
23
+
24
+ /** @type {Array<(ctx: MiddlewareContext, next: () => Promise<*>) => Promise<*>>} */
25
+ const middlewareChain = [];
26
+
27
+ /**
28
+ * Execute the middleware chain for a request context.
29
+ * Each middleware calls `next()` to pass control to the next middleware.
30
+ * Response flows back through in reverse order.
31
+ * @param {MiddlewareContext} ctx
32
+ * @param {() => Promise<*>} finalHandler — the actual fetch at the end of the chain
33
+ * @returns {Promise<*>}
34
+ */
35
+ async function runMiddleware(ctx, finalHandler) {
36
+ let index = 0;
37
+ async function next() {
38
+ if (index < middlewareChain.length) {
39
+ const mw = middlewareChain[index++];
40
+ return mw(ctx, next);
41
+ }
42
+ return finalHandler();
43
+ }
44
+ return next();
45
+ }
46
+
47
+ // ─── Glob Pattern Matching ──────────────────────────────────────
48
+
49
+ /**
50
+ * Convert a glob pattern to a RegExp.
51
+ * Supports `*` (any segment chars) and `**` (any path including dots).
52
+ * @param {string} pattern
53
+ * @returns {RegExp}
54
+ */
55
+ function globToRegex(pattern) {
56
+ let re = '';
57
+ for (let i = 0; i < pattern.length; i++) {
58
+ const ch = pattern[i];
59
+ if (ch === '*' && pattern[i + 1] === '*') {
60
+ re += '.*';
61
+ i++; // skip second *
62
+ } else if (ch === '*') {
63
+ re += '[^.]*';
64
+ } else if (ch === '?') {
65
+ re += '[^.]';
66
+ } else if ('.+^${}()|[]\\'.indexOf(ch) !== -1) {
67
+ re += '\\' + ch;
68
+ } else {
69
+ re += ch;
70
+ }
71
+ }
72
+ return new RegExp('^' + re + '$');
73
+ }
74
+
75
+ // ─── Internal cache ─────────────────────────────────────────────
76
+
77
+ /**
78
+ * @typedef {Object} CacheEntry
79
+ * @property {*} data
80
+ * @property {number} timestamp
81
+ * @property {Set<Function>} subscribers — active refetch callbacks
82
+ * @property {Promise|null} fetchPromise — in-flight dedup
83
+ * @property {AbortController|null} abortController
84
+ */
85
+
86
+ /** @type {Map<string, CacheEntry>} */
87
+ const cache = new Map();
88
+
89
+ /** @type {Map<string, number>} */
90
+ const gcTimers = new Map();
91
+
92
+ /**
93
+ * Get or create a cache entry.
94
+ * @param {string} key
95
+ * @returns {CacheEntry}
96
+ */
97
+ function getEntry(key) {
98
+ let entry = cache.get(key);
99
+ if (!entry) {
100
+ entry = { data: undefined, timestamp: 0, subscribers: new Set(), fetchPromise: null, abortController: null };
101
+ cache.set(key, entry);
102
+ }
103
+ return entry;
104
+ }
105
+
106
+ /**
107
+ * Schedule garbage collection for an inactive cache entry.
108
+ * @param {string} key
109
+ * @param {number} cacheTime
110
+ */
111
+ function scheduleGC(key, cacheTime) {
112
+ if (gcTimers.has(key)) clearTimeout(gcTimers.get(key));
113
+ gcTimers.set(key, setTimeout(() => {
114
+ const entry = cache.get(key);
115
+ if (entry && entry.subscribers.size === 0) {
116
+ cache.delete(key);
117
+ gcTimers.delete(key);
118
+ }
119
+ }, cacheTime));
120
+ }
121
+
122
+ /**
123
+ * Cancel a pending GC timer.
124
+ * @param {string} key
125
+ */
126
+ function cancelGC(key) {
127
+ if (gcTimers.has(key)) {
128
+ clearTimeout(gcTimers.get(key));
129
+ gcTimers.delete(key);
130
+ }
131
+ }
132
+
133
+ // ─── createQuery ────────────────────────────────────────────────
134
+
135
+ /**
136
+ * Reactive server-state query with caching, deduplication, and background refetch.
137
+ *
138
+ * @template T
139
+ * @param {string|(() => string)} key — static key or reactive key getter
140
+ * @param {(ctx: { key: string, signal: AbortSignal }) => Promise<T>} fetcher
141
+ * @param {Object} [options]
142
+ * @param {number} [options.staleTime=0] — ms before data is considered stale
143
+ * @param {number} [options.cacheTime=300000] — ms to retain inactive cache entries
144
+ * @param {number} [options.retry=3] — retry attempts on failure
145
+ * @param {number} [options.refetchInterval] — auto-refetch interval in ms
146
+ * @param {boolean} [options.refetchOnWindowFocus=true]
147
+ * @param {() => boolean} [options.enabled] — reactive getter; false = idle
148
+ * @param {(raw: T) => *} [options.select] — transform raw data
149
+ * @param {T} [options.initialData]
150
+ * @param {T} [options.placeholderData]
151
+ * @returns {{ data: () => T, status: () => string, error: () => Error|null, isLoading: () => boolean, isStale: () => boolean, isFetching: () => boolean, refetch: () => Promise<void>, setData: (v: T) => void }}
152
+ */
153
+ export function createQuery(key, fetcher, options = {}) {
154
+ const {
155
+ staleTime = 0,
156
+ cacheTime = 300000,
157
+ retry = 3,
158
+ refetchInterval,
159
+ refetchOnWindowFocus = true,
160
+ enabled,
161
+ select,
162
+ initialData,
163
+ placeholderData
164
+ } = options;
165
+
166
+ const resolveKey = typeof key === 'function' ? key : () => key;
167
+
168
+ // Signals
169
+ const init = initialData !== undefined ? initialData : placeholderData;
170
+ const [data, _setData] = createSignal(init !== undefined ? (select ? select(init) : init) : undefined);
171
+ const [status, setStatus] = createSignal(init !== undefined ? 'success' : 'idle');
172
+ const [error, setError] = createSignal(/** @type {Error|null} */ (null));
173
+ const [isFetching, setIsFetching] = createSignal(false);
174
+
175
+ const isLoading = createMemo(() => status() === 'loading');
176
+ const isStale = createMemo(() => {
177
+ const k = untrack(resolveKey);
178
+ const entry = cache.get(k);
179
+ if (!entry || !entry.timestamp) return true;
180
+ return Date.now() - entry.timestamp > staleTime;
181
+ });
182
+
183
+ let currentKey = /** @type {string|null} */ (null);
184
+ let intervalId = /** @type {number|null} */ (null);
185
+ let focusHandler = /** @type {Function|null} */ (null);
186
+
187
+ /**
188
+ * Core fetch with retry and deduplication.
189
+ * @param {string} k
190
+ * @param {boolean} [background=false]
191
+ * @returns {Promise<void>}
192
+ */
193
+ async function doFetch(k, background = false) {
194
+ const entry = getEntry(k);
195
+
196
+ // Deduplication: if an identical fetch is already in flight, piggyback on it
197
+ if (entry.fetchPromise) {
198
+ try {
199
+ await entry.fetchPromise;
200
+ const raw = entry.data;
201
+ batch(() => {
202
+ _setData(select ? select(raw) : raw);
203
+ setStatus('success');
204
+ setError(null);
205
+ });
206
+ } catch (err) {
207
+ batch(() => {
208
+ setError(err instanceof Error ? err : new Error(String(err)));
209
+ setStatus('error');
210
+ });
211
+ }
212
+ return;
213
+ }
214
+
215
+ // Abort previous fetch for this entry
216
+ if (entry.abortController) {
217
+ entry.abortController.abort();
218
+ entry.abortController = null;
219
+ }
220
+
221
+ const ac = new AbortController();
222
+ entry.abortController = ac;
223
+
224
+ if (!background) {
225
+ setStatus(entry.data !== undefined ? 'success' : 'loading');
226
+ }
227
+ setIsFetching(true);
228
+
229
+ const promise = (async () => {
230
+ let lastErr;
231
+ for (let attempt = 0; attempt <= retry; attempt++) {
232
+ if (ac.signal.aborted) return;
233
+ try {
234
+ /** @type {*} */
235
+ let result;
236
+ if (middlewareChain.length > 0) {
237
+ const ctx = { url: k, method: 'GET', headers: {}, body: undefined };
238
+ result = await runMiddleware(ctx, () => fetcher({ key: k, signal: ac.signal }));
239
+ } else {
240
+ result = await fetcher({ key: k, signal: ac.signal });
241
+ }
242
+ if (ac.signal.aborted) return;
243
+ entry.data = result;
244
+ entry.timestamp = Date.now();
245
+ batch(() => {
246
+ _setData(select ? select(result) : result);
247
+ setStatus('success');
248
+ setError(null);
249
+ setIsFetching(false);
250
+ });
251
+ return;
252
+ } catch (err) {
253
+ if (ac.signal.aborted) return;
254
+ // Don't retry AbortError
255
+ if (err && err.name === 'AbortError') return;
256
+ lastErr = err instanceof Error ? err : new Error(String(err));
257
+ if (attempt < retry) {
258
+ const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
259
+ await new Promise(r => setTimeout(r, delay));
260
+ }
261
+ }
262
+ }
263
+ // All retries exhausted
264
+ if (!ac.signal.aborted) {
265
+ batch(() => {
266
+ setError(lastErr);
267
+ setStatus('error');
268
+ setIsFetching(false);
269
+ });
270
+ }
271
+ })();
272
+
273
+ entry.fetchPromise = promise;
274
+ _pendingQueries.add(promise);
275
+
276
+ try {
277
+ await promise;
278
+ } finally {
279
+ if (entry.fetchPromise === promise) entry.fetchPromise = null;
280
+ if (entry.abortController === ac) entry.abortController = null;
281
+ _pendingQueries.delete(promise);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Public refetch — forces a fresh fetch for the current key.
287
+ * @returns {Promise<void>}
288
+ */
289
+ async function refetch() {
290
+ const k = untrack(resolveKey);
291
+ if (!k) return;
292
+ const entry = getEntry(k);
293
+ // Kill existing in-flight so we start fresh
294
+ if (entry.fetchPromise) {
295
+ if (entry.abortController) entry.abortController.abort();
296
+ entry.fetchPromise = null;
297
+ }
298
+ await doFetch(k, false);
299
+ }
300
+
301
+ /**
302
+ * Manually overwrite cached data for the current key.
303
+ * @param {T} value
304
+ */
305
+ function setData(value) {
306
+ const k = untrack(resolveKey);
307
+ if (!k) return;
308
+ const entry = getEntry(k);
309
+ entry.data = value;
310
+ entry.timestamp = Date.now();
311
+ _setData(select ? select(value) : value);
312
+ setStatus('success');
313
+ setError(null);
314
+ }
315
+
316
+ // Subscribe / unsubscribe helper for cache entry
317
+ function subscribe(k) {
318
+ const entry = getEntry(k);
319
+ cancelGC(k);
320
+ entry.subscribers.add(refetch);
321
+ }
322
+
323
+ function unsubscribe(k) {
324
+ const entry = cache.get(k);
325
+ if (entry) {
326
+ entry.subscribers.delete(refetch);
327
+ if (entry.subscribers.size === 0) scheduleGC(k, cacheTime);
328
+ }
329
+ }
330
+
331
+ // Reactive key tracking — triggers fetch when key changes
332
+ createEffect(() => {
333
+ const isEnabled = enabled ? enabled() : true;
334
+ const k = resolveKey();
335
+
336
+ if (!isEnabled || !k) {
337
+ if (status() !== 'idle') setStatus('idle');
338
+ return;
339
+ }
340
+
341
+ // Key changed — clean up old subscription
342
+ if (currentKey && currentKey !== k) {
343
+ unsubscribe(currentKey);
344
+ const oldEntry = cache.get(currentKey);
345
+ if (oldEntry && oldEntry.abortController) {
346
+ oldEntry.abortController.abort();
347
+ }
348
+ }
349
+ currentKey = k;
350
+ subscribe(k);
351
+
352
+ const entry = cache.get(k);
353
+ // Stale-while-revalidate: serve cached data immediately
354
+ if (entry && entry.data !== undefined) {
355
+ const raw = entry.data;
356
+ const isDataStale = Date.now() - entry.timestamp > staleTime;
357
+ batch(() => {
358
+ _setData(select ? select(raw) : raw);
359
+ setStatus('success');
360
+ setError(null);
361
+ });
362
+ if (isDataStale) {
363
+ doFetch(k, true);
364
+ }
365
+ } else {
366
+ doFetch(k, false);
367
+ }
368
+
369
+ // Cleanup on re-run / disposal
370
+ return () => {
371
+ unsubscribe(k);
372
+ };
373
+ });
374
+
375
+ // Refetch interval
376
+ if (refetchInterval) {
377
+ createEffect(() => {
378
+ const isEnabled = enabled ? enabled() : true;
379
+ if (!isEnabled) return;
380
+ intervalId = setInterval(() => {
381
+ const k = untrack(resolveKey);
382
+ if (k) doFetch(k, true);
383
+ }, refetchInterval);
384
+ return () => {
385
+ if (intervalId !== null) { clearInterval(intervalId); intervalId = null; }
386
+ };
387
+ });
388
+ }
389
+
390
+ // Window focus refetch
391
+ if (refetchOnWindowFocus && typeof window !== 'undefined') {
392
+ focusHandler = () => {
393
+ const isEnabled = enabled ? untrack(enabled) : true;
394
+ if (!isEnabled) return;
395
+ const k = untrack(resolveKey);
396
+ if (!k) return;
397
+ const entry = cache.get(k);
398
+ if (!entry || Date.now() - entry.timestamp > staleTime) {
399
+ doFetch(k, true);
400
+ }
401
+ };
402
+ window.addEventListener('focus', focusHandler);
403
+ createEffect(() => {
404
+ return () => {
405
+ if (focusHandler) {
406
+ window.removeEventListener('focus', focusHandler);
407
+ focusHandler = null;
408
+ }
409
+ };
410
+ });
411
+ }
412
+
413
+ return { data, status, error, isLoading, isStale, isFetching, refetch, setData };
414
+ }
415
+
416
+ // ─── createInfiniteQuery ────────────────────────────────────────
417
+
418
+ /**
419
+ * Infinite / paginated query. Accumulates pages and exposes a flat `allItems` view.
420
+ *
421
+ * @template T
422
+ * @param {string|(() => string)} key
423
+ * @param {(ctx: { key: string, pageParam: *, signal: AbortSignal }) => Promise<T>} fetcher
424
+ * @param {Object} options
425
+ * @param {(lastPage: T, allPages: T[]) => *} options.getNextPageParam — return next cursor or undefined
426
+ * @param {number} [options.staleTime=0]
427
+ * @param {number} [options.cacheTime=300000]
428
+ * @param {number} [options.retry=3]
429
+ * @param {() => boolean} [options.enabled]
430
+ * @returns {{ pages: () => T[], allItems: () => *[], hasNextPage: () => boolean, fetchNextPage: () => Promise<void>, isFetchingNextPage: () => boolean, refetch: () => Promise<void> }}
431
+ */
432
+ export function createInfiniteQuery(key, fetcher, options = {}) {
433
+ const {
434
+ getNextPageParam,
435
+ staleTime = 0,
436
+ cacheTime = 300000,
437
+ retry = 3,
438
+ enabled
439
+ } = options;
440
+
441
+ const resolveKey = typeof key === 'function' ? key : () => key;
442
+
443
+ const [pages, setPages] = createSignal(/** @type {T[]} */ ([]));
444
+ const [isFetchingNextPage, setIsFetchingNextPage] = createSignal(false);
445
+ const [status, setStatus] = createSignal(/** @type {string} */ ('idle'));
446
+ const [error, setError] = createSignal(/** @type {Error|null} */ (null));
447
+
448
+ const allItems = createMemo(() => {
449
+ const p = pages();
450
+ const items = [];
451
+ for (let i = 0; i < p.length; i++) {
452
+ const page = p[i];
453
+ if (Array.isArray(page)) {
454
+ for (let j = 0; j < page.length; j++) items.push(page[j]);
455
+ } else if (page && typeof page === 'object' && Array.isArray(page.items)) {
456
+ for (let j = 0; j < page.items.length; j++) items.push(page.items[j]);
457
+ } else if (page && typeof page === 'object' && Array.isArray(page.data)) {
458
+ for (let j = 0; j < page.data.length; j++) items.push(page.data[j]);
459
+ } else {
460
+ items.push(page);
461
+ }
462
+ }
463
+ return items;
464
+ });
465
+
466
+ const hasNextPage = createMemo(() => {
467
+ const p = pages();
468
+ if (p.length === 0) return true; // not yet fetched
469
+ return getNextPageParam(p[p.length - 1], p) !== undefined;
470
+ });
471
+
472
+ /** @type {AbortController|null} */
473
+ let ac = null;
474
+
475
+ /**
476
+ * Fetch a single page with retry.
477
+ * @param {string} k
478
+ * @param {*} pageParam
479
+ * @param {AbortSignal} signal
480
+ * @returns {Promise<T>}
481
+ */
482
+ async function fetchPage(k, pageParam, signal) {
483
+ let lastErr;
484
+ for (let attempt = 0; attempt <= retry; attempt++) {
485
+ if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
486
+ try {
487
+ if (middlewareChain.length > 0) {
488
+ const ctx = { url: k, method: 'GET', headers: {}, body: undefined };
489
+ return await runMiddleware(ctx, () => fetcher({ key: k, pageParam, signal }));
490
+ }
491
+ return await fetcher({ key: k, pageParam, signal });
492
+ } catch (err) {
493
+ if (signal.aborted || (err && err.name === 'AbortError')) throw err;
494
+ lastErr = err instanceof Error ? err : new Error(String(err));
495
+ if (attempt < retry) {
496
+ await new Promise(r => setTimeout(r, Math.min(1000 * Math.pow(2, attempt), 30000)));
497
+ }
498
+ }
499
+ }
500
+ throw lastErr;
501
+ }
502
+
503
+ /**
504
+ * Fetch the first page (or refetch all pages).
505
+ * @returns {Promise<void>}
506
+ */
507
+ async function fetchInitial() {
508
+ const k = untrack(resolveKey);
509
+ if (!k) return;
510
+ if (ac) ac.abort();
511
+ ac = new AbortController();
512
+ const signal = ac.signal;
513
+
514
+ setStatus('loading');
515
+ setIsFetchingNextPage(false);
516
+
517
+ const promise = (async () => {
518
+ try {
519
+ const firstPage = await fetchPage(k, undefined, signal);
520
+ if (signal.aborted) return;
521
+ batch(() => {
522
+ setPages([firstPage]);
523
+ setStatus('success');
524
+ setError(null);
525
+ });
526
+ // Update cache entry
527
+ const entry = getEntry(k);
528
+ entry.data = [firstPage];
529
+ entry.timestamp = Date.now();
530
+ } catch (err) {
531
+ if (signal.aborted || (err && err.name === 'AbortError')) return;
532
+ batch(() => {
533
+ setError(err instanceof Error ? err : new Error(String(err)));
534
+ setStatus('error');
535
+ });
536
+ }
537
+ })();
538
+
539
+ _pendingQueries.add(promise);
540
+ try { await promise; } finally { _pendingQueries.delete(promise); }
541
+ }
542
+
543
+ /**
544
+ * Fetch the next page.
545
+ * @returns {Promise<void>}
546
+ */
547
+ async function fetchNextPage() {
548
+ const k = untrack(resolveKey);
549
+ if (!k) return;
550
+ const currentPages = untrack(pages);
551
+ if (currentPages.length === 0) return fetchInitial();
552
+
553
+ const nextParam = getNextPageParam(currentPages[currentPages.length - 1], currentPages);
554
+ if (nextParam === undefined) return;
555
+
556
+ if (ac) ac.abort();
557
+ ac = new AbortController();
558
+ const signal = ac.signal;
559
+
560
+ setIsFetchingNextPage(true);
561
+
562
+ try {
563
+ const nextPage = await fetchPage(k, nextParam, signal);
564
+ if (signal.aborted) return;
565
+ batch(() => {
566
+ setPages(prev => [...prev, nextPage]);
567
+ setIsFetchingNextPage(false);
568
+ setError(null);
569
+ });
570
+ const entry = getEntry(k);
571
+ entry.data = untrack(pages);
572
+ entry.timestamp = Date.now();
573
+ } catch (err) {
574
+ if (signal.aborted || (err && err.name === 'AbortError')) return;
575
+ batch(() => {
576
+ setError(err instanceof Error ? err : new Error(String(err)));
577
+ setIsFetchingNextPage(false);
578
+ });
579
+ }
580
+ }
581
+
582
+ // Initial fetch driven by reactive key + enabled
583
+ createEffect(() => {
584
+ const isEnabled = enabled ? enabled() : true;
585
+ const k = resolveKey();
586
+ if (!isEnabled || !k) {
587
+ if (untrack(status) !== 'idle') setStatus('idle');
588
+ return;
589
+ }
590
+
591
+ const entry = cache.get(k);
592
+ if (entry && entry.data !== undefined && Date.now() - entry.timestamp <= staleTime) {
593
+ batch(() => {
594
+ setPages(entry.data);
595
+ setStatus('success');
596
+ setError(null);
597
+ });
598
+ } else {
599
+ fetchInitial();
600
+ }
601
+
602
+ return () => {
603
+ if (ac) { ac.abort(); ac = null; }
604
+ const e = cache.get(k);
605
+ if (e && e.subscribers.size === 0) scheduleGC(k, cacheTime);
606
+ };
607
+ });
608
+
609
+ return { pages, allItems, hasNextPage, fetchNextPage, isFetchingNextPage, refetch: fetchInitial };
610
+ }
611
+
612
+ // ─── createMutation ─────────────────────────────────────────────
613
+
614
+ /**
615
+ * Mutation primitive for create / update / delete operations.
616
+ *
617
+ * @template TData, TVariables
618
+ * @param {(variables: TVariables) => Promise<TData>} mutationFn
619
+ * @param {Object} [options]
620
+ * @param {(variables: TVariables) => *} [options.onMutate] — optimistic update; return rollback context
621
+ * @param {(data: TData, variables: TVariables, ctx: *) => void} [options.onSuccess]
622
+ * @param {(error: Error, variables: TVariables, ctx: *) => void} [options.onError]
623
+ * @param {(data: TData|undefined, error: Error|undefined, variables: TVariables, ctx: *) => void} [options.onSettled]
624
+ * @returns {{ mutate: (variables: TVariables) => void, mutateAsync: (variables: TVariables) => Promise<TData>, isLoading: () => boolean, error: () => Error|null, data: () => TData|undefined, reset: () => void }}
625
+ */
626
+ export function createMutation(mutationFn, options = {}) {
627
+ const { onMutate, onSuccess, onError, onSettled } = options;
628
+
629
+ const [data, setData] = createSignal(/** @type {TData|undefined} */ (undefined));
630
+ const [error, setError] = createSignal(/** @type {Error|null} */ (null));
631
+ const [isLoading, setIsLoading] = createSignal(false);
632
+
633
+ /**
634
+ * Execute the mutation and return the result.
635
+ * @param {TVariables} variables
636
+ * @returns {Promise<TData>}
637
+ */
638
+ async function mutateAsync(variables) {
639
+ let context;
640
+ batch(() => {
641
+ setIsLoading(true);
642
+ setError(null);
643
+ });
644
+
645
+ try {
646
+ if (onMutate) context = await onMutate(variables);
647
+ } catch (_) {
648
+ // onMutate failure is non-fatal to the mutation itself
649
+ }
650
+
651
+ try {
652
+ /** @type {*} */
653
+ let result;
654
+ if (middlewareChain.length > 0) {
655
+ const ctx = { url: '', method: 'POST', headers: {}, body: variables };
656
+ result = await runMiddleware(ctx, () => mutationFn(variables));
657
+ } else {
658
+ result = await mutationFn(variables);
659
+ }
660
+ batch(() => {
661
+ setData(() => result);
662
+ setIsLoading(false);
663
+ setError(null);
664
+ });
665
+ if (onSuccess) {
666
+ try { onSuccess(result, variables, context); } catch (_) {}
667
+ }
668
+ if (onSettled) {
669
+ try { onSettled(result, undefined, variables, context); } catch (_) {}
670
+ }
671
+ return result;
672
+ } catch (err) {
673
+ const error = err instanceof Error ? err : new Error(String(err));
674
+ batch(() => {
675
+ setError(error);
676
+ setIsLoading(false);
677
+ });
678
+ if (onError) {
679
+ try {
680
+ onError(error, variables, context);
681
+ } catch (_) {}
682
+ }
683
+ if (onSettled) {
684
+ try { onSettled(undefined, error, variables, context); } catch (_) {}
685
+ }
686
+ throw error;
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Fire-and-forget mutation.
692
+ * @param {TVariables} variables
693
+ */
694
+ function mutate(variables) {
695
+ mutateAsync(variables).catch(() => {});
696
+ }
697
+
698
+ /** Reset mutation state to initial. */
699
+ function reset() {
700
+ batch(() => {
701
+ setData(() => undefined);
702
+ setError(null);
703
+ setIsLoading(false);
704
+ });
705
+ }
706
+
707
+ return { mutate, mutateAsync, isLoading, error, data, reset };
708
+ }
709
+
710
+ // ─── queryClient (singleton) ────────────────────────────────────
711
+
712
+ /**
713
+ * Global query client for imperative cache operations.
714
+ * @type {{ use: (middleware: Function) => () => void, invalidate: (keyPrefix: string) => void, invalidateQueries: (keyPattern: string) => void, prefetch: (key: string, fetcher: Function) => Promise<void>, setCache: (key: string, data: *) => void, getCache: (key: string) => *, clear: () => void }}
715
+ */
716
+ export const queryClient = {
717
+ /**
718
+ * Add a request middleware to the chain.
719
+ * Middleware signature: `async (ctx, next) => { ... }`
720
+ * ctx has: `{ url, method, headers, body }`.
721
+ * Middleware runs in order before fetch; response passes back in reverse.
722
+ * Returns an unsubscribe function to remove the middleware.
723
+ * @param {(ctx: MiddlewareContext, next: () => Promise<*>) => Promise<*>} middleware
724
+ * @returns {() => void}
725
+ */
726
+ use(middleware) {
727
+ middlewareChain.push(middleware);
728
+ return () => {
729
+ const idx = middlewareChain.indexOf(middleware);
730
+ if (idx !== -1) middlewareChain.splice(idx, 1);
731
+ };
732
+ },
733
+
734
+ /**
735
+ * Invalidate all queries whose key matches a glob pattern.
736
+ * Supports `*` (single segment) and `**` (any depth).
737
+ * Example: 'user.*' matches 'user.profile', 'user.settings'.
738
+ * Invalidated queries are marked stale and refetched if active.
739
+ * @param {string} keyPattern — glob pattern to match cache keys
740
+ */
741
+ invalidateQueries(keyPattern) {
742
+ const regex = globToRegex(keyPattern);
743
+ for (const [k, entry] of cache) {
744
+ if (regex.test(k)) {
745
+ entry.timestamp = 0; // mark stale
746
+ // Trigger refetch for active subscribers
747
+ if (entry.subscribers.size > 0) {
748
+ for (const refetchFn of entry.subscribers) {
749
+ try { refetchFn(); } catch (_) {}
750
+ }
751
+ }
752
+ }
753
+ }
754
+ },
755
+ /**
756
+ * Mark all queries whose key starts with `keyPrefix` as stale.
757
+ * Active queries (those with subscribers) are refetched immediately.
758
+ * @param {string} keyPrefix
759
+ */
760
+ invalidate(keyPrefix) {
761
+ for (const [k, entry] of cache) {
762
+ if (k.startsWith(keyPrefix)) {
763
+ entry.timestamp = 0; // mark stale
764
+ // Trigger refetch for active subscribers
765
+ if (entry.subscribers.size > 0) {
766
+ for (const refetchFn of entry.subscribers) {
767
+ try { refetchFn(); } catch (_) {}
768
+ }
769
+ }
770
+ }
771
+ }
772
+ },
773
+
774
+ /**
775
+ * Warm the cache for a key without an active query.
776
+ * @param {string} key
777
+ * @param {(ctx: { key: string, signal: AbortSignal }) => Promise<*>} fetcher
778
+ * @returns {Promise<void>}
779
+ */
780
+ async prefetch(key, fetcher) {
781
+ const entry = getEntry(key);
782
+ if (entry.fetchPromise) {
783
+ await entry.fetchPromise;
784
+ return;
785
+ }
786
+ const ac = new AbortController();
787
+ entry.abortController = ac;
788
+ const promise = (async () => {
789
+ try {
790
+ const result = await fetcher({ key, signal: ac.signal });
791
+ if (!ac.signal.aborted) {
792
+ entry.data = result;
793
+ entry.timestamp = Date.now();
794
+ }
795
+ } finally {
796
+ if (entry.abortController === ac) entry.abortController = null;
797
+ }
798
+ })();
799
+ entry.fetchPromise = promise;
800
+ try { await promise; } finally {
801
+ if (entry.fetchPromise === promise) entry.fetchPromise = null;
802
+ }
803
+ },
804
+
805
+ /**
806
+ * Manually write data into the cache.
807
+ * @param {string} key
808
+ * @param {*} data
809
+ */
810
+ setCache(key, data) {
811
+ const entry = getEntry(key);
812
+ entry.data = data;
813
+ entry.timestamp = Date.now();
814
+ },
815
+
816
+ /**
817
+ * Read cached data for a key.
818
+ * @param {string} key
819
+ * @returns {*} — cached data or undefined
820
+ */
821
+ getCache(key) {
822
+ const entry = cache.get(key);
823
+ return entry ? entry.data : undefined;
824
+ },
825
+
826
+ /**
827
+ * Clear all cache entries, abort in-flight requests, cancel GC timers.
828
+ */
829
+ clear() {
830
+ for (const [, entry] of cache) {
831
+ if (entry.abortController) entry.abortController.abort();
832
+ }
833
+ for (const [, timerId] of gcTimers) {
834
+ clearTimeout(timerId);
835
+ }
836
+ cache.clear();
837
+ gcTimers.clear();
838
+ }
839
+ };