"use client";
import type { ReactNode } from "react";
import { useEffect, useMemo, useState } from "react";
import { Alert, Box, Button, Code, Skeleton, Text } from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
import { useCustomWidgetRuntime } from "./context";
import { formatDisplayValue, getByPath, normalizeParams, SubFetchDataContext } from "./data";
import { RequestIdRequiredAlert } from "./status";
import type { CustomJsxRuntimeParams } from "./types";
export interface SubFetchMetadata {
ok: boolean;
status: number;
statusText?: string;
loading: false;
error?: undefined;
}
export interface SubFetchProps {
requestId?: string;
params?: CustomJsxRuntimeParams;
refreshInterval?: number;
children?: ReactNode | ((data: unknown, metadata: SubFetchMetadata) => ReactNode);
loadingLabel?: string;
errorMessage?: string;
fallback?: ReactNode;
triggerContent?: ReactNode;
triggerAriaLabel?: string;
path?: string;
as?: "json" | "text";
trigger?: "auto" | "manual";
}
// Browser timers use signed 32-bit millisecond delays. Whole-second refresh
// intervals are capped at the largest value that remains below that boundary.
export const MAX_REFRESH_INTERVAL_SECONDS = 2_147_483;
export const MAX_REFRESH_INTERVAL_MS = MAX_REFRESH_INTERVAL_SECONDS * 1_000;
export function SubFetch(props: SubFetchProps) {
const { itemId, previewSessionId, queryCacheKey, queriesDisabled, port, messages, setQueryState } =
useCustomWidgetRuntime();
const [manualRun, setManualRun] = useState(false);
const params = useMemo(() => normalizeParams(props.params), [props.params]);
const paramsKey = useMemo(() => JSON.stringify(params), [params]);
useEffect(() => setManualRun(false), [paramsKey, props.requestId, props.trigger]);
const enabled = Boolean(
!queriesDisabled &&
(itemId || previewSessionId) &&
props.requestId &&
params &&
(props.trigger !== "manual" || manualRun),
);
const refreshMs = normalizeRefreshInterval(props.refreshInterval);
const scope = itemId ? "item" : "preview";
const scopeId = itemId ?? previewSessionId;
const query = useQuery({
queryKey: ["custom-widget", scope, scopeId, props.requestId, paramsKey, queryCacheKey],
queryFn: ({ signal }) =>
port.query({ itemId, previewSessionId, requestId: props.requestId ?? "", params: params ?? {} }, signal),
enabled,
retry: 2,
refetchInterval: refreshMs,
});
useEffect(() => {
// Manual queries are local to their SubFetch instance. Publishing them by request ID makes
// repeated cards compete for the same data slot and can remount the active query mid-request.
if (!props.requestId || !setQueryState || props.trigger === "manual") return;
if (!enabled) {
setQueryState(props.requestId, null);
return;
}
const result = query.data;
setQueryState(props.requestId, {
data: result?.data ?? null,
status: {
loading: query.isFetching && !result,
ok: result?.ok,
status: result?.status,
statusText: result?.statusText,
error: result?.error ?? (query.error ? messages.requestFailed : undefined),
},
});
}, [
enabled,
messages.requestFailed,
props.requestId,
props.trigger,
query.data,
query.error,
query.isFetching,
setQueryState,
]);
if (!props.requestId) return
{JSON.stringify(value, null, 2)}
);
}
function Loading({ label }: { label: string }) {
return (