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.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- 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
|
+
};
|