@jini-ai/ui 0.4.6 → 0.4.7
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/README.md +31 -0
- package/dist/features/interactive-ui/interactive-ui.css +1 -1
- package/dist/features/interactive-ui/providers/native/data-table.js +8 -1
- package/dist/features/interactive-ui/providers/native/data-table.manifest.d.ts +3 -5
- package/dist/features/interactive-ui/providers/native/data-table.manifest.js +5 -7
- package/dist/features/interactive-ui/providers/recharts/bar-chart.js +8 -3
- package/dist/features/interactive-ui/providers/recharts/chart-config.d.ts +40 -0
- package/dist/features/interactive-ui/providers/recharts/chart-config.js +32 -0
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +18 -12
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +6 -11
- package/dist/features/interactive-ui/providers/recharts/chart.d.ts +31 -0
- package/dist/features/interactive-ui/providers/recharts/chart.js +34 -0
- package/dist/features/interactive-ui/providers/recharts/line-chart.js +10 -3
- package/dist/features/interactive-ui/providers/recharts/pie-chart.js +8 -3
- package/dist/features/interactive-ui/providers/shadcn/data-table.d.ts +5 -1
- package/dist/features/interactive-ui/providers/shadcn/data-table.js +26 -2
- package/dist/features/interactive-ui/providers/shadcn/data-table.manifest.d.ts +8 -1
- package/dist/features/interactive-ui/providers/shadcn/data-table.manifest.js +6 -3
- package/dist/features/interactive-ui/table-data.d.ts +25 -0
- package/dist/features/interactive-ui/table-data.js +36 -0
- package/dist/features/mcp-ui/sandbox-proxy-link-forwarder.d.ts +26 -0
- package/dist/features/mcp-ui/sandbox-proxy-link-forwarder.js +40 -0
- package/dist/features/mcp-ui/sandbox-proxy.d.ts +5 -1
- package/dist/features/mcp-ui/sandbox-proxy.js +11 -0
- package/dist/features/mcp-ui/surfaces/bridge.js +6 -2
- package/dist/features/panel-kit/fetch-query/adapter.react.js +3 -1
- package/dist/features/panel-kit/fetch-query/adapter.tanstack.d.ts +11 -0
- package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +190 -0
- package/dist/features/panel-kit/fetch-query/binding.react.d.ts +17 -0
- package/dist/features/panel-kit/fetch-query/binding.react.js +24 -0
- package/dist/features/panel-kit/fetch-query/cache.d.ts +2 -0
- package/dist/features/panel-kit/fetch-query/cache.js +2 -1
- package/dist/features/panel-kit/fetch-query/index.d.ts +2 -1
- package/dist/features/panel-kit/fetch-query/index.js +2 -1
- package/dist/features/panel-kit/fetch-query/tanstack.d.ts +6 -0
- package/dist/features/panel-kit/fetch-query/tanstack.js +4 -0
- package/dist/features/panel-kit/fetch-query/types.d.ts +2 -0
- package/package.json +16 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
/**
|
|
3
|
-
* Same wire shape as `native.data-table` (see that manifest)
|
|
3
|
+
* Same required wire shape as `native.data-table` (see that manifest), plus optional local
|
|
4
|
+
* sorting/paging settings — shared capabilities, so
|
|
4
5
|
* `resolveByCapability('data-table')` returns both, proving the fallback-chain mechanic across
|
|
5
6
|
* two real providers. `.passthrough()` for the same reason as that manifest: an A2UI-embedded
|
|
6
7
|
* instance may carry an `action` prop this schema doesn't itself validate.
|
|
@@ -9,13 +10,15 @@ export const shadcnDataTablePropsSchema = z
|
|
|
9
10
|
.object({
|
|
10
11
|
columns: z.array(z.object({ key: z.string(), label: z.string() }).strict()).min(1),
|
|
11
12
|
rows: z.array(z.record(z.unknown())),
|
|
13
|
+
sortable: z.boolean().optional(),
|
|
14
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
12
15
|
})
|
|
13
16
|
.passthrough();
|
|
14
17
|
export const shadcnDataTableManifest = {
|
|
15
18
|
id: 'shadcn.data-table',
|
|
16
19
|
provider: 'shadcn',
|
|
17
|
-
capabilities: ['data-table', 'table', 'tabular-data'],
|
|
20
|
+
capabilities: ['data-table', 'table', 'tabular-data', 'spreadsheet', 'list-of-records'],
|
|
18
21
|
propsSchema: shadcnDataTablePropsSchema,
|
|
19
|
-
description: '
|
|
22
|
+
description: 'Preferred styled table for tabular data, spreadsheets and lists of records: columns + rows, optional row click. Rounded border, sticky muted headers, row hover, numeric alignment and empty state. Client-side column sorting (sortable defaults to true); optional pageSize (1–100) enables paging. Load @jini-ai/ui/interactive-ui.css in the renderer host.',
|
|
20
23
|
};
|
|
21
24
|
//# sourceMappingURL=data-table.manifest.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface TableData {
|
|
2
|
+
readonly columns: readonly {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
}[];
|
|
5
|
+
readonly rows: readonly Record<string, unknown>[];
|
|
6
|
+
}
|
|
7
|
+
export interface TableSort {
|
|
8
|
+
readonly key: string;
|
|
9
|
+
readonly direction: 'ascending' | 'descending';
|
|
10
|
+
}
|
|
11
|
+
/** Infer numeric columns once for both providers; empty cells do not turn a numeric column into text.
|
|
12
|
+
* @complexity O(c × r), c columns and r rows; no per-cell rescans during rendering. */
|
|
13
|
+
export declare function getNumericColumnKeys({ columns, rows }: TableData, _options?: {}): ReadonlySet<string>;
|
|
14
|
+
/** Sort a copy while preserving source indexes for host actions; missing values stay last in both directions.
|
|
15
|
+
* @complexity O(r log r) time and O(r) space for r rows; the caller memoizes the result. */
|
|
16
|
+
export declare function sortTableRows({ rows, sort, numericKeys }: {
|
|
17
|
+
readonly rows: TableData['rows'];
|
|
18
|
+
readonly sort: TableSort | null;
|
|
19
|
+
readonly numericKeys: ReadonlySet<string>;
|
|
20
|
+
}, _options?: {}): {
|
|
21
|
+
row: Record<string, unknown>;
|
|
22
|
+
index: number;
|
|
23
|
+
}[];
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=table-data.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function isNumericValue(value) {
|
|
2
|
+
if (typeof value === 'number')
|
|
3
|
+
return Number.isFinite(value);
|
|
4
|
+
return typeof value === 'string' && /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(value.trim())
|
|
5
|
+
&& Number.isFinite(Number(value));
|
|
6
|
+
}
|
|
7
|
+
/** Infer numeric columns once for both providers; empty cells do not turn a numeric column into text.
|
|
8
|
+
* @complexity O(c × r), c columns and r rows; no per-cell rescans during rendering. */
|
|
9
|
+
export function getNumericColumnKeys({ columns, rows }, _options = {}) {
|
|
10
|
+
return new Set(columns.filter(({ key }) => {
|
|
11
|
+
const values = rows.map((row) => row[key]).filter((value) => value !== null && value !== undefined && value !== '');
|
|
12
|
+
return values.length > 0 && values.every(isNumericValue);
|
|
13
|
+
}).map(({ key }) => key));
|
|
14
|
+
}
|
|
15
|
+
/** Sort a copy while preserving source indexes for host actions; missing values stay last in both directions.
|
|
16
|
+
* @complexity O(r log r) time and O(r) space for r rows; the caller memoizes the result. */
|
|
17
|
+
export function sortTableRows({ rows, sort, numericKeys }, _options = {}) {
|
|
18
|
+
const indexed = rows.map((row, index) => ({ row, index }));
|
|
19
|
+
if (!sort)
|
|
20
|
+
return indexed;
|
|
21
|
+
const { key, direction } = sort;
|
|
22
|
+
const multiplier = direction === 'ascending' ? 1 : -1;
|
|
23
|
+
return indexed.sort((a, b) => {
|
|
24
|
+
const left = a.row[key];
|
|
25
|
+
const right = b.row[key];
|
|
26
|
+
const leftEmpty = left === null || left === undefined || left === '';
|
|
27
|
+
const rightEmpty = right === null || right === undefined || right === '';
|
|
28
|
+
if (leftEmpty || rightEmpty)
|
|
29
|
+
return Number(leftEmpty) - Number(rightEmpty);
|
|
30
|
+
const compared = numericKeys.has(key)
|
|
31
|
+
? Number(left) - Number(right)
|
|
32
|
+
: String(left).localeCompare(String(right));
|
|
33
|
+
return compared * multiplier;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=table-data.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module features/mcp-ui/sandbox-proxy-link-forwarder
|
|
3
|
+
*
|
|
4
|
+
* Page-script source the sandbox proxy (`sandbox-proxy.ts`) embeds so a View's plain `<a href>`
|
|
5
|
+
* opens in a new tab. Inside the proxy's frame a link is useless on its own: `AppFrame`'s hardcoded
|
|
6
|
+
* sandbox has neither `allow-popups` nor `allow-top-navigation`, so a same-tab link navigates the
|
|
7
|
+
* card's own frame away from the View and a `target="_blank"` link is silently blocked. The one
|
|
8
|
+
* channel a View has to open anything is the `ui/open-link` request, answered by the Host's
|
|
9
|
+
* `onOpenLink` — so every qualifying link click becomes that request instead.
|
|
10
|
+
*
|
|
11
|
+
* Window, bubble phase, and skipped once `defaultPrevented`: it runs after every listener the View
|
|
12
|
+
* itself registered, so a View that already handles its own link (calling `openLink` itself and
|
|
13
|
+
* cancelling the click) is not opened twice.
|
|
14
|
+
*
|
|
15
|
+
* The request id is a string the View's own JSON-RPC ids (numbers) never use, so the Host's reply
|
|
16
|
+
* cannot resolve one of the View's pending requests. `link.href` is the resolved URL when the
|
|
17
|
+
* proxy has a real base; under the opaque `data:` proxy a relative href stays as written and the
|
|
18
|
+
* Host decides how to resolve it.
|
|
19
|
+
*
|
|
20
|
+
* Not re-exported from the package (`index.ts` re-exports `sandbox-proxy.ts` only); the tests
|
|
21
|
+
* evaluate this exact source.
|
|
22
|
+
*
|
|
23
|
+
* Expects `host` (the embedding window) and `hostOrigin` in scope — the proxy script's own vars.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PROXY_LINK_FORWARDER_JS = "var openLinkSeq = 0;\n function forwardLinkClick(event) {\n if (event.defaultPrevented || event.button !== 0) return;\n var target = event.target;\n var link = target && target.closest ? target.closest(\"a[href]\") : null;\n if (!link || link.hasAttribute(\"download\")) return;\n var href = link.getAttribute(\"href\");\n if (!href || href.charAt(0) === \"#\" || /^\\s*javascript:/i.test(href)) return;\n event.preventDefault();\n openLinkSeq += 1;\n host.postMessage(\n { jsonrpc: \"2.0\", id: \"jini-proxy-open-link-\" + openLinkSeq, method: \"ui/open-link\", params: { url: link.href } },\n hostOrigin\n );\n }";
|
|
26
|
+
//# sourceMappingURL=sandbox-proxy-link-forwarder.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module features/mcp-ui/sandbox-proxy-link-forwarder
|
|
3
|
+
*
|
|
4
|
+
* Page-script source the sandbox proxy (`sandbox-proxy.ts`) embeds so a View's plain `<a href>`
|
|
5
|
+
* opens in a new tab. Inside the proxy's frame a link is useless on its own: `AppFrame`'s hardcoded
|
|
6
|
+
* sandbox has neither `allow-popups` nor `allow-top-navigation`, so a same-tab link navigates the
|
|
7
|
+
* card's own frame away from the View and a `target="_blank"` link is silently blocked. The one
|
|
8
|
+
* channel a View has to open anything is the `ui/open-link` request, answered by the Host's
|
|
9
|
+
* `onOpenLink` — so every qualifying link click becomes that request instead.
|
|
10
|
+
*
|
|
11
|
+
* Window, bubble phase, and skipped once `defaultPrevented`: it runs after every listener the View
|
|
12
|
+
* itself registered, so a View that already handles its own link (calling `openLink` itself and
|
|
13
|
+
* cancelling the click) is not opened twice.
|
|
14
|
+
*
|
|
15
|
+
* The request id is a string the View's own JSON-RPC ids (numbers) never use, so the Host's reply
|
|
16
|
+
* cannot resolve one of the View's pending requests. `link.href` is the resolved URL when the
|
|
17
|
+
* proxy has a real base; under the opaque `data:` proxy a relative href stays as written and the
|
|
18
|
+
* Host decides how to resolve it.
|
|
19
|
+
*
|
|
20
|
+
* Not re-exported from the package (`index.ts` re-exports `sandbox-proxy.ts` only); the tests
|
|
21
|
+
* evaluate this exact source.
|
|
22
|
+
*
|
|
23
|
+
* Expects `host` (the embedding window) and `hostOrigin` in scope — the proxy script's own vars.
|
|
24
|
+
*/
|
|
25
|
+
export const PROXY_LINK_FORWARDER_JS = `var openLinkSeq = 0;
|
|
26
|
+
function forwardLinkClick(event) {
|
|
27
|
+
if (event.defaultPrevented || event.button !== 0) return;
|
|
28
|
+
var target = event.target;
|
|
29
|
+
var link = target && target.closest ? target.closest("a[href]") : null;
|
|
30
|
+
if (!link || link.hasAttribute("download")) return;
|
|
31
|
+
var href = link.getAttribute("href");
|
|
32
|
+
if (!href || href.charAt(0) === "#" || /^\\s*javascript:/i.test(href)) return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
openLinkSeq += 1;
|
|
35
|
+
host.postMessage(
|
|
36
|
+
{ jsonrpc: "2.0", id: "jini-proxy-open-link-" + openLinkSeq, method: "ui/open-link", params: { url: link.href } },
|
|
37
|
+
hostOrigin
|
|
38
|
+
);
|
|
39
|
+
}`;
|
|
40
|
+
//# sourceMappingURL=sandbox-proxy-link-forwarder.js.map
|
|
@@ -127,8 +127,12 @@
|
|
|
127
127
|
* survives `document.open()` and would then apply to the guest HTML written in below, silently
|
|
128
128
|
* breaking any UIResource whose HTML this package did not build. Surfaces built by
|
|
129
129
|
* `surfaces/document.ts` already carry their own `SURFACE_CSP` meta tag for exactly that job.
|
|
130
|
+
*
|
|
131
|
+
* Once the guest HTML is written, the page also forwards the View's link clicks to the Host as
|
|
132
|
+
* `ui/open-link` (`sandbox-proxy-link-forwarder.ts`) — registered after `document.close()` because
|
|
133
|
+
* `document.open()` erases every listener registered before it.
|
|
130
134
|
*/
|
|
131
|
-
export declare const SANDBOX_PROXY_HTML = "<!doctype html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>MCP-UI sandbox proxy</title>\n<style>html, body { margin: 0; padding: 0; width: 100%; height: 100%; }</style>\n</head>\n<body>\n<script>\n(function () {\n \"use strict\";\n var host = window.parent;\n var hostOrigin = window.location.origin;\n if (host === window) return;\n\n function isFromHost(event) {\n return event.source === host && event.origin === hostOrigin;\n }\n\n window.addEventListener(\"message\", function (event) {\n if (!isFromHost(event)) return;\n var data = event.data;\n if (!data || typeof data !== \"object\") return;\n if (data.method !== \"ui/notifications/sandbox-resource-ready\") return;\n var html = data.params && data.params.html;\n if (typeof html !== \"string\") return;\n document.open();\n document.write(html);\n document.close();\n });\n\n host.postMessage({ method: \"ui/notifications/sandbox-proxy-ready\", params: {} }, hostOrigin);\n}());\n</script>\n</body>\n</html>";
|
|
135
|
+
export declare const SANDBOX_PROXY_HTML = "<!doctype html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>MCP-UI sandbox proxy</title>\n<style>html, body { margin: 0; padding: 0; width: 100%; height: 100%; }</style>\n</head>\n<body>\n<script>\n(function () {\n \"use strict\";\n var host = window.parent;\n var hostOrigin = window.location.origin;\n if (host === window) return;\n\n function isFromHost(event) {\n return event.source === host && event.origin === hostOrigin;\n }\n\n var openLinkSeq = 0;\n function forwardLinkClick(event) {\n if (event.defaultPrevented || event.button !== 0) return;\n var target = event.target;\n var link = target && target.closest ? target.closest(\"a[href]\") : null;\n if (!link || link.hasAttribute(\"download\")) return;\n var href = link.getAttribute(\"href\");\n if (!href || href.charAt(0) === \"#\" || /^\\s*javascript:/i.test(href)) return;\n event.preventDefault();\n openLinkSeq += 1;\n host.postMessage(\n { jsonrpc: \"2.0\", id: \"jini-proxy-open-link-\" + openLinkSeq, method: \"ui/open-link\", params: { url: link.href } },\n hostOrigin\n );\n }\n\n window.addEventListener(\"message\", function (event) {\n if (!isFromHost(event)) return;\n var data = event.data;\n if (!data || typeof data !== \"object\") return;\n if (data.method !== \"ui/notifications/sandbox-resource-ready\") return;\n var html = data.params && data.params.html;\n if (typeof html !== \"string\") return;\n document.open();\n document.write(html);\n document.close();\n window.addEventListener(\"click\", forwardLinkClick);\n });\n\n host.postMessage({ method: \"ui/notifications/sandbox-proxy-ready\", params: {} }, hostOrigin);\n}());\n</script>\n</body>\n</html>";
|
|
132
136
|
/**
|
|
133
137
|
* Same script as {@link SANDBOX_PROXY_HTML}, with one load-bearing difference: `hostOrigin` is the
|
|
134
138
|
* caller-supplied `hostOrigin` argument, baked in as a literal, instead of read from
|
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
* hardcodes any particular host — a consumer's origin is discovered at runtime from the page's own
|
|
110
110
|
* URL — and neither touches the single-hop `document.write` shape the section above depends on.
|
|
111
111
|
*/
|
|
112
|
+
import { PROXY_LINK_FORWARDER_JS } from './sandbox-proxy-link-forwarder.js';
|
|
112
113
|
/**
|
|
113
114
|
* The sandbox proxy page's complete, self-contained source — see this module's own doc for the
|
|
114
115
|
* protocol it implements and the security tradeoff serving it makes.
|
|
@@ -127,6 +128,10 @@
|
|
|
127
128
|
* survives `document.open()` and would then apply to the guest HTML written in below, silently
|
|
128
129
|
* breaking any UIResource whose HTML this package did not build. Surfaces built by
|
|
129
130
|
* `surfaces/document.ts` already carry their own `SURFACE_CSP` meta tag for exactly that job.
|
|
131
|
+
*
|
|
132
|
+
* Once the guest HTML is written, the page also forwards the View's link clicks to the Host as
|
|
133
|
+
* `ui/open-link` (`sandbox-proxy-link-forwarder.ts`) — registered after `document.close()` because
|
|
134
|
+
* `document.open()` erases every listener registered before it.
|
|
130
135
|
*/
|
|
131
136
|
export const SANDBOX_PROXY_HTML = `<!doctype html>
|
|
132
137
|
<html>
|
|
@@ -147,6 +152,8 @@ export const SANDBOX_PROXY_HTML = `<!doctype html>
|
|
|
147
152
|
return event.source === host && event.origin === hostOrigin;
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
${PROXY_LINK_FORWARDER_JS}
|
|
156
|
+
|
|
150
157
|
window.addEventListener("message", function (event) {
|
|
151
158
|
if (!isFromHost(event)) return;
|
|
152
159
|
var data = event.data;
|
|
@@ -157,6 +164,7 @@ export const SANDBOX_PROXY_HTML = `<!doctype html>
|
|
|
157
164
|
document.open();
|
|
158
165
|
document.write(html);
|
|
159
166
|
document.close();
|
|
167
|
+
window.addEventListener("click", forwardLinkClick);
|
|
160
168
|
});
|
|
161
169
|
|
|
162
170
|
host.postMessage({ method: "ui/notifications/sandbox-proxy-ready", params: {} }, hostOrigin);
|
|
@@ -220,6 +228,8 @@ export function buildIsolatedSandboxProxyHtml(hostOrigin) {
|
|
|
220
228
|
return event.source === host && event.origin === hostOrigin;
|
|
221
229
|
}
|
|
222
230
|
|
|
231
|
+
${PROXY_LINK_FORWARDER_JS}
|
|
232
|
+
|
|
223
233
|
window.addEventListener("message", function (event) {
|
|
224
234
|
if (!isFromHost(event)) return;
|
|
225
235
|
var data = event.data;
|
|
@@ -230,6 +240,7 @@ export function buildIsolatedSandboxProxyHtml(hostOrigin) {
|
|
|
230
240
|
document.open();
|
|
231
241
|
document.write(html);
|
|
232
242
|
document.close();
|
|
243
|
+
window.addEventListener("click", forwardLinkClick);
|
|
233
244
|
});
|
|
234
245
|
|
|
235
246
|
host.postMessage({ method: "ui/notifications/sandbox-proxy-ready", params: {} }, hostOrigin);
|
|
@@ -133,9 +133,13 @@ export function renderBridgeScript(spec) {
|
|
|
133
133
|
return requestWhenReady("ui/open-link", { url: url });
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Height only, never width: @mcp-ui/client's AppFrame writes a reported width onto the iframe as a
|
|
137
|
+
// fixed px style. A surface is fluid, so its scrollWidth is just the frame's width at that moment;
|
|
138
|
+
// reported while mounting in a hidden or narrow pane it froze the frame at min-content width for
|
|
139
|
+
// good (owner bug 2026-10-08: an ask-choice card stuck at 192px after a reload). Leaving width out
|
|
140
|
+
// keeps the frame at the host's width: 100%, and the ResizeObserver below re-reports height on resize.
|
|
136
141
|
function reportSize() {
|
|
137
|
-
|
|
138
|
-
notify("ui/notifications/size-changed", { width: root.scrollWidth, height: root.scrollHeight });
|
|
142
|
+
notify("ui/notifications/size-changed", { height: document.documentElement.scrollHeight });
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
window.addEventListener("message", function (event) {
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
3
|
import { DEFAULT_STALE_TIME, FetchQueryCache, toQueryError } from './cache.js';
|
|
4
4
|
import { browserFetchQueryEnvironment } from './environment.browser.js';
|
|
5
|
+
import { FetchQueryBinding } from './binding.react.js';
|
|
5
6
|
const CacheContext = createContext(null);
|
|
6
7
|
/** Isolated cache scope; no external provider or query-library singleton is needed. */
|
|
7
8
|
export function FetchQueryProvider({ children, environment = browserFetchQueryEnvironment }) {
|
|
@@ -12,7 +13,7 @@ export function FetchQueryProvider({ children, environment = browserFetchQueryEn
|
|
|
12
13
|
cache.activate();
|
|
13
14
|
return () => cache.dispose();
|
|
14
15
|
}, [cache]);
|
|
15
|
-
return _jsx(CacheContext.Provider, { value: cache, children: children });
|
|
16
|
+
return _jsx(CacheContext.Provider, { value: cache, children: _jsx(FetchQueryBinding, { adapter: adapter, children: children }) });
|
|
16
17
|
}
|
|
17
18
|
function useCache() {
|
|
18
19
|
const cache = useContext(CacheContext);
|
|
@@ -119,4 +120,5 @@ export function useInvalidate() {
|
|
|
119
120
|
const cache = useCache();
|
|
120
121
|
return useCallback(({ key }) => cache.invalidate({ prefix: key }), [cache]);
|
|
121
122
|
}
|
|
123
|
+
const adapter = { useFetchQuery, useFetchMutation, useCachedLoader, useInvalidate };
|
|
122
124
|
//# sourceMappingURL=adapter.react.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Optional TanStack translation layer. No library types escape the fetch-query contract. */
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import { type FetchQueryEnvironmentPort } from './cache.js';
|
|
4
|
+
/** Provider-owned TanStack cache and binding, compatible with hooks from the default entry.
|
|
5
|
+
* @example <FetchQueryProvider><ResourceScreen /></FetchQueryProvider>
|
|
6
|
+
* @param props Children and optional host connectivity/focus port. */
|
|
7
|
+
export declare function FetchQueryProvider({ children, environment }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
environment?: FetchQueryEnvironmentPort;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=adapter.tanstack.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Optional TanStack translation layer. No library types escape the fetch-query contract. */
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
4
|
+
import { isCancelledError, QueryClient, QueryClientProvider, useMutation, useQuery } from '@tanstack/react-query';
|
|
5
|
+
import { DEFAULT_STALE_TIME, FetchQueryCache, IDLE_RETENTION, toQueryError } from './cache.js';
|
|
6
|
+
import { browserFetchQueryEnvironment } from './environment.browser.js';
|
|
7
|
+
import { resolveFetchQueryError, resolveFetchQueryStatus } from './adapter.react.js';
|
|
8
|
+
import { FetchQueryBinding } from './binding.react.js';
|
|
9
|
+
const ScopeContext = createContext(null);
|
|
10
|
+
/** Match built-in defaults. Failed authorization/validation must surface immediately;
|
|
11
|
+
* automatic retries would hide the server's first useful error behind repeated requests. */
|
|
12
|
+
function createScope({ environment }) {
|
|
13
|
+
return {
|
|
14
|
+
client: new QueryClient({ defaultOptions: {
|
|
15
|
+
queries: { staleTime: DEFAULT_STALE_TIME, retry: false, refetchOnWindowFocus: false, refetchOnReconnect: false, networkMode: 'always' },
|
|
16
|
+
mutations: { retry: false, networkMode: 'always' },
|
|
17
|
+
} }),
|
|
18
|
+
// Reuse the existing connectivity gate; TanStack's online/focus managers are global
|
|
19
|
+
// and cannot represent two providers with independent host environment ports.
|
|
20
|
+
gate: new FetchQueryCache({}, { environment }), environment, history: new WeakMap(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Provider-owned TanStack cache and binding, compatible with hooks from the default entry.
|
|
24
|
+
* @example <FetchQueryProvider><ResourceScreen /></FetchQueryProvider>
|
|
25
|
+
* @param props Children and optional host connectivity/focus port. */
|
|
26
|
+
export function FetchQueryProvider({ children, environment = browserFetchQueryEnvironment }) {
|
|
27
|
+
// A module singleton would leak cache values between independent providers/tests,
|
|
28
|
+
// producing cases that pass alone but fail together. Scope ownership to this provider.
|
|
29
|
+
const scope = useMemo(() => createScope({ environment }), [environment]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
scope.gate.activate();
|
|
32
|
+
let online = environment.isOnline();
|
|
33
|
+
const release = environment.subscribeOnline({ listener: next => {
|
|
34
|
+
const reconnect = next && !online;
|
|
35
|
+
online = next;
|
|
36
|
+
// An offline/online pair may be batched into one React render. Signal the cache
|
|
37
|
+
// directly rather than depending on a disabled -> enabled render to revalidate.
|
|
38
|
+
if (reconnect)
|
|
39
|
+
void scope.client.refetchQueries({ predicate: query => query.isActive() && query.isStale() && query.state.fetchStatus !== 'fetching' });
|
|
40
|
+
} });
|
|
41
|
+
return () => { release(); scope.gate.dispose(); };
|
|
42
|
+
}, [scope, environment]);
|
|
43
|
+
return _jsx(QueryClientProvider, { client: scope.client, children: _jsx(ScopeContext.Provider, { value: scope, children: _jsx(FetchQueryBinding, { adapter: adapter, children: children }) }) });
|
|
44
|
+
}
|
|
45
|
+
/** Fail closed when an adapter hook is used without its provider. */
|
|
46
|
+
function useScope() {
|
|
47
|
+
const scope = useContext(ScopeContext);
|
|
48
|
+
if (!scope)
|
|
49
|
+
throw new Error('Fetch-query hooks require FetchQueryProvider');
|
|
50
|
+
return scope;
|
|
51
|
+
}
|
|
52
|
+
/** Observe only this provider's connectivity; paused reads do not claim network activity. */
|
|
53
|
+
function useOnline({ environment }) {
|
|
54
|
+
const subscribe = useCallback((listener) => environment.subscribeOnline({ listener }), [environment]);
|
|
55
|
+
const snapshot = useCallback(() => environment.isOnline(), [environment]);
|
|
56
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
57
|
+
}
|
|
58
|
+
/** Preserve contract metadata across observers, without retaining evicted query objects. */
|
|
59
|
+
function historyFor({ scope, query }) {
|
|
60
|
+
let history = scope.history.get(query);
|
|
61
|
+
if (!history) {
|
|
62
|
+
history = { replacement: 0, request: 0, error: null };
|
|
63
|
+
scope.history.set(query, history);
|
|
64
|
+
}
|
|
65
|
+
return history;
|
|
66
|
+
}
|
|
67
|
+
/** Replacement writes win over either outcome of an older transport, across all loaders
|
|
68
|
+
* and reactive reads. Weak keys follow TanStack's own query eviction lifecycle. */
|
|
69
|
+
async function read({ scope, key, fetch }) {
|
|
70
|
+
const query = scope.client.getQueryCache().find({ queryKey: key, exact: true });
|
|
71
|
+
const history = historyFor({ scope, query });
|
|
72
|
+
const generation = history.replacement;
|
|
73
|
+
const request = ++history.request;
|
|
74
|
+
try {
|
|
75
|
+
const value = await scope.gate.runOnline({ run: fetch });
|
|
76
|
+
if (generation !== history.replacement)
|
|
77
|
+
return scope.client.getQueryData(key);
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
throw new Error(`${JSON.stringify(key)} data is undefined`);
|
|
80
|
+
if (request === history.request)
|
|
81
|
+
history.error = null;
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (generation !== history.replacement)
|
|
86
|
+
return scope.client.getQueryData(key);
|
|
87
|
+
// Earlier data remains useful, but a failed refresh is not a fresh cache hit.
|
|
88
|
+
// The next loader/mount must retry even when its staleTime would retain that data.
|
|
89
|
+
if (request === history.request) {
|
|
90
|
+
history.error = toQueryError(error);
|
|
91
|
+
query.invalidate();
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Match the built-in cache's idle retention. TanStack's default gcTime (5 min) would
|
|
97
|
+
* otherwise evict an unobserved entry before a longer staleTime says it is stale. */
|
|
98
|
+
function gcTimeFor({ staleTime }) {
|
|
99
|
+
return Math.max(IDLE_RETENTION, staleTime);
|
|
100
|
+
}
|
|
101
|
+
/** Reactive read translated to the library-free contract, with explicit focus opt-in. */
|
|
102
|
+
function useFetchQuery({ key, fetch }, { enabled = true, staleTime = DEFAULT_STALE_TIME, refetchOnWindowFocus = false } = {}) {
|
|
103
|
+
const scope = useScope();
|
|
104
|
+
const online = useOnline(scope);
|
|
105
|
+
const query = useQuery({ queryKey: key, queryFn: () => read({ scope, key, fetch }), enabled: enabled && online, staleTime, gcTime: gcTimeFor({ staleTime }) }, scope.client);
|
|
106
|
+
const { refetch: refresh } = query;
|
|
107
|
+
const refetch = useCallback(() => {
|
|
108
|
+
if (scope.environment.isOnline()) {
|
|
109
|
+
void refresh();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Reconnect may already have started a fetch (re-enabled observer or provider refetch);
|
|
113
|
+
// that request postdates this one, so join it instead of cancelling it into a duplicate.
|
|
114
|
+
void scope.gate.runOnline({ run: () => refresh({ cancelRefetch: false }) });
|
|
115
|
+
}, [scope, refresh]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!enabled || !refetchOnWindowFocus)
|
|
118
|
+
return;
|
|
119
|
+
return scope.environment.subscribeFocus({ listener: () => {
|
|
120
|
+
const cached = scope.client.getQueryCache().find({ queryKey: key, exact: true });
|
|
121
|
+
if (scope.environment.isOnline() && cached?.isStaleByTime(staleTime) && cached.state.fetchStatus !== 'fetching')
|
|
122
|
+
refetch();
|
|
123
|
+
} });
|
|
124
|
+
}, [scope, JSON.stringify(key), enabled, staleTime, refetchOnWindowFocus, refetch]);
|
|
125
|
+
// TanStack clears an initial error when a retry starts. The contract keeps it until
|
|
126
|
+
// recovery, so a retry is never mistaken for evidence that the last failure is gone.
|
|
127
|
+
const cached = scope.client.getQueryCache().find({ queryKey: key, exact: true });
|
|
128
|
+
const history = historyFor({ scope, query: cached });
|
|
129
|
+
const hasData = query.data !== undefined;
|
|
130
|
+
return {
|
|
131
|
+
data: query.data,
|
|
132
|
+
status: resolveFetchQueryStatus({ disabled: !enabled, hasData, status: query.status }),
|
|
133
|
+
error: resolveFetchQueryError({ error: history.error ?? query.error, disabled: !enabled, hasData }),
|
|
134
|
+
isFetching: online && query.isFetching, refetch,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Preserve promise rejection for awaiting writes while also handling discarded promises. */
|
|
138
|
+
function useFetchMutation({ run }, { invalidates } = {}) {
|
|
139
|
+
const scope = useScope();
|
|
140
|
+
const mutation = useMutation({
|
|
141
|
+
mutationFn: required => scope.gate.runOnline({ run: () => run(required) }),
|
|
142
|
+
onSuccess: () => {
|
|
143
|
+
// Do not await dependent reads: the mutation promise means the write succeeded,
|
|
144
|
+
// not that every list finished reloading. Refetches proceed on their own schedule.
|
|
145
|
+
for (const key of invalidates ?? [])
|
|
146
|
+
void scope.client.invalidateQueries({ queryKey: key });
|
|
147
|
+
},
|
|
148
|
+
}, scope.client);
|
|
149
|
+
const { mutateAsync, reset } = mutation;
|
|
150
|
+
const mutate = useCallback((required) => {
|
|
151
|
+
const promise = mutateAsync(required);
|
|
152
|
+
// Handle this same promise for callers that discard it, while still returning it so
|
|
153
|
+
// await/catch observes the original rejection; discarding the derived promise is intentional.
|
|
154
|
+
void promise.catch(() => { });
|
|
155
|
+
return promise;
|
|
156
|
+
}, [mutateAsync]);
|
|
157
|
+
return { mutate, status: mutation.status, error: mutation.status === 'error' ? toQueryError(mutation.error) : null, reset };
|
|
158
|
+
}
|
|
159
|
+
/** Loader shares reactive reads, retains fresh idle values, and protects replacement writes. */
|
|
160
|
+
function useCachedLoader({ key, fetch }, { staleTime = DEFAULT_STALE_TIME } = {}) {
|
|
161
|
+
const scope = useScope();
|
|
162
|
+
return useMemo(() => {
|
|
163
|
+
const options = { queryKey: key, queryFn: () => read({ scope, key, fetch }), staleTime, gcTime: gcTimeFor({ staleTime }) };
|
|
164
|
+
return {
|
|
165
|
+
peek: () => scope.client.getQueryData(key),
|
|
166
|
+
load: () => scope.client.fetchQuery(options).catch(error => {
|
|
167
|
+
// A forced refresh can supersede a loader's request without aborting its transport.
|
|
168
|
+
// Follow the new request instead of exposing TanStack's cancellation vocabulary.
|
|
169
|
+
if (!isCancelledError(error))
|
|
170
|
+
throw error;
|
|
171
|
+
const query = scope.client.getQueryCache().find({ queryKey: key, exact: true });
|
|
172
|
+
return (query.promise ?? scope.client.getQueryData(key));
|
|
173
|
+
}),
|
|
174
|
+
replace: ({ value }) => {
|
|
175
|
+
const query = scope.client.getQueryCache().build(scope.client, options);
|
|
176
|
+
const history = historyFor({ scope, query });
|
|
177
|
+
history.replacement += 1;
|
|
178
|
+
history.error = null;
|
|
179
|
+
scope.client.setQueryData(key, value);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}, [scope, key, fetch, staleTime]);
|
|
183
|
+
}
|
|
184
|
+
/** Prefix invalidation also reaches child keys read by other mounted consumers. */
|
|
185
|
+
function useInvalidate() {
|
|
186
|
+
const { client } = useScope();
|
|
187
|
+
return useCallback(({ key }) => { void client.invalidateQueries({ queryKey: key }); }, [client]);
|
|
188
|
+
}
|
|
189
|
+
const adapter = { useFetchQuery, useFetchMutation, useCachedLoader, useInvalidate };
|
|
190
|
+
//# sourceMappingURL=adapter.tanstack.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { FetchQueryAdapter } from './types.js';
|
|
3
|
+
/** Internal composition seam: hooks and imperative loaders always use the same provider.
|
|
4
|
+
* Keep the adapter fixed for this provider's lifetime to preserve React hook ordering. */
|
|
5
|
+
export declare function FetchQueryBinding({ adapter, children }: {
|
|
6
|
+
adapter: FetchQueryAdapter;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
/** Library-free read facade; the provider selects its implementation. */
|
|
10
|
+
export declare const useFetchQuery: FetchQueryAdapter['useFetchQuery'];
|
|
11
|
+
/** Library-free write facade, including safe discarded promises. */
|
|
12
|
+
export declare const useFetchMutation: FetchQueryAdapter['useFetchMutation'];
|
|
13
|
+
/** Prefix invalidator for the nearest provider's cache. */
|
|
14
|
+
export declare const useInvalidate: FetchQueryAdapter['useInvalidate'];
|
|
15
|
+
/** Promise loader using the same cache as reactive reads. */
|
|
16
|
+
export declare const useCachedLoader: FetchQueryAdapter['useCachedLoader'];
|
|
17
|
+
//# sourceMappingURL=binding.react.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
const AdapterContext = createContext(null);
|
|
4
|
+
/** Internal composition seam: hooks and imperative loaders always use the same provider.
|
|
5
|
+
* Keep the adapter fixed for this provider's lifetime to preserve React hook ordering. */
|
|
6
|
+
export function FetchQueryBinding({ adapter, children }) {
|
|
7
|
+
return _jsx(AdapterContext.Provider, { value: adapter, children: children });
|
|
8
|
+
}
|
|
9
|
+
/** Resolve the nearest provider; fail before attempting an unscoped read or write. */
|
|
10
|
+
function useAdapter() {
|
|
11
|
+
const adapter = useContext(AdapterContext);
|
|
12
|
+
if (!adapter)
|
|
13
|
+
throw new Error('Fetch-query hooks require FetchQueryProvider');
|
|
14
|
+
return adapter;
|
|
15
|
+
}
|
|
16
|
+
/** Library-free read facade; the provider selects its implementation. */
|
|
17
|
+
export const useFetchQuery = (required, optional = {}) => useAdapter().useFetchQuery(required, optional);
|
|
18
|
+
/** Library-free write facade, including safe discarded promises. */
|
|
19
|
+
export const useFetchMutation = (required, optional = {}) => useAdapter().useFetchMutation(required, optional);
|
|
20
|
+
/** Prefix invalidator for the nearest provider's cache. */
|
|
21
|
+
export const useInvalidate = () => useAdapter().useInvalidate();
|
|
22
|
+
/** Promise loader using the same cache as reactive reads. */
|
|
23
|
+
export const useCachedLoader = (required, optional = {}) => useAdapter().useCachedLoader(required, optional);
|
|
24
|
+
//# sourceMappingURL=binding.react.js.map
|
|
@@ -9,6 +9,8 @@ import type { QueryKey } from './types.js';
|
|
|
9
9
|
* must be fresh on every mount can choose staleTime: 0 without lowering every query default.
|
|
10
10
|
*/
|
|
11
11
|
export declare const DEFAULT_STALE_TIME = 10000;
|
|
12
|
+
/** Minimum time an unobserved entry is kept; both adapters retain max(this, staleTime). */
|
|
13
|
+
export declare const IDLE_RETENTION: number;
|
|
12
14
|
export interface QueryCacheSchedulerPort {
|
|
13
15
|
schedule(required: {
|
|
14
16
|
callback: () => void;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* must be fresh on every mount can choose staleTime: 0 without lowering every query default.
|
|
8
8
|
*/
|
|
9
9
|
export const DEFAULT_STALE_TIME = 10_000;
|
|
10
|
-
|
|
10
|
+
/** Minimum time an unobserved entry is kept; both adapters retain max(this, staleTime). */
|
|
11
|
+
export const IDLE_RETENTION = 5 * 60_000;
|
|
11
12
|
const browserScheduler = {
|
|
12
13
|
schedule: ({ callback, delayMs }) => setTimeout(callback, delayMs),
|
|
13
14
|
cancel: ({ handle }) => clearTimeout(handle),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { FetchQueryProvider
|
|
1
|
+
export { FetchQueryProvider } from './adapter.react.js';
|
|
2
|
+
export { useFetchQuery, useFetchMutation, useCachedLoader, useInvalidate } from './binding.react.js';
|
|
2
3
|
export type { FetchQueryEnvironmentPort } from './cache.js';
|
|
3
4
|
export type { QueryKey, QueryStatus, QueryResult, FetchQueryOptions, MutationStatus, MutationResult, FetchMutationOptions, CachedLoaderOptions, CachedLoader, FetchQueryAdapter } from './types.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { FetchQueryProvider
|
|
1
|
+
export { FetchQueryProvider } from './adapter.react.js';
|
|
2
|
+
export { useFetchQuery, useFetchMutation, useCachedLoader, useInvalidate } from './binding.react.js';
|
|
2
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Optional adapter entry. Importing the default entry never loads TanStack. */
|
|
2
|
+
export { FetchQueryProvider } from './adapter.tanstack.js';
|
|
3
|
+
export { useFetchQuery, useFetchMutation, useCachedLoader, useInvalidate } from './binding.react.js';
|
|
4
|
+
export type { FetchQueryEnvironmentPort } from './cache.js';
|
|
5
|
+
export type { QueryKey, QueryStatus, QueryResult, FetchQueryOptions, MutationStatus, MutationResult, FetchMutationOptions, CachedLoaderOptions, CachedLoader, FetchQueryAdapter } from './types.js';
|
|
6
|
+
//# sourceMappingURL=tanstack.d.ts.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Optional adapter entry. Importing the default entry never loads TanStack. */
|
|
2
|
+
export { FetchQueryProvider } from './adapter.tanstack.js';
|
|
3
|
+
export { useFetchQuery, useFetchMutation, useCachedLoader, useInvalidate } from './binding.react.js';
|
|
4
|
+
//# sourceMappingURL=tanstack.js.map
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* key also refreshes its derived child keys. Exact-key equality would compile while quietly
|
|
10
10
|
* breaking that guarantee; avoid a shared prefix for resources that merely sound related,
|
|
11
11
|
* or writes will fan out unnecessary refetches.
|
|
12
|
+
* Stay primitive-only: the built-in cache matches each part with === while TanStack deep-matches,
|
|
13
|
+
* so an object part would invalidate under one adapter and silently not under the other.
|
|
12
14
|
*/
|
|
13
15
|
export type QueryKey = readonly (string | number)[];
|
|
14
16
|
export type QueryStatus = "loading" | "success" | "error";
|