@databricks/appkit-ui 0.62.0 → 0.63.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.
@@ -0,0 +1,144 @@
1
+ import { ArrowClient } from "../../js/arrow/arrow-client.js";
2
+ import { connectSSE } from "../../js/sse/connect-sse.js";
3
+ import { GENERIC_LOAD_ERROR, handleAnalyticsSseError, handleAnalyticsSseMessage, userFacingFetchError } from "./analytics-sse.js";
4
+ import { createRequestStore } from "./request-store.js";
5
+
6
+ //#region src/react/hooks/analytics-request-store.ts
7
+ /** Idle snapshot returned for keys with no live entry. Referentially stable. */
8
+ const EMPTY_SNAPSHOT = {
9
+ data: null,
10
+ loading: false,
11
+ error: null,
12
+ errorCode: null,
13
+ warehouseStatus: null
14
+ };
15
+ /** Snapshot a request resets to when it (re)starts. */
16
+ const LOADING_SNAPSHOT = {
17
+ data: null,
18
+ loading: true,
19
+ error: null,
20
+ errorCode: null,
21
+ warehouseStatus: null
22
+ };
23
+ /**
24
+ * Fetch the real column names for a statement from the fallback endpoint,
25
+ * used when a very wide schema's names didn't fit in the response header.
26
+ * Returns undefined on any failure so decoding falls back to the raw Arrow
27
+ * schema names.
28
+ */
29
+ async function fetchArrowColumns(statementId, signal) {
30
+ try {
31
+ const res = await fetch(`/api/analytics/columns/${encodeURIComponent(statementId)}`, { signal });
32
+ if (!res.ok) return void 0;
33
+ const body = await res.json();
34
+ return Array.isArray(body.columns) ? body.columns : void 0;
35
+ } catch {
36
+ return;
37
+ }
38
+ }
39
+ /**
40
+ * Fetch an ARROW_STREAM query result as raw Arrow IPC bytes directly from
41
+ * the query endpoint (no SSE, no second /arrow-result request) and decode
42
+ * it into a Table. The server streams the bytes back as the POST response
43
+ * body; errors before the first byte arrive as a JSON `{ error, errorCode }`.
44
+ */
45
+ async function fetchArrowDirect(controls, options) {
46
+ const { signal } = controls;
47
+ try {
48
+ const response = await fetch(options.url, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: options.payload,
52
+ signal
53
+ });
54
+ if (signal.aborted) return;
55
+ if (!response.ok) {
56
+ let message = GENERIC_LOAD_ERROR;
57
+ let code = null;
58
+ try {
59
+ const body = await response.json();
60
+ if (body.error) message = body.error;
61
+ if (typeof body.errorCode === "string") code = body.errorCode;
62
+ } catch {}
63
+ controls.patch({
64
+ loading: false,
65
+ error: message,
66
+ errorCode: code
67
+ });
68
+ return;
69
+ }
70
+ const buffer = await response.arrayBuffer();
71
+ if (signal.aborted) return;
72
+ let columnNames;
73
+ const header = response.headers.get("X-Appkit-Arrow-Columns");
74
+ if (header) try {
75
+ columnNames = JSON.parse(decodeURIComponent(header));
76
+ } catch {}
77
+ else {
78
+ const ref = response.headers.get("X-Appkit-Arrow-Columns-Ref");
79
+ if (ref) columnNames = await fetchArrowColumns(ref, signal);
80
+ }
81
+ const table = await ArrowClient.processArrowBuffer(new Uint8Array(buffer), columnNames);
82
+ controls.patch({
83
+ loading: false,
84
+ data: table
85
+ });
86
+ } catch (error) {
87
+ if (signal.aborted) return;
88
+ controls.patch({
89
+ loading: false,
90
+ error: userFacingFetchError(error)
91
+ });
92
+ }
93
+ }
94
+ /**
95
+ * Build the runner for a request: reset to loading, then run the
96
+ * format-appropriate transport, reporting state through `controls.patch`.
97
+ */
98
+ function runAnalyticsRequest(options) {
99
+ return (controls) => {
100
+ controls.patch(LOADING_SNAPSHOT);
101
+ if (options.format === "ARROW_STREAM") {
102
+ fetchArrowDirect(controls, options);
103
+ return;
104
+ }
105
+ const sseContext = {
106
+ source: "useAnalyticsQuery",
107
+ resource: { url: options.url },
108
+ defaultExecutionError: "Unable to execute query",
109
+ unpublishOnMalformedMessage: false,
110
+ signal: controls.signal,
111
+ abort: controls.abort,
112
+ setLoading: (loading) => controls.patch({ loading }),
113
+ setError: (error) => controls.patch({ error }),
114
+ setErrorCode: (errorCode) => controls.patch({ errorCode }),
115
+ onWarehouseStatus: (status) => controls.patch({ warehouseStatus: status }),
116
+ onResult: (message) => controls.patch({ data: message.data }),
117
+ unpublishWarehouseStatus: () => {}
118
+ };
119
+ connectSSE({
120
+ url: options.url,
121
+ payload: options.payload,
122
+ signal: controls.signal,
123
+ onMessage: (message) => handleAnalyticsSseMessage(message.data, sseContext),
124
+ onError: (error) => handleAnalyticsSseError(error, sseContext)
125
+ });
126
+ };
127
+ }
128
+ const store = createRequestStore(EMPTY_SNAPSHOT);
129
+ /**
130
+ * Register a subscriber for `key`, starting the shared request on first use.
131
+ * Returns a `release` function that must be called on unmount.
132
+ */
133
+ function retain(key, options, autoStart = true) {
134
+ return store.retain(key, runAnalyticsRequest(options), autoStart);
135
+ }
136
+ const start = store.start;
137
+ const subscribe = store.subscribe;
138
+ const getSnapshot = store.getSnapshot;
139
+ /** Test-only: abort every in-flight request and clear the store. */
140
+ const resetAnalyticsRequestStore = store.reset;
141
+
142
+ //#endregion
143
+ export { getSnapshot, retain, start, subscribe };
144
+ //# sourceMappingURL=analytics-request-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-request-store.js","names":[],"sources":["../../../src/react/hooks/analytics-request-store.ts"],"sourcesContent":["import { ArrowClient, connectSSE } from \"@/js\";\n\nimport {\n type AnalyticsSseHandlerContext,\n GENERIC_LOAD_ERROR,\n handleAnalyticsSseError,\n handleAnalyticsSseMessage,\n userFacingFetchError,\n} from \"./analytics-sse\";\nimport {\n createRequestStore,\n type RequestControls,\n type RequestRunner,\n} from \"./request-store\";\nimport type { WarehouseStatus } from \"./types\";\n\n/**\n * Shared in-flight request store for `useAnalyticsQuery`: an instance of the\n * generic {@link createRequestStore} lifecycle wired to the analytics\n * transports. Multiple hook instances resolving to the same request (query\n * key, parameters, format, dev mode) share one network request and see the\n * same result and mid-flight `warehouse_status` updates.\n *\n * The lifecycle (dedup, refcount, deferred teardown) lives in the factory; this\n * module only supplies the snapshot shape and how a request runs — SSE via\n * `analytics-sse.ts` or a direct Arrow fetch.\n */\n\n/** Options describing the request a keyed entry runs. */\ninterface AnalyticsRequestOptions {\n /** Full request URL (already includes the encoded query key and dev suffix). */\n url: string;\n /** Serialized `{ parameters, format }` body. */\n payload: string;\n /** Response format; selects the transport. */\n format: string;\n}\n\n/** Immutable per-key request state; mirrors the hook's public result shape. */\ninterface AnalyticsRequestSnapshot {\n data: unknown;\n loading: boolean;\n error: string | null;\n errorCode: string | null;\n warehouseStatus: WarehouseStatus | null;\n}\n\n/** Idle snapshot returned for keys with no live entry. Referentially stable. */\nconst EMPTY_SNAPSHOT: AnalyticsRequestSnapshot = {\n data: null,\n loading: false,\n error: null,\n errorCode: null,\n warehouseStatus: null,\n};\n\n/** Snapshot a request resets to when it (re)starts. */\nconst LOADING_SNAPSHOT: AnalyticsRequestSnapshot = {\n data: null,\n loading: true,\n error: null,\n errorCode: null,\n warehouseStatus: null,\n};\n\ntype Controls = RequestControls<AnalyticsRequestSnapshot>;\n\n/**\n * Fetch the real column names for a statement from the fallback endpoint,\n * used when a very wide schema's names didn't fit in the response header.\n * Returns undefined on any failure so decoding falls back to the raw Arrow\n * schema names.\n */\nasync function fetchArrowColumns(\n statementId: string,\n signal: AbortSignal,\n): Promise<string[] | undefined> {\n try {\n const res = await fetch(\n `/api/analytics/columns/${encodeURIComponent(statementId)}`,\n { signal },\n );\n if (!res.ok) return undefined;\n const body = (await res.json()) as { columns?: unknown };\n return Array.isArray(body.columns) ? (body.columns as string[]) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Fetch an ARROW_STREAM query result as raw Arrow IPC bytes directly from\n * the query endpoint (no SSE, no second /arrow-result request) and decode\n * it into a Table. The server streams the bytes back as the POST response\n * body; errors before the first byte arrive as a JSON `{ error, errorCode }`.\n */\nasync function fetchArrowDirect(\n controls: Controls,\n options: AnalyticsRequestOptions,\n): Promise<void> {\n const { signal } = controls;\n try {\n const response = await fetch(options.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: options.payload,\n signal,\n });\n if (signal.aborted) return;\n\n if (!response.ok) {\n let message = GENERIC_LOAD_ERROR;\n let code: string | null = null;\n try {\n const body = (await response.json()) as {\n error?: string;\n errorCode?: string;\n };\n if (body.error) message = body.error;\n if (typeof body.errorCode === \"string\") code = body.errorCode;\n } catch {\n // Non-JSON error body — keep the generic message.\n }\n controls.patch({ loading: false, error: message, errorCode: code });\n return;\n }\n\n const buffer = await response.arrayBuffer();\n if (signal.aborted) return;\n // Databricks encodes ARROW_STREAM columns positionally (col_0, …); the\n // server sends the real manifest names so we can relabel the decoded\n // Table (charts look columns up by name). Normally inline in the\n // `X-Appkit-Arrow-Columns` header; for very wide schemas the header\n // carries only a statement-id reference and we fetch the names.\n let columnNames: string[] | undefined;\n const header = response.headers.get(\"X-Appkit-Arrow-Columns\");\n if (header) {\n try {\n columnNames = JSON.parse(decodeURIComponent(header));\n } catch {\n // Malformed header — fall back to the raw Arrow schema names.\n }\n } else {\n const ref = response.headers.get(\"X-Appkit-Arrow-Columns-Ref\");\n if (ref) {\n columnNames = await fetchArrowColumns(ref, signal);\n }\n }\n const table = await ArrowClient.processArrowBuffer(\n new Uint8Array(buffer),\n columnNames,\n );\n controls.patch({ loading: false, data: table });\n } catch (error) {\n if (signal.aborted) return;\n controls.patch({ loading: false, error: userFacingFetchError(error) });\n }\n}\n\n/**\n * Build the runner for a request: reset to loading, then run the\n * format-appropriate transport, reporting state through `controls.patch`.\n */\nfunction runAnalyticsRequest(\n options: AnalyticsRequestOptions,\n): RequestRunner<AnalyticsRequestSnapshot> {\n return (controls) => {\n controls.patch(LOADING_SNAPSHOT);\n\n // ARROW_STREAM: the server streams raw Arrow IPC bytes back on the query\n // response body (no SSE). Fetch and decode directly.\n if (options.format === \"ARROW_STREAM\") {\n void fetchArrowDirect(controls, options);\n return;\n }\n\n // Adapt the shared SSE handler onto the snapshot model. No warehouse\n // publisher lives here — the hook mirrors status from the snapshot — so\n // `unpublishWarehouseStatus` is a no-op.\n const sseContext: AnalyticsSseHandlerContext = {\n source: \"useAnalyticsQuery\",\n resource: { url: options.url },\n defaultExecutionError: \"Unable to execute query\",\n unpublishOnMalformedMessage: false,\n signal: controls.signal,\n abort: controls.abort,\n setLoading: (loading) => controls.patch({ loading }),\n setError: (error) => controls.patch({ error }),\n setErrorCode: (errorCode) => controls.patch({ errorCode }),\n onWarehouseStatus: (status) =>\n controls.patch({ warehouseStatus: status }),\n onResult: (message) => controls.patch({ data: message.data }),\n unpublishWarehouseStatus: () => {},\n };\n\n connectSSE({\n url: options.url,\n payload: options.payload,\n signal: controls.signal,\n onMessage: (message) =>\n handleAnalyticsSseMessage(message.data, sseContext),\n onError: (error) => handleAnalyticsSseError(error, sseContext),\n });\n };\n}\n\nconst store = createRequestStore<AnalyticsRequestSnapshot>(EMPTY_SNAPSHOT);\n\n/**\n * Register a subscriber for `key`, starting the shared request on first use.\n * Returns a `release` function that must be called on unmount.\n */\nexport function retain(\n key: string,\n options: AnalyticsRequestOptions,\n autoStart = true,\n): () => void {\n return store.retain(key, runAnalyticsRequest(options), autoStart);\n}\n\nexport const start = store.start;\nexport const subscribe = store.subscribe;\nexport const getSnapshot = store.getSnapshot;\n\n/** Test-only: abort every in-flight request and clear the store. */\nexport const resetAnalyticsRequestStore = store.reset;\n"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ //#region src/react/hooks/request-store.ts
2
+ function createRequestStore(idle) {
3
+ const entries = /* @__PURE__ */ new Map();
4
+ const listenersByKey = /* @__PURE__ */ new Map();
5
+ function notify(key) {
6
+ const listeners = listenersByKey.get(key);
7
+ if (!listeners) return;
8
+ for (const listener of listeners) listener();
9
+ }
10
+ function start(key) {
11
+ const entry = entries.get(key);
12
+ if (!entry) return;
13
+ entry.abortController?.abort();
14
+ entry.started = true;
15
+ const abortController = new AbortController();
16
+ entry.abortController = abortController;
17
+ entry.run({
18
+ signal: abortController.signal,
19
+ abort: () => abortController.abort(),
20
+ patch(next) {
21
+ entry.snapshot = {
22
+ ...entry.snapshot,
23
+ ...next
24
+ };
25
+ notify(key);
26
+ }
27
+ });
28
+ }
29
+ function release(key) {
30
+ const entry = entries.get(key);
31
+ if (!entry) return;
32
+ entry.refCount -= 1;
33
+ if (entry.refCount > 0) return;
34
+ entry.teardownTimer = setTimeout(() => {
35
+ const current = entries.get(key);
36
+ if (!current || current.refCount > 0) return;
37
+ current.abortController?.abort();
38
+ entries.delete(key);
39
+ }, 0);
40
+ }
41
+ return {
42
+ retain(key, run, autoStart = true) {
43
+ let entry = entries.get(key);
44
+ if (!entry) {
45
+ entry = {
46
+ snapshot: idle,
47
+ refCount: 0,
48
+ abortController: null,
49
+ teardownTimer: null,
50
+ started: false,
51
+ run
52
+ };
53
+ entries.set(key, entry);
54
+ }
55
+ if (entry.teardownTimer !== null) {
56
+ clearTimeout(entry.teardownTimer);
57
+ entry.teardownTimer = null;
58
+ }
59
+ entry.refCount += 1;
60
+ if (autoStart && !entry.started) start(key);
61
+ return () => release(key);
62
+ },
63
+ start,
64
+ subscribe(key, listener) {
65
+ let listeners = listenersByKey.get(key);
66
+ if (!listeners) {
67
+ listeners = /* @__PURE__ */ new Set();
68
+ listenersByKey.set(key, listeners);
69
+ }
70
+ listeners.add(listener);
71
+ return () => {
72
+ listeners.delete(listener);
73
+ if (listeners.size === 0) listenersByKey.delete(key);
74
+ };
75
+ },
76
+ getSnapshot(key) {
77
+ return entries.get(key)?.snapshot ?? idle;
78
+ },
79
+ reset() {
80
+ for (const entry of entries.values()) {
81
+ if (entry.teardownTimer !== null) clearTimeout(entry.teardownTimer);
82
+ entry.abortController?.abort();
83
+ }
84
+ entries.clear();
85
+ listenersByKey.clear();
86
+ }
87
+ };
88
+ }
89
+
90
+ //#endregion
91
+ export { createRequestStore };
92
+ //# sourceMappingURL=request-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-store.js","names":[],"sources":["../../../src/react/hooks/request-store.ts"],"sourcesContent":["/**\n * Generic keyed request store: coalesces identical in-flight requests so N\n * subscribers of the same key share one run, and fans state updates back out\n * via `useSyncExternalStore`.\n *\n * Owns only the lifecycle — refcount, deferred teardown, subscribe/notify — and\n * is transport-agnostic: the caller's `run` performs the actual fetch (SSE,\n * Arrow, plain fetch, …) and reports state through `controls.patch`. The\n * snapshot type `S` and its reset policy live entirely with the caller.\n *\n * Dedup-only: an entry lives exactly as long as it has subscribers. When the\n * last one releases, teardown is deferred a tick (so a React StrictMode\n * unmount→remount reuses the in-flight request instead of aborting it); if no\n * one re-subscribes by then, the request is aborted and the entry dropped.\n */\n\n/** What a `run` uses to drive its request and report state. */\nexport interface RequestControls<S> {\n /** Aborted when the request is superseded or torn down. */\n signal: AbortSignal;\n /** Abort this run's transport (e.g. to close a stream on a fatal frame). */\n abort(): void;\n /** Merge fields into the entry's snapshot and notify subscribers. */\n patch(next: Partial<S>): void;\n}\n\n/** Starts a request and reports state through `controls`. */\nexport type RequestRunner<S> = (controls: RequestControls<S>) => void;\n\ninterface RequestStore<S> {\n /**\n * Register a subscriber for `key`, creating and starting the shared request\n * on first use. Returns a `release` function to call on unmount.\n *\n * @param run Runs the request; stored on the entry and re-invoked by\n * `start`. Only the first caller's `run` is used (later joiners share it).\n * @param autoStart Start the request on creation. Default true.\n */\n retain(key: string, run: RequestRunner<S>, autoStart?: boolean): () => void;\n /** (Re)start the request for `key`: abort any in-flight run, then re-run. */\n start(key: string): void;\n subscribe(key: string, listener: () => void): () => void;\n getSnapshot(key: string): S;\n /** Test-only: abort every in-flight request and clear the store. */\n reset(): void;\n}\n\ninterface Entry<S> {\n snapshot: S;\n refCount: number;\n abortController: AbortController | null;\n teardownTimer: ReturnType<typeof setTimeout> | null;\n /** True once `start` has run at least once; guards re-run on late `retain`. */\n started: boolean;\n run: RequestRunner<S>;\n}\n\nexport function createRequestStore<S>(idle: S): RequestStore<S> {\n const entries = new Map<string, Entry<S>>();\n\n // Keyed separately from `entries`: `subscribe` can run before `retain`\n // creates the entry, so listeners must survive independently of entry life.\n const listenersByKey = new Map<string, Set<() => void>>();\n\n function notify(key: string): void {\n const listeners = listenersByKey.get(key);\n if (!listeners) return;\n for (const listener of listeners) listener();\n }\n\n function start(key: string): void {\n const entry = entries.get(key);\n if (!entry) return;\n\n entry.abortController?.abort();\n entry.started = true;\n\n const abortController = new AbortController();\n entry.abortController = abortController;\n\n entry.run({\n signal: abortController.signal,\n abort: () => abortController.abort(),\n patch(next) {\n entry.snapshot = { ...entry.snapshot, ...next };\n notify(key);\n },\n });\n }\n\n function release(key: string): void {\n const entry = entries.get(key);\n if (!entry) return;\n entry.refCount -= 1;\n if (entry.refCount > 0) return;\n\n // Defer teardown one tick: a StrictMode unmount→remount (or fast route\n // swap) re-`retain`s within the same tick and reuses the request.\n entry.teardownTimer = setTimeout(() => {\n const current = entries.get(key);\n if (!current || current.refCount > 0) return;\n current.abortController?.abort();\n entries.delete(key);\n }, 0);\n }\n\n return {\n retain(key, run, autoStart = true) {\n let entry = entries.get(key);\n if (!entry) {\n entry = {\n snapshot: idle,\n refCount: 0,\n abortController: null,\n teardownTimer: null,\n started: false,\n run,\n };\n entries.set(key, entry);\n }\n\n // A late joiner cancels any pending teardown so it keeps the live request.\n if (entry.teardownTimer !== null) {\n clearTimeout(entry.teardownTimer);\n entry.teardownTimer = null;\n }\n entry.refCount += 1;\n\n if (autoStart && !entry.started) {\n start(key);\n }\n\n return () => release(key);\n },\n\n start,\n\n subscribe(key, listener) {\n let listeners = listenersByKey.get(key);\n if (!listeners) {\n listeners = new Set();\n listenersByKey.set(key, listeners);\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) listenersByKey.delete(key);\n };\n },\n\n getSnapshot(key) {\n return entries.get(key)?.snapshot ?? idle;\n },\n\n reset() {\n for (const entry of entries.values()) {\n if (entry.teardownTimer !== null) clearTimeout(entry.teardownTimer);\n entry.abortController?.abort();\n }\n entries.clear();\n listenersByKey.clear();\n },\n };\n}\n"],"mappings":";AAyDA,SAAgB,mBAAsB,MAA0B;CAC9D,MAAM,0BAAU,IAAI,KAAuB;CAI3C,MAAM,iCAAiB,IAAI,KAA8B;CAEzD,SAAS,OAAO,KAAmB;EACjC,MAAM,YAAY,eAAe,IAAI,IAAI;AACzC,MAAI,CAAC,UAAW;AAChB,OAAK,MAAM,YAAY,UAAW,WAAU;;CAG9C,SAAS,MAAM,KAAmB;EAChC,MAAM,QAAQ,QAAQ,IAAI,IAAI;AAC9B,MAAI,CAAC,MAAO;AAEZ,QAAM,iBAAiB,OAAO;AAC9B,QAAM,UAAU;EAEhB,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,QAAM,kBAAkB;AAExB,QAAM,IAAI;GACR,QAAQ,gBAAgB;GACxB,aAAa,gBAAgB,OAAO;GACpC,MAAM,MAAM;AACV,UAAM,WAAW;KAAE,GAAG,MAAM;KAAU,GAAG;KAAM;AAC/C,WAAO,IAAI;;GAEd,CAAC;;CAGJ,SAAS,QAAQ,KAAmB;EAClC,MAAM,QAAQ,QAAQ,IAAI,IAAI;AAC9B,MAAI,CAAC,MAAO;AACZ,QAAM,YAAY;AAClB,MAAI,MAAM,WAAW,EAAG;AAIxB,QAAM,gBAAgB,iBAAiB;GACrC,MAAM,UAAU,QAAQ,IAAI,IAAI;AAChC,OAAI,CAAC,WAAW,QAAQ,WAAW,EAAG;AACtC,WAAQ,iBAAiB,OAAO;AAChC,WAAQ,OAAO,IAAI;KAClB,EAAE;;AAGP,QAAO;EACL,OAAO,KAAK,KAAK,YAAY,MAAM;GACjC,IAAI,QAAQ,QAAQ,IAAI,IAAI;AAC5B,OAAI,CAAC,OAAO;AACV,YAAQ;KACN,UAAU;KACV,UAAU;KACV,iBAAiB;KACjB,eAAe;KACf,SAAS;KACT;KACD;AACD,YAAQ,IAAI,KAAK,MAAM;;AAIzB,OAAI,MAAM,kBAAkB,MAAM;AAChC,iBAAa,MAAM,cAAc;AACjC,UAAM,gBAAgB;;AAExB,SAAM,YAAY;AAElB,OAAI,aAAa,CAAC,MAAM,QACtB,OAAM,IAAI;AAGZ,gBAAa,QAAQ,IAAI;;EAG3B;EAEA,UAAU,KAAK,UAAU;GACvB,IAAI,YAAY,eAAe,IAAI,IAAI;AACvC,OAAI,CAAC,WAAW;AACd,gCAAY,IAAI,KAAK;AACrB,mBAAe,IAAI,KAAK,UAAU;;AAEpC,aAAU,IAAI,SAAS;AACvB,gBAAa;AACX,cAAU,OAAO,SAAS;AAC1B,QAAI,UAAU,SAAS,EAAG,gBAAe,OAAO,IAAI;;;EAIxD,YAAY,KAAK;AACf,UAAO,QAAQ,IAAI,IAAI,EAAE,YAAY;;EAGvC,QAAQ;AACN,QAAK,MAAM,SAAS,QAAQ,QAAQ,EAAE;AACpC,QAAI,MAAM,kBAAkB,KAAM,cAAa,MAAM,cAAc;AACnE,UAAM,iBAAiB,OAAO;;AAEhC,WAAQ,OAAO;AACf,kBAAe,OAAO;;EAEzB"}
@@ -7,6 +7,12 @@ import { AnalyticsFormat, InferParams, InferResultByFormat, QueryKey, UseAnalyti
7
7
  * results are fetched as raw Arrow bytes directly from the query endpoint.
8
8
  * Integration hook between client and analytics plugin.
9
9
  *
10
+ * Identical requests (same query key, parameters, format, and dev mode) share
11
+ * a single in-flight network request: the first mounting instance starts it,
12
+ * later instances subscribe to the same {@link store} entry and see the same
13
+ * result and warehouse-status updates. The request is torn down once its last
14
+ * subscriber unmounts.
15
+ *
10
16
  * The return type is automatically inferred based on the format:
11
17
  * - `format: "JSON_ARRAY"` (default): Returns typed array from QueryRegistry
12
18
  * - `format: "ARROW_STREAM"`: Returns TypedArrowTable with row type preserved
@@ -1 +1 @@
1
- {"version":3,"file":"use-analytics-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"mappings":";;;;;AA6MA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,iBAAA,wBAEJ,QAAA,GAAW,QAAA,YACX,eAAA,gBAAA,CAEV,QAAA,EAAU,CAAA,EACV,UAAA,GAAa,WAAA,CAAY,CAAA,UACzB,OAAA,GAAS,wBAAA,CAAyB,CAAA,IACjC,uBAAA,CAAwB,mBAAA,CAAoB,CAAA,EAAG,CAAA,EAAG,CAAA"}
1
+ {"version":3,"file":"use-analytics-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"mappings":";;;;;AAgGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,iBAAA,wBAEJ,QAAA,GAAW,QAAA,YACX,eAAA,gBAAA,CAEV,QAAA,EAAU,CAAA,EACV,UAAA,GAAa,WAAA,CAAY,CAAA,UACzB,OAAA,GAAS,wBAAA,CAAyB,CAAA,IACjC,uBAAA,CAAwB,mBAAA,CAAoB,CAAA,EAAG,CAAA,EAAG,CAAA"}
@@ -1,9 +1,8 @@
1
- import { ArrowClient } from "../../js/arrow/arrow-client.js";
2
- import { connectSSE } from "../../js/sse/connect-sse.js";
3
- import { GENERIC_LOAD_ERROR, getDevMode, handleAnalyticsSseError, handleAnalyticsSseMessage, userFacingFetchError } from "./analytics-sse.js";
1
+ import { getDevMode } from "./analytics-sse.js";
2
+ import { getSnapshot, retain, start, subscribe } from "./analytics-request-store.js";
4
3
  import { useAnalyticsWarehousePublisher } from "./use-analytics-warehouse-status.js";
5
4
  import { useQueryHMR } from "./use-query-hmr.js";
6
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
5
+ import { useCallback, useEffect, useId, useMemo, useRef, useSyncExternalStore } from "react";
7
6
 
8
7
  //#region src/react/hooks/use-analytics-query.ts
9
8
  /** Shallow equality for plain-object query parameters (primitive values only). */
@@ -26,78 +25,17 @@ function useStableParams(value) {
26
25
  return ref.current;
27
26
  }
28
27
  /**
29
- * Fetch the real column names for a statement from the fallback endpoint,
30
- * used when a very wide schema's names didn't fit in the response header.
31
- * Returns undefined on any failure so decoding falls back to the raw Arrow
32
- * schema names.
33
- */
34
- async function fetchArrowColumns(statementId, signal) {
35
- try {
36
- const res = await fetch(`/api/analytics/columns/${encodeURIComponent(statementId)}`, { signal });
37
- if (!res.ok) return void 0;
38
- const body = await res.json();
39
- return Array.isArray(body.columns) ? body.columns : void 0;
40
- } catch {
41
- return;
42
- }
43
- }
44
- /**
45
- * Fetch an ARROW_STREAM query result as raw Arrow IPC bytes directly from
46
- * the query endpoint (no SSE, no second /arrow-result request) and decode
47
- * it into a Table. The server streams the bytes back as the POST response
48
- * body; errors before the first byte arrive as a JSON `{ error, errorCode }`.
49
- */
50
- async function fetchArrowDirect(ctx) {
51
- try {
52
- const response = await fetch(ctx.url, {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: ctx.payload,
56
- signal: ctx.signal
57
- });
58
- if (ctx.signal.aborted) return;
59
- if (!response.ok) {
60
- let message = GENERIC_LOAD_ERROR;
61
- let code = null;
62
- try {
63
- const body = await response.json();
64
- if (body.error) message = body.error;
65
- if (typeof body.errorCode === "string") code = body.errorCode;
66
- } catch {}
67
- ctx.setLoading(false);
68
- ctx.setError(message);
69
- if (code) ctx.setErrorCode(code);
70
- ctx.unpublishWarehouseStatus();
71
- return;
72
- }
73
- const buffer = await response.arrayBuffer();
74
- if (ctx.signal.aborted) return;
75
- let columnNames;
76
- const header = response.headers.get("X-Appkit-Arrow-Columns");
77
- if (header) try {
78
- columnNames = JSON.parse(decodeURIComponent(header));
79
- } catch {}
80
- else {
81
- const ref = response.headers.get("X-Appkit-Arrow-Columns-Ref");
82
- if (ref) columnNames = await fetchArrowColumns(ref, ctx.signal);
83
- }
84
- const table = await ArrowClient.processArrowBuffer(new Uint8Array(buffer), columnNames);
85
- ctx.setData(table);
86
- ctx.setLoading(false);
87
- ctx.unpublishWarehouseStatus();
88
- } catch (error) {
89
- if (ctx.signal.aborted) return;
90
- ctx.setLoading(false);
91
- ctx.unpublishWarehouseStatus();
92
- ctx.setError(userFacingFetchError(error));
93
- }
94
- }
95
- /**
96
28
  * Subscribe to an analytics query and return its latest result. JSON_ARRAY
97
29
  * results stream over SSE (with warehouse-readiness progress); ARROW_STREAM
98
30
  * results are fetched as raw Arrow bytes directly from the query endpoint.
99
31
  * Integration hook between client and analytics plugin.
100
32
  *
33
+ * Identical requests (same query key, parameters, format, and dev mode) share
34
+ * a single in-flight network request: the first mounting instance starts it,
35
+ * later instances subscribe to the same {@link store} entry and see the same
36
+ * result and warehouse-status updates. The request is torn down once its last
37
+ * subscriber unmounts.
38
+ *
101
39
  * The return type is automatically inferred based on the format:
102
40
  * - `format: "JSON_ARRAY"` (default): Returns typed array from QueryRegistry
103
41
  * - `format: "ARROW_STREAM"`: Returns TypedArrowTable with row type preserved
@@ -129,12 +67,6 @@ function useAnalyticsQuery(queryKey, parameters, options = {}) {
129
67
  const autoStart = options?.autoStart ?? true;
130
68
  const devMode = getDevMode();
131
69
  const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;
132
- const [data, setData] = useState(null);
133
- const [loading, setLoading] = useState(false);
134
- const [error, setError] = useState(null);
135
- const [errorCode, setErrorCode] = useState(null);
136
- const [warehouseStatus, setWarehouseStatus] = useState(null);
137
- const abortControllerRef = useRef(null);
138
70
  const { publish: publishWarehouseStatus, unpublish: unpublishWarehouseStatus } = useAnalyticsWarehousePublisher(useId(), queryKey);
139
71
  if (!queryKey || queryKey.trim().length === 0) throw new Error("useAnalyticsQuery: 'queryKey' must be a non-empty string.");
140
72
  const stableParameters = useStableParams(parameters);
@@ -155,83 +87,42 @@ function useAnalyticsQuery(queryKey, parameters, options = {}) {
155
87
  format,
156
88
  maxParametersSize
157
89
  ]);
158
- const start = useCallback(() => {
159
- if (payload === null) {
160
- setError("Failed to serialize query parameters");
161
- return;
162
- }
163
- abortControllerRef.current?.abort();
164
- setLoading(true);
165
- setError(null);
166
- setErrorCode(null);
167
- setData(null);
168
- setWarehouseStatus(null);
169
- publishWarehouseStatus(null);
170
- const abortController = new AbortController();
171
- abortControllerRef.current = abortController;
172
- if (format === "ARROW_STREAM") {
173
- fetchArrowDirect({
174
- url: urlSuffix,
175
- payload,
176
- signal: abortController.signal,
177
- setLoading,
178
- setError,
179
- setErrorCode,
180
- setData: (table) => setData(table),
181
- unpublishWarehouseStatus
182
- });
183
- return;
184
- }
185
- const sseContext = {
186
- source: "useAnalyticsQuery",
187
- resource: { queryKey },
188
- defaultExecutionError: "Unable to execute query",
189
- unpublishOnMalformedMessage: false,
190
- signal: abortController.signal,
191
- abort: () => abortController.abort(),
192
- setLoading,
193
- setError,
194
- setErrorCode,
195
- onWarehouseStatus: (status) => {
196
- setWarehouseStatus(status);
197
- publishWarehouseStatus(status);
198
- },
199
- onResult: (message) => setData(message.data),
200
- unpublishWarehouseStatus
201
- };
202
- connectSSE({
90
+ const cacheKey = `${urlSuffix}::${payload}`;
91
+ const subscribe$1 = useCallback((listener) => subscribe(cacheKey, listener), [cacheKey]);
92
+ const getSnapshot$1 = useCallback(() => getSnapshot(cacheKey), [cacheKey]);
93
+ const snapshot = useSyncExternalStore(subscribe$1, getSnapshot$1, getSnapshot$1);
94
+ const start$1 = useCallback(() => start(cacheKey), [cacheKey]);
95
+ useEffect(() => {
96
+ if (payload === null) return;
97
+ return retain(cacheKey, {
203
98
  url: urlSuffix,
204
99
  payload,
205
- signal: abortController.signal,
206
- onMessage: (message) => handleAnalyticsSseMessage(message.data, sseContext),
207
- onError: (error) => handleAnalyticsSseError(error, sseContext)
208
- });
100
+ format
101
+ }, autoStart);
209
102
  }, [
210
- queryKey,
211
- payload,
103
+ cacheKey,
212
104
  urlSuffix,
105
+ payload,
213
106
  format,
214
- publishWarehouseStatus,
215
- unpublishWarehouseStatus
107
+ autoStart
216
108
  ]);
217
109
  useEffect(() => {
218
- if (autoStart) start();
219
- return () => {
220
- abortControllerRef.current?.abort();
221
- unpublishWarehouseStatus();
222
- };
110
+ if (snapshot.loading) publishWarehouseStatus(snapshot.warehouseStatus);
111
+ else unpublishWarehouseStatus();
223
112
  }, [
224
- start,
225
- autoStart,
113
+ snapshot.loading,
114
+ snapshot.warehouseStatus,
115
+ publishWarehouseStatus,
226
116
  unpublishWarehouseStatus
227
117
  ]);
228
- useQueryHMR(queryKey, start);
118
+ useEffect(() => unpublishWarehouseStatus, [unpublishWarehouseStatus]);
119
+ useQueryHMR(queryKey, start$1);
229
120
  return {
230
- data,
231
- loading,
232
- error,
233
- errorCode,
234
- warehouseStatus
121
+ data: snapshot.data,
122
+ loading: snapshot.loading,
123
+ error: payload === null ? "Failed to serialize query parameters" : snapshot.error,
124
+ errorCode: snapshot.errorCode,
125
+ warehouseStatus: snapshot.warehouseStatus
235
126
  };
236
127
  }
237
128
 
@@ -1 +1 @@
1
- {"version":3,"file":"use-analytics-query.js","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { ArrowClient, connectSSE } from \"@/js\";\n\nimport {\n type AnalyticsSseHandlerContext,\n GENERIC_LOAD_ERROR,\n getDevMode,\n handleAnalyticsSseError,\n handleAnalyticsSseMessage,\n userFacingFetchError,\n} from \"./analytics-sse\";\nimport type {\n AnalyticsFormat,\n InferParams,\n InferResultByFormat,\n QueryKey,\n UseAnalyticsQueryOptions,\n UseAnalyticsQueryResult,\n WarehouseStatus,\n} from \"./types\";\nimport { useAnalyticsWarehousePublisher } from \"./use-analytics-warehouse-status\";\nimport { useQueryHMR } from \"./use-query-hmr\";\n\n/** Shallow equality for plain-object query parameters (primitive values only). */\nfunction shallowEqualParams(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (\n a === null ||\n b === null ||\n typeof a !== \"object\" ||\n typeof b !== \"object\"\n ) {\n return false;\n }\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(b, key)) return false;\n if (\n !Object.is(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key],\n )\n ) {\n return false;\n }\n }\n return true;\n}\n\n/** Keep structurally-equal params referentially stable across renders. */\nfunction useStableParams<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!shallowEqualParams(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n\ninterface ArrowDirectContext {\n url: string;\n payload: string;\n signal: AbortSignal;\n setLoading: (loading: boolean) => void;\n setError: (error: string | null) => void;\n setErrorCode: (code: string | null) => void;\n setData: (data: unknown) => void;\n unpublishWarehouseStatus: () => void;\n}\n\n/**\n * Fetch the real column names for a statement from the fallback endpoint,\n * used when a very wide schema's names didn't fit in the response header.\n * Returns undefined on any failure so decoding falls back to the raw Arrow\n * schema names.\n */\nasync function fetchArrowColumns(\n statementId: string,\n signal: AbortSignal,\n): Promise<string[] | undefined> {\n try {\n const res = await fetch(\n `/api/analytics/columns/${encodeURIComponent(statementId)}`,\n { signal },\n );\n if (!res.ok) return undefined;\n const body = (await res.json()) as { columns?: unknown };\n return Array.isArray(body.columns) ? (body.columns as string[]) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Fetch an ARROW_STREAM query result as raw Arrow IPC bytes directly from\n * the query endpoint (no SSE, no second /arrow-result request) and decode\n * it into a Table. The server streams the bytes back as the POST response\n * body; errors before the first byte arrive as a JSON `{ error, errorCode }`.\n */\nasync function fetchArrowDirect(ctx: ArrowDirectContext): Promise<void> {\n try {\n const response = await fetch(ctx.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: ctx.payload,\n signal: ctx.signal,\n });\n if (ctx.signal.aborted) return;\n\n if (!response.ok) {\n let message = GENERIC_LOAD_ERROR;\n let code: string | null = null;\n try {\n const body = (await response.json()) as {\n error?: string;\n errorCode?: string;\n };\n if (body.error) message = body.error;\n if (typeof body.errorCode === \"string\") code = body.errorCode;\n } catch {\n // Non-JSON error body — keep the generic message.\n }\n ctx.setLoading(false);\n ctx.setError(message);\n if (code) ctx.setErrorCode(code);\n ctx.unpublishWarehouseStatus();\n return;\n }\n\n const buffer = await response.arrayBuffer();\n if (ctx.signal.aborted) return;\n // Databricks encodes ARROW_STREAM columns positionally (col_0, …); the\n // server sends the real manifest names so we can relabel the decoded\n // Table (charts look columns up by name). Normally inline in the\n // `X-Appkit-Arrow-Columns` header; for very wide schemas the header\n // carries only a statement-id reference and we fetch the names.\n let columnNames: string[] | undefined;\n const header = response.headers.get(\"X-Appkit-Arrow-Columns\");\n if (header) {\n try {\n columnNames = JSON.parse(decodeURIComponent(header));\n } catch {\n // Malformed header — fall back to the raw Arrow schema names.\n }\n } else {\n const ref = response.headers.get(\"X-Appkit-Arrow-Columns-Ref\");\n if (ref) {\n columnNames = await fetchArrowColumns(ref, ctx.signal);\n }\n }\n const table = await ArrowClient.processArrowBuffer(\n new Uint8Array(buffer),\n columnNames,\n );\n ctx.setData(table);\n ctx.setLoading(false);\n ctx.unpublishWarehouseStatus();\n } catch (error) {\n if (ctx.signal.aborted) return;\n ctx.setLoading(false);\n ctx.unpublishWarehouseStatus();\n ctx.setError(userFacingFetchError(error));\n }\n}\n\n/**\n * Subscribe to an analytics query and return its latest result. JSON_ARRAY\n * results stream over SSE (with warehouse-readiness progress); ARROW_STREAM\n * results are fetched as raw Arrow bytes directly from the query endpoint.\n * Integration hook between client and analytics plugin.\n *\n * The return type is automatically inferred based on the format:\n * - `format: \"JSON_ARRAY\"` (default): Returns typed array from QueryRegistry\n * - `format: \"ARROW_STREAM\"`: Returns TypedArrowTable with row type preserved\n *\n * Note: User context execution is determined by query file naming:\n * - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)\n * - `queryKey.sql`: Executes as service principal\n *\n * @param queryKey - Analytics query identifier\n * @param parameters - Query parameters (type-safe based on QueryRegistry)\n * @param options - Analytics query settings including format\n * @returns Query result state with format-appropriate data type\n *\n * @example JSON_ARRAY format (default)\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params);\n * // data: Array<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n *\n * @example ARROW_STREAM format\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params, { format: \"ARROW_STREAM\" });\n * // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n */\nexport function useAnalyticsQuery<\n T = unknown,\n K extends QueryKey = QueryKey,\n F extends AnalyticsFormat = \"JSON_ARRAY\",\n>(\n queryKey: K,\n parameters?: InferParams<K> | null,\n options: UseAnalyticsQueryOptions<F> = {} as UseAnalyticsQueryOptions<F>,\n): UseAnalyticsQueryResult<InferResultByFormat<T, K, F>> {\n const format = options?.format ?? \"JSON_ARRAY\";\n const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;\n const autoStart = options?.autoStart ?? true;\n\n const devMode = getDevMode();\n const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;\n\n type ResultType = InferResultByFormat<T, K, F>;\n const [data, setData] = useState<ResultType | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [errorCode, setErrorCode] = useState<string | null>(null);\n const [warehouseStatus, setWarehouseStatus] =\n useState<WarehouseStatus | null>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const publisherId = useId();\n const {\n publish: publishWarehouseStatus,\n unpublish: unpublishWarehouseStatus,\n } = useAnalyticsWarehousePublisher(publisherId, queryKey);\n\n if (!queryKey || queryKey.trim().length === 0) {\n throw new Error(\n \"useAnalyticsQuery: 'queryKey' must be a non-empty string.\",\n );\n }\n\n const stableParameters = useStableParams(parameters);\n\n const payload = useMemo(() => {\n try {\n const serialized = JSON.stringify({\n parameters: stableParameters,\n format,\n });\n const sizeInBytes = new Blob([serialized]).size;\n if (sizeInBytes > maxParametersSize) {\n throw new Error(\n \"useAnalyticsQuery: Parameters size exceeds the maximum allowed size\",\n );\n }\n\n return serialized;\n } catch (error) {\n console.error(\"useAnalyticsQuery: Failed to serialize parameters\", error);\n return null;\n }\n }, [stableParameters, format, maxParametersSize]);\n\n const start = useCallback(() => {\n if (payload === null) {\n setError(\"Failed to serialize query parameters\");\n return;\n }\n\n abortControllerRef.current?.abort();\n\n setLoading(true);\n setError(null);\n setErrorCode(null);\n setData(null);\n setWarehouseStatus(null);\n publishWarehouseStatus(null);\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n // ARROW_STREAM: the server streams raw Arrow IPC bytes back on the query\n // response body (no SSE). Fetch and decode directly.\n if (format === \"ARROW_STREAM\") {\n void fetchArrowDirect({\n url: urlSuffix,\n payload,\n signal: abortController.signal,\n setLoading,\n setError,\n setErrorCode,\n setData: (table) => setData(table as ResultType),\n unpublishWarehouseStatus,\n });\n return;\n }\n\n const sseContext: AnalyticsSseHandlerContext = {\n source: \"useAnalyticsQuery\",\n resource: { queryKey },\n defaultExecutionError: \"Unable to execute query\",\n unpublishOnMalformedMessage: false,\n signal: abortController.signal,\n abort: () => abortController.abort(),\n setLoading,\n setError,\n setErrorCode,\n onWarehouseStatus: (status) => {\n setWarehouseStatus(status);\n publishWarehouseStatus(status);\n },\n onResult: (message) => setData(message.data as ResultType),\n unpublishWarehouseStatus,\n };\n\n connectSSE({\n url: urlSuffix,\n payload,\n signal: abortController.signal,\n onMessage: (message) =>\n handleAnalyticsSseMessage(message.data, sseContext),\n onError: (error) => handleAnalyticsSseError(error, sseContext),\n });\n }, [\n queryKey,\n payload,\n urlSuffix,\n format,\n publishWarehouseStatus,\n unpublishWarehouseStatus,\n ]);\n\n useEffect(() => {\n if (autoStart) {\n start();\n }\n\n return () => {\n abortControllerRef.current?.abort();\n unpublishWarehouseStatus();\n };\n }, [start, autoStart, unpublishWarehouseStatus]);\n\n useQueryHMR(queryKey, start);\n\n return { data, loading, error, errorCode, warehouseStatus };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-analytics-query.js","names":["subscribe","store.subscribe","getSnapshot","store.getSnapshot","start","store.start","store.retain"],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\n\nimport * as store from \"./analytics-request-store\";\nimport { getDevMode } from \"./analytics-sse\";\nimport type {\n AnalyticsFormat,\n InferParams,\n InferResultByFormat,\n QueryKey,\n UseAnalyticsQueryOptions,\n UseAnalyticsQueryResult,\n} from \"./types\";\nimport { useAnalyticsWarehousePublisher } from \"./use-analytics-warehouse-status\";\nimport { useQueryHMR } from \"./use-query-hmr\";\n\n/** Shallow equality for plain-object query parameters (primitive values only). */\nfunction shallowEqualParams(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (\n a === null ||\n b === null ||\n typeof a !== \"object\" ||\n typeof b !== \"object\"\n ) {\n return false;\n }\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(b, key)) return false;\n if (\n !Object.is(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key],\n )\n ) {\n return false;\n }\n }\n return true;\n}\n\n/** Keep structurally-equal params referentially stable across renders. */\nfunction useStableParams<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!shallowEqualParams(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n\n/**\n * Subscribe to an analytics query and return its latest result. JSON_ARRAY\n * results stream over SSE (with warehouse-readiness progress); ARROW_STREAM\n * results are fetched as raw Arrow bytes directly from the query endpoint.\n * Integration hook between client and analytics plugin.\n *\n * Identical requests (same query key, parameters, format, and dev mode) share\n * a single in-flight network request: the first mounting instance starts it,\n * later instances subscribe to the same {@link store} entry and see the same\n * result and warehouse-status updates. The request is torn down once its last\n * subscriber unmounts.\n *\n * The return type is automatically inferred based on the format:\n * - `format: \"JSON_ARRAY\"` (default): Returns typed array from QueryRegistry\n * - `format: \"ARROW_STREAM\"`: Returns TypedArrowTable with row type preserved\n *\n * Note: User context execution is determined by query file naming:\n * - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)\n * - `queryKey.sql`: Executes as service principal\n *\n * @param queryKey - Analytics query identifier\n * @param parameters - Query parameters (type-safe based on QueryRegistry)\n * @param options - Analytics query settings including format\n * @returns Query result state with format-appropriate data type\n *\n * @example JSON_ARRAY format (default)\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params);\n * // data: Array<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n *\n * @example ARROW_STREAM format\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params, { format: \"ARROW_STREAM\" });\n * // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n */\nexport function useAnalyticsQuery<\n T = unknown,\n K extends QueryKey = QueryKey,\n F extends AnalyticsFormat = \"JSON_ARRAY\",\n>(\n queryKey: K,\n parameters?: InferParams<K> | null,\n options: UseAnalyticsQueryOptions<F> = {} as UseAnalyticsQueryOptions<F>,\n): UseAnalyticsQueryResult<InferResultByFormat<T, K, F>> {\n const format = options?.format ?? \"JSON_ARRAY\";\n const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;\n const autoStart = options?.autoStart ?? true;\n\n const devMode = getDevMode();\n const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;\n\n type ResultType = InferResultByFormat<T, K, F>;\n\n const publisherId = useId();\n const {\n publish: publishWarehouseStatus,\n unpublish: unpublishWarehouseStatus,\n } = useAnalyticsWarehousePublisher(publisherId, queryKey);\n\n if (!queryKey || queryKey.trim().length === 0) {\n throw new Error(\n \"useAnalyticsQuery: 'queryKey' must be a non-empty string.\",\n );\n }\n\n const stableParameters = useStableParams(parameters);\n\n const payload = useMemo(() => {\n try {\n const serialized = JSON.stringify({\n parameters: stableParameters,\n format,\n });\n const sizeInBytes = new Blob([serialized]).size;\n if (sizeInBytes > maxParametersSize) {\n throw new Error(\n \"useAnalyticsQuery: Parameters size exceeds the maximum allowed size\",\n );\n }\n\n return serialized;\n } catch (error) {\n console.error(\"useAnalyticsQuery: Failed to serialize parameters\", error);\n return null;\n }\n }, [stableParameters, format, maxParametersSize]);\n\n // Cache key shared across hook instances. `payload` already serializes\n // `{ parameters, format }`, so identical requests collapse to one key.\n // On a serialization failure (`payload === null`) the key stays unused: no\n // request is retained and the store reports the stable idle snapshot.\n const cacheKey = `${urlSuffix}::${payload}`;\n\n const subscribe = useCallback(\n (listener: () => void) => store.subscribe(cacheKey, listener),\n [cacheKey],\n );\n const getSnapshot = useCallback(\n () => store.getSnapshot(cacheKey),\n [cacheKey],\n );\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n const start = useCallback(() => store.start(cacheKey), [cacheKey]);\n\n // Register with the shared store on mount / key change; release on cleanup.\n // The store starts the request on first retain of a key and reuses the\n // in-flight request for later subscribers.\n useEffect(() => {\n if (payload === null) return;\n return store.retain(\n cacheKey,\n { url: urlSuffix, payload, format },\n autoStart,\n );\n }, [cacheKey, urlSuffix, payload, format, autoStart]);\n\n // Mirror this instance's warehouse status into the nearest resource-status\n // provider while the request is in flight; clear the slot once it settles.\n useEffect(() => {\n if (snapshot.loading) {\n publishWarehouseStatus(snapshot.warehouseStatus);\n } else {\n unpublishWarehouseStatus();\n }\n }, [\n snapshot.loading,\n snapshot.warehouseStatus,\n publishWarehouseStatus,\n unpublishWarehouseStatus,\n ]);\n\n useEffect(() => unpublishWarehouseStatus, [unpublishWarehouseStatus]);\n\n useQueryHMR(queryKey, start);\n\n return {\n data: snapshot.data as ResultType | null,\n loading: snapshot.loading,\n // A serialization failure never creates a store entry, so surface it here.\n error:\n payload === null\n ? \"Failed to serialize query parameters\"\n : snapshot.error,\n errorCode: snapshot.errorCode,\n warehouseStatus: snapshot.warehouseStatus,\n };\n}\n"],"mappings":";;;;;;;;AAuBA,SAAS,mBAAmB,GAAY,GAAqB;AAC3D,KAAI,OAAO,GAAG,GAAG,EAAE,CAAE,QAAO;AAC5B,KACE,MAAM,QACN,MAAM,QACN,OAAO,MAAM,YACb,OAAO,MAAM,SAEb,QAAO;CAET,MAAM,QAAQ,OAAO,KAAK,EAA6B;CACvD,MAAM,QAAQ,OAAO,KAAK,EAA6B;AACvD,KAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1C,MAAK,MAAM,OAAO,OAAO;AACvB,MAAI,CAAC,OAAO,OAAO,GAAG,IAAI,CAAE,QAAO;AACnC,MACE,CAAC,OAAO,GACL,EAA8B,MAC9B,EAA8B,KAChC,CAED,QAAO;;AAGX,QAAO;;;AAIT,SAAS,gBAAmB,OAAa;CACvC,MAAM,MAAM,OAAU,MAAM;AAC5B,KAAI,CAAC,mBAAmB,IAAI,SAAS,MAAM,CACzC,KAAI,UAAU;AAEhB,QAAO,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCb,SAAgB,kBAKd,UACA,YACA,UAAuC,EAAE,EACc;CACvD,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,oBAAoB,SAAS,qBAAqB,MAAM;CAC9D,MAAM,YAAY,SAAS,aAAa;CAExC,MAAM,UAAU,YAAY;CAC5B,MAAM,YAAY,wBAAwB,mBAAmB,SAAS,GAAG;CAKzE,MAAM,EACJ,SAAS,wBACT,WAAW,6BACT,+BAJgB,OAAO,EAIqB,SAAS;AAEzD,KAAI,CAAC,YAAY,SAAS,MAAM,CAAC,WAAW,EAC1C,OAAM,IAAI,MACR,4DACD;CAGH,MAAM,mBAAmB,gBAAgB,WAAW;CAEpD,MAAM,UAAU,cAAc;AAC5B,MAAI;GACF,MAAM,aAAa,KAAK,UAAU;IAChC,YAAY;IACZ;IACD,CAAC;AAEF,OADoB,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,OACzB,kBAChB,OAAM,IAAI,MACR,sEACD;AAGH,UAAO;WACA,OAAO;AACd,WAAQ,MAAM,qDAAqD,MAAM;AACzE,UAAO;;IAER;EAAC;EAAkB;EAAQ;EAAkB,CAAC;CAMjD,MAAM,WAAW,GAAG,UAAU,IAAI;CAElC,MAAMA,cAAY,aACf,aAAyBC,UAAgB,UAAU,SAAS,EAC7D,CAAC,SAAS,CACX;CACD,MAAMC,gBAAc,kBACZC,YAAkB,SAAS,EACjC,CAAC,SAAS,CACX;CACD,MAAM,WAAW,qBAAqBH,aAAWE,eAAaA,cAAY;CAE1E,MAAME,UAAQ,kBAAkBC,MAAY,SAAS,EAAE,CAAC,SAAS,CAAC;AAKlE,iBAAgB;AACd,MAAI,YAAY,KAAM;AACtB,SAAOC,OACL,UACA;GAAE,KAAK;GAAW;GAAS;GAAQ,EACnC,UACD;IACA;EAAC;EAAU;EAAW;EAAS;EAAQ;EAAU,CAAC;AAIrD,iBAAgB;AACd,MAAI,SAAS,QACX,wBAAuB,SAAS,gBAAgB;MAEhD,2BAA0B;IAE3B;EACD,SAAS;EACT,SAAS;EACT;EACA;EACD,CAAC;AAEF,iBAAgB,0BAA0B,CAAC,yBAAyB,CAAC;AAErE,aAAY,UAAUF,QAAM;AAE5B,QAAO;EACL,MAAM,SAAS;EACf,SAAS,SAAS;EAElB,OACE,YAAY,OACR,yCACA,SAAS;EACf,WAAW,SAAS;EACpB,iBAAiB,SAAS;EAC3B"}
@@ -6,7 +6,7 @@ import * as class_variance_authority_types0 from "class-variance-authority/types
6
6
  //#region src/react/ui/button.d.ts
7
7
  declare const buttonVariants: (props?: ({
8
8
  variant?: "default" | "destructive" | "secondary" | "outline" | "ghost" | "link" | null | undefined;
9
- size?: "default" | "sm" | "lg" | "icon" | "icon-sm" | "icon-lg" | null | undefined;
9
+ size?: "default" | "sm" | "icon-sm" | "lg" | "icon" | "icon-lg" | null | undefined;
10
10
  } & class_variance_authority_types0.ClassProp) | undefined) => string;
11
11
  /** Clickable button with multiple variants and sizes */
12
12
  declare function Button({
@@ -20,7 +20,7 @@ declare function InputGroupAddon({
20
20
  ...props
21
21
  }: React$1.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>): react_jsx_runtime0.JSX.Element;
22
22
  declare const inputGroupButtonVariants: (props?: ({
23
- size?: "sm" | "icon-sm" | "xs" | "icon-xs" | null | undefined;
23
+ size?: "xs" | "sm" | "icon-xs" | "icon-sm" | null | undefined;
24
24
  } & class_variance_authority_types0.ClassProp) | undefined) => string;
25
25
  /** Button integrated within an input group */
26
26
  declare function InputGroupButton({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@databricks/appkit-ui",
3
3
  "type": "module",
4
- "version": "0.62.0",
4
+ "version": "0.63.0",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": [
7
7
  "**/*.css"