metric individual subscriber

This commit is contained in:
Nate Kelley 2025-02-15 18:22:43 -07:00
parent bb912b28ca
commit 5b4da9fc4a
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 115 additions and 230 deletions

View File

@ -4,15 +4,19 @@ import { useMemoizedFn } from 'ahooks';
import { getMetric, getMetric_server, listMetrics, listMetrics_server } from './requests';
import type { GetMetricParams, ListMetricsParams } from './interfaces';
import { BusterMetric, BusterMetricListItem } from '@/api/asset_interfaces';
import { IBusterMetric } from '@/context/Metrics';
import { upgradeMetricToIMetric } from '@/context/Metrics/helpers';
export const useGetMetric = (params: GetMetricParams) => {
const queryFn = useMemoizedFn(() => {
return getMetric(params);
const queryFn = useMemoizedFn(async () => {
const result = await getMetric(params);
return upgradeMetricToIMetric(result, null);
});
return useCreateReactQuery<BusterMetric>({
return useCreateReactQuery<IBusterMetric>({
queryKey: ['metric', params],
queryFn
queryFn,
enabled: false //this is handle via a socket query? maybe it should not be?
});
};

View File

@ -20,6 +20,7 @@ import type {
InferBusterSocketResponseData
} from './types';
import isEmpty from 'lodash/isEmpty';
import { RustApiError } from '../buster_rest/errors';
/**
* A custom hook that combines WebSocket communication with React Query's mutation capabilities.
@ -122,6 +123,9 @@ export function useSocketQueryMutation<
} as BusterSocketRequest,
responseEvent: {
route: socketResponse,
onError: (error: RustApiError) => {
throw error;
},
callback: (d: unknown) => d
} as BusterSocketResponse
});
@ -144,6 +148,7 @@ export function useSocketQueryMutation<
);
return useMutation<TData, TError, TPayload>({
mutationFn
mutationFn,
throwOnError: true
});
}

View File

@ -2,9 +2,10 @@ import { queryOptions } from '@tanstack/react-query';
import type { BusterMetric, BusterMetricListItem } from '@/api/asset_interfaces';
import type { MetricListRequest } from '@/api/request_interfaces/metrics';
import type { BusterMetricData } from '@/context/MetricData';
import { IBusterMetric } from '@/context/Metrics';
export const metricsGetMetric = (metricId: string) =>
queryOptions<BusterMetric>({
queryOptions<IBusterMetric>({
queryKey: ['metrics', 'get', metricId] as const,
staleTime: 10 * 1000,
enabled: false

View File

@ -1,4 +1,4 @@
import React, { PropsWithChildren, useRef } from 'react';
import React, { PropsWithChildren } from 'react';
import {
createContext,
ContextSelector,
@ -12,73 +12,44 @@ import { resolveEmptyMetric, upgradeMetricToIMetric } from '../helpers';
import { useUpdateMetricConfig } from './useMetricUpdateConfig';
import { useUpdateMetricAssosciations } from './useMetricUpdateAssosciations';
import { useShareMetric } from './useMetricShare';
import { useMetricSubscribe } from './useMetricSubscribe';
import { useParams } from 'next/navigation';
import { useMetricDataIndividual } from '@/context/MetricData/useMetricDataIndividual';
import { useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/api/query_keys';
export const useBusterMetricsIndividual = () => {
const [isPending, startTransition] = useTransition();
const { metricId: selectedMetricId } = useParams<{ metricId: string }>();
const metricsRef = useRef<Record<string, IBusterMetric>>({});
const queryClient = useQueryClient();
const getMetricId = useMemoizedFn((metricId?: string): string => {
return metricId || selectedMetricId;
});
const setMetrics = useMemoizedFn((newMetrics: Record<string, IBusterMetric>) => {
metricsRef.current = { ...metricsRef.current, ...newMetrics };
startTransition(() => {
//trigger a rerender
});
});
const resetMetric = useMemoizedFn(({ metricId }: { metricId: string }) => {
const prev = metricsRef.current;
delete prev[metricId];
setMetrics(prev);
});
//UI SELECTORS
const getMetricMemoized = useMemoizedFn(({ metricId }: { metricId?: string }): IBusterMetric => {
const _metricId = getMetricId(metricId);
const metrics = metricsRef.current || {};
const currentMetric = metrics[_metricId];
return resolveEmptyMetric(currentMetric, _metricId);
const options = queryKeys['/metrics/get:getMetric'](_metricId);
const data = queryClient.getQueryData(options.queryKey);
return resolveEmptyMetric(data, _metricId);
});
//STATE UPDATERS
const onInitializeMetric = useMemoizedFn((newMetric: BusterMetric) => {
const metrics = metricsRef.current || {};
const oldMetric = metrics[newMetric.id] as IBusterMetric | undefined; //HMMM is this right?
const oldMetric = getMetricMemoized({ metricId: newMetric.id });
const upgradedMetric = upgradeMetricToIMetric(newMetric, oldMetric);
metricUpdateConfig.onUpdateMetric(upgradedMetric, false);
});
// EMITTERS
const metricSubscribe = useMetricSubscribe({
metricsRef,
setMetrics,
onInitializeMetric
});
const metricShare = useShareMetric({ onInitializeMetric });
const metricAssosciations = useUpdateMetricAssosciations({
metricsRef,
setMetrics,
getMetricMemoized
});
const metricAssosciations = useUpdateMetricAssosciations({ getMetricMemoized });
const metricUpdateConfig = useUpdateMetricConfig({
getMetricId,
setMetrics,
startTransition,
onInitializeMetric,
getMetricMemoized
});
@ -87,11 +58,8 @@ export const useBusterMetricsIndividual = () => {
...metricAssosciations,
...metricShare,
...metricUpdateConfig,
...metricSubscribe,
resetMetric,
onInitializeMetric,
getMetricMemoized,
metrics: metricsRef.current
getMetricMemoized
};
};
@ -115,21 +83,3 @@ export const useBusterMetricsIndividualContextSelector = <T,>(
) => {
return useContextSelector(BusterMetricsIndividual, selector);
};
export const useBusterMetricIndividual = ({ metricId }: { metricId: string }) => {
const subscribeToMetric = useBusterMetricsIndividualContextSelector((x) => x.subscribeToMetric);
const metric = useBusterMetricsIndividualContextSelector((x) => x.metrics[metricId]);
const metricIndividualData = useMetricDataIndividual({
metricId
});
useMount(() => {
subscribeToMetric({ metricId });
});
return {
metric: resolveEmptyMetric(metric, metricId),
...metricIndividualData
};
};

View File

@ -0,0 +1,51 @@
import { useMetricDataIndividual } from '@/context/MetricData';
import { useBusterMetricsIndividualContextSelector } from './BusterMetricsIndividualProvider';
import { useSocketQueryEmitOn } from '@/api/buster_socket_query';
import { queryKeys } from '@/api/query_keys';
import { resolveEmptyMetric, upgradeMetricToIMetric } from '../helpers';
import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider';
import { useEffect } from 'react';
export const useBusterMetricIndividual = ({ metricId }: { metricId: string }) => {
const onInitializeMetric = useBusterMetricsIndividualContextSelector((x) => x.onInitializeMetric);
const getMetricMemoized = useBusterMetricsIndividualContextSelector((x) => x.getMetricMemoized);
const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword);
const setAssetPasswordError = useBusterAssetsContextSelector(
(state) => state.setAssetPasswordError
);
const assetPassword = getAssetPassword(metricId);
const {
data: metric,
refetch: refetchMetric,
isFetched: isMetricFetched,
error: metricError
} = useSocketQueryEmitOn(
{ route: '/metrics/get', payload: { id: metricId, password: assetPassword.password } },
'/metrics/get:updateMetricState',
queryKeys['/metrics/get:getMetric'](metricId),
(currentData, newData) => {
return upgradeMetricToIMetric(newData, currentData);
}
);
const metricIndividualData = useMetricDataIndividual({
metricId
});
useEffect(() => {
if (metricError) {
setAssetPasswordError(metricId, metricError.message || 'An error occurred');
} else {
setAssetPasswordError(metricId, null);
}
}, [metricError]);
return {
metric: resolveEmptyMetric(metric, metricId),
isMetricFetched,
refetchMetric,
...metricIndividualData
};
};

View File

@ -1,9 +0,0 @@
import { useBusterMetricsIndividualContextSelector } from './BusterMetricsIndividualProvider';
export const useMetricFetched = ({ metricId }: { metricId: string }) => {
const fetched = useBusterMetricsIndividualContextSelector((x) => x.metrics[metricId]?.fetched);
const fetching = useBusterMetricsIndividualContextSelector((x) => x.metrics[metricId]?.fetching);
const error = useBusterMetricsIndividualContextSelector((x) => x.metrics[metricId]?.error);
return { fetched, fetching, error };
};

View File

@ -1,90 +0,0 @@
import { useMemoizedFn } from 'ahooks';
import { useBusterAssetsContextSelector } from '../../Assets/BusterAssetsProvider';
import { IBusterMetric } from '../interfaces';
import { useBusterWebSocket } from '../../BusterWebSocket';
import { BusterMetric } from '@/api/asset_interfaces';
import { RustApiError } from '@/api/buster_rest/errors';
import { resolveEmptyMetric } from '../helpers';
import React from 'react';
export const useMetricSubscribe = ({
metricsRef,
onInitializeMetric,
setMetrics
}: {
metricsRef: React.MutableRefObject<Record<string, IBusterMetric>>;
onInitializeMetric: (metric: BusterMetric) => void;
setMetrics: (newMetrics: Record<string, IBusterMetric>) => void;
}) => {
const busterSocket = useBusterWebSocket();
const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword);
const setAssetPasswordError = useBusterAssetsContextSelector(
(state) => state.setAssetPasswordError
);
const _onGetMetricState = useMemoizedFn((metric: BusterMetric) => {
onInitializeMetric(metric);
});
const _onGetMetricStateError = useMemoizedFn((_error: any, metricId: string) => {
const error = _error as RustApiError;
setAssetPasswordError(metricId, error.message || 'An error occurred');
});
const _setLoadingMetric = useMemoizedFn((metricId: string) => {
const metrics = metricsRef.current || {};
metrics[metricId] = resolveEmptyMetric(
{
...metrics[metricId],
fetching: true
},
metricId
);
setMetrics(metrics);
return metrics[metricId];
});
const subscribeToMetric = useMemoizedFn(async ({ metricId }: { metricId: string }) => {
const { password } = getAssetPassword(metricId);
const foundMetric: undefined | IBusterMetric = metricsRef.current[metricId];
if (foundMetric && (foundMetric?.fetching || foundMetric?.fetched)) {
return foundMetric;
}
_setLoadingMetric(metricId);
return await busterSocket.emitAndOnce({
emitEvent: {
route: '/metrics/get',
payload: {
id: metricId,
password
}
},
responseEvent: {
route: '/metrics/get:updateMetricState',
callback: _onGetMetricState,
onError: (error) => _onGetMetricStateError(error, metricId)
}
});
});
const unsubscribeToMetricEvents = useMemoizedFn(({ metricId }: { metricId: string }) => {
busterSocket.off({
route: '/metrics/get:updateMetricState',
callback: onInitializeMetric
});
busterSocket.off({
route: '/metrics/update:updateMetricState',
callback: onInitializeMetric
});
});
return {
unsubscribeToMetricEvents,
subscribeToMetric
};
};

View File

@ -8,12 +8,8 @@ import { useBusterNotifications } from '../../BusterNotifications';
import { useBusterDashboardContextSelector } from '../../Dashboards';
export const useUpdateMetricAssosciations = ({
metricsRef,
setMetrics,
getMetricMemoized
}: {
metricsRef: MutableRefObject<Record<string, IBusterMetric>>;
setMetrics: (metrics: Record<string, IBusterMetric>) => void;
getMetricMemoized: ({ metricId }: { metricId?: string }) => IBusterMetric;
}) => {
const busterSocket = useBusterWebSocket();

View File

@ -8,24 +8,29 @@ import {
VerificationStatus
} from '@/api/asset_interfaces';
import { prepareMetricUpdateMetric } from '../helpers';
import { MetricUpdateMetric } from '@/api/buster_socket/metrics';
import { ColumnSettings, IColumnLabelFormat } from '@/components/charts';
import { useBusterWebSocket } from '../../BusterWebSocket';
import { useTransition } from 'react';
import { queryKeys } from '@/api/query_keys';
import { useQueryClient } from '@tanstack/react-query';
import { useSocketQueryMutation } from '@/api/buster_socket_query';
export const useUpdateMetricConfig = ({
getMetricId,
getMetricMemoized,
setMetrics,
startTransition,
onInitializeMetric
}: {
getMetricMemoized: ({ metricId }: { metricId?: string }) => IBusterMetric;
onInitializeMetric: (metric: BusterMetric) => void;
getMetricId: (metricId?: string) => string;
setMetrics: (metrics: Record<string, IBusterMetric>) => void;
startTransition: (fn: () => void) => void;
}) => {
const busterSocket = useBusterWebSocket();
const [isPending, startTransition] = useTransition();
const queryClient = useQueryClient();
const setMetricToState = useMemoizedFn((metric: IBusterMetric) => {
const metricId = getMetricId(metric.id);
const options = queryKeys['/metrics/get:getMetric'](metricId);
queryClient.setQueryData(options.queryKey, metric);
});
const onUpdateMetric = useMemoizedFn(
async (newMetricPartial: Partial<IBusterMetric>, saveToServer: boolean = true) => {
@ -35,10 +40,7 @@ export const useUpdateMetricConfig = ({
...currentMetric,
...newMetricPartial
};
setMetrics({
[metricId]: newMetric
});
setMetricToState(newMetric);
//This will trigger a rerender and push prepareMetricUpdateMetric off UI metric
startTransition(() => {
const isReadyOnly = currentMetric.permission === ShareRole.VIEWER;
@ -46,48 +48,39 @@ export const useUpdateMetricConfig = ({
_prepareMetricAndSaveToServer(newMetric, currentMetric);
}
});
return newMetric;
}
);
const _CheckUpdateMetric = useMemoizedFn((metric: BusterMetric) => {
const draftSessionId = metric.draft_session_id;
const currentMessage = getMetricMemoized({ metricId: metric.id });
if (draftSessionId && !currentMessage?.draft_session_id) {
onUpdateMetric({
id: metric.id,
draft_session_id: draftSessionId
});
}
return metric;
});
const updateMetricToServer = useMemoizedFn((payload: MetricUpdateMetric['payload']) => {
return busterSocket.emitAndOnce({
emitEvent: {
route: '/metrics/update',
payload
},
responseEvent: {
route: '/metrics/update:updateMetricState',
callback: _CheckUpdateMetric
const { mutateAsync: updateMetricToServer } = useSocketQueryMutation(
'/metrics/update',
'/metrics/update:updateMetricState',
null,
null,
(metric, currentData, variables) => {
const draftSessionId = metric.draft_session_id;
const currentMessage = getMetricMemoized({ metricId: metric.id });
if (draftSessionId && !currentMessage?.draft_session_id) {
onUpdateMetric(
{
id: metric.id,
draft_session_id: draftSessionId
},
false
);
}
});
});
const { run: _updateMetricToServer } = useDebounceFn(updateMetricToServer, {
wait: 300
});
return metric;
}
);
const { run: _prepareMetricAndSaveToServer } = useDebounceFn(
useMemoizedFn((newMetric: IBusterMetric, oldMetric: IBusterMetric) => {
const changedValues = prepareMetricUpdateMetric(newMetric, oldMetric);
if (changedValues) {
_updateMetricToServer(changedValues);
updateMetricToServer(changedValues);
}
}),
{ wait: 700 }
{ wait: 750 }
);
const onUpdateMetricChartConfig = useMemoizedFn(
@ -185,26 +178,10 @@ export const useUpdateMetricConfig = ({
);
const onSaveMetricChanges = useMemoizedFn(
async ({
metricId,
...params
}: {
metricId: string;
save_draft: boolean;
save_as_metric_state?: string;
}) => {
return busterSocket.emitAndOnce({
emitEvent: {
route: '/metrics/update',
payload: {
id: metricId,
...params
}
},
responseEvent: {
route: '/metrics/update:updateMetricState',
callback: onInitializeMetric
}
async (params: { metricId: string; save_draft: boolean; save_as_metric_state?: string }) => {
return updateMetricToServer({
id: params.metricId,
...params
});
}
);