Add SSE for metadata and plug front components (#17998)
Create the necessary tooling to listen to metadata events and plug it to the front components. Now we have a hot reload like experience when we edit a component in an app. ## Backend - Split `EventWithQueryIds` into `ObjectRecordEventWithQueryIds` and `MetadataEventWithQueryIds` - Publish metadata event batches to active SSE streams in `MetadataEventsToDbListener` ## Frontend - Create a metadata event dispatching pipeline: SSE metadata events are grouped by metadata name, transformed into `MetadataOperationBrowserEventDetail` objects, and dispatched as browser `CustomEvents` - Add `useListenToMetadataOperationBrowserEvent` hook for consuming metadata operation events filtered by metadata name and operation type - Rename `useListenToObjectRecordEventsForQuery` to `useListenToEventsForQuery`, now accepting both `RecordGqlOperationSignature` and `MetadataGqlOperationSignature` - Implement `useOnFrontComponentUpdated` which subscribes to front component metadata events and updates the Apollo cache when the component is modified - Add `builtComponentChecksum` to the front component query and appends it to the component URL for browser cache invalidation
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
export const METADATA_OPERATION_BROWSER_EVENT_NAME =
|
||||
'metadata-operation-browser-event';
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { METADATA_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/MetadataOperationBrowserEventName';
|
||||
import { type MetadataOperation } from '@/browser-event/types/MetadataOperation';
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useEffect } from 'react';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
export const useListenToMetadataOperationBrowserEvent = <
|
||||
T extends Record<string, unknown>,
|
||||
>({
|
||||
onMetadataOperationBrowserEvent,
|
||||
metadataName,
|
||||
operationTypes,
|
||||
}: {
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<T>,
|
||||
) => void;
|
||||
metadataName?: AllMetadataName;
|
||||
operationTypes?: MetadataOperation<T>['type'][];
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const handleMetadataOperationEvent = (
|
||||
event: CustomEvent<MetadataOperationBrowserEventDetail<T>>,
|
||||
) => {
|
||||
const detail = event.detail;
|
||||
|
||||
if (isDefined(metadataName) && detail.metadataName !== metadataName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isNonEmptyArray(operationTypes) &&
|
||||
!operationTypes.includes(detail.operation.type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onMetadataOperationBrowserEvent(detail);
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
METADATA_OPERATION_BROWSER_EVENT_NAME,
|
||||
handleMetadataOperationEvent as EventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
METADATA_OPERATION_BROWSER_EVENT_NAME,
|
||||
handleMetadataOperationEvent as EventListener,
|
||||
);
|
||||
};
|
||||
}, [metadataName, onMetadataOperationBrowserEvent, operationTypes]);
|
||||
};
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/object-record/constants/ObjectRecordOperationBrowserEventName';
|
||||
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
|
||||
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export type MetadataOperation<T extends Record<string, unknown>> =
|
||||
| {
|
||||
type: 'create';
|
||||
createdRecord: T;
|
||||
}
|
||||
| {
|
||||
type: 'update';
|
||||
updatedRecord: T;
|
||||
updatedFields?: string[];
|
||||
}
|
||||
| {
|
||||
type: 'delete';
|
||||
deletedRecordId: string;
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type MetadataOperation } from '@/browser-event/types/MetadataOperation';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
export type MetadataOperationBrowserEventDetail<
|
||||
T extends Record<string, unknown>,
|
||||
> = {
|
||||
metadataName: AllMetadataName;
|
||||
operation: MetadataOperation<T>;
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { METADATA_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/MetadataOperationBrowserEventName';
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
|
||||
export const dispatchMetadataOperationBrowserEvent = <
|
||||
T extends Record<string, unknown>,
|
||||
>(
|
||||
detail: MetadataOperationBrowserEventDetail<T>,
|
||||
) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(METADATA_OPERATION_BROWSER_EVENT_NAME, {
|
||||
detail,
|
||||
}),
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/object-record/constants/ObjectRecordOperationBrowserEventName';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
|
||||
export const dispatchObjectRecordOperationBrowserEvent = (
|
||||
detail: ObjectRecordOperationBrowserEventDetail,
|
||||
+11
-3
@@ -1,5 +1,6 @@
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext';
|
||||
import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated';
|
||||
import { getFrontComponentUrl } from '@/front-components/utils/getFrontComponentUrl';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -21,8 +22,6 @@ export const FrontComponentRenderer = ({
|
||||
const { executionContext, frontComponentHostCommunicationApi } =
|
||||
useFrontComponentExecutionContext();
|
||||
|
||||
const componentUrl = `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
|
||||
|
||||
const handleError = useCallback(
|
||||
(error?: Error) => {
|
||||
if (!isDefined(error)) {
|
||||
@@ -43,6 +42,15 @@ export const FrontComponentRenderer = ({
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
useOnFrontComponentUpdated({
|
||||
frontComponentId,
|
||||
});
|
||||
|
||||
const componentUrl = getFrontComponentUrl({
|
||||
frontComponentId,
|
||||
checksum: data?.frontComponent?.builtComponentChecksum,
|
||||
});
|
||||
|
||||
if (
|
||||
loading ||
|
||||
!isDefined(data?.frontComponent) ||
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
|
||||
id
|
||||
name
|
||||
applicationId
|
||||
builtComponentChecksum
|
||||
applicationTokenPair {
|
||||
applicationAccessToken {
|
||||
token
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useUpdateFrontComponentApolloCache } from '@/front-components/hooks/useUpdateFrontComponentApolloCache';
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import {
|
||||
AllMetadataName,
|
||||
type FrontComponent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseOnFrontComponentUpdatedArgs = {
|
||||
frontComponentId: string;
|
||||
};
|
||||
|
||||
export const useOnFrontComponentUpdated = ({
|
||||
frontComponentId,
|
||||
}: UseOnFrontComponentUpdatedArgs) => {
|
||||
const queryId = `front-component-updated-${frontComponentId}`;
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.frontComponent,
|
||||
variables: {
|
||||
filter: { id: { eq: frontComponentId } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { updateFrontComponentApolloCache } =
|
||||
useUpdateFrontComponentApolloCache({
|
||||
frontComponentId,
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent<FrontComponent>({
|
||||
metadataName: AllMetadataName.frontComponent,
|
||||
onMetadataOperationBrowserEvent: updateFrontComponentApolloCache,
|
||||
});
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FindOneFrontComponentDocument,
|
||||
type FindOneFrontComponentQuery,
|
||||
type FrontComponent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseUpdateFrontComponentApolloCacheArgs = {
|
||||
frontComponentId: string;
|
||||
};
|
||||
|
||||
export const useUpdateFrontComponentApolloCache = ({
|
||||
frontComponentId,
|
||||
}: UseUpdateFrontComponentApolloCacheArgs) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const updateFrontComponentApolloCache = (
|
||||
detail: MetadataOperationBrowserEventDetail<FrontComponent>,
|
||||
) => {
|
||||
if (detail.operation.type !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { updatedRecord } = detail.operation;
|
||||
|
||||
if (!isDefined(updatedRecord) || updatedRecord.id !== frontComponentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
apolloClient.cache.updateQuery<FindOneFrontComponentQuery>(
|
||||
{
|
||||
query: FindOneFrontComponentDocument,
|
||||
variables: { id: frontComponentId },
|
||||
},
|
||||
(existingData) => {
|
||||
if (!isDefined(existingData?.frontComponent)) {
|
||||
return existingData;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingData,
|
||||
frontComponent: {
|
||||
...existingData.frontComponent,
|
||||
...updatedRecord,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return { updateFrontComponentApolloCache };
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getFrontComponentUrl = ({
|
||||
frontComponentId,
|
||||
checksum,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
checksum?: string;
|
||||
}): string => {
|
||||
return isDefined(checksum)
|
||||
? `${REST_API_BASE_URL}/front-components/${frontComponentId}?checksum=${checksum}`
|
||||
: `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
|
||||
};
|
||||
+2
-2
@@ -2,11 +2,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
|
||||
import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords';
|
||||
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
|
||||
jest.mock('@/object-record/utils/dispatchObjectRecordOperationBrowserEvent');
|
||||
jest.mock('@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent');
|
||||
jest.mock('@/object-record/hooks/useUpdateManyRecords', () => ({
|
||||
useUpdateManyRecords: jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
@@ -19,7 +19,7 @@ import { type FieldActorForInputValue } from '@/object-record/record-field/ui/ty
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import { type BaseObjectRecord } from '@/object-record/types/BaseObjectRecord';
|
||||
import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
|
||||
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIn
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
|
||||
|
||||
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQue
|
||||
import { useMergeManyRecordsMutation } from '@/object-record/hooks/useMergeManyRecordsMutation';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getMergeManyRecordsMutationResponseField } from '@/object-record/utils/getMergeManyRecordsMutationResponseField';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr
|
||||
import { useUpdateManyRecordsMutation } from '@/object-record/hooks/useUpdateManyRecordsMutation';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
|
||||
import { getUpdateManyRecordsMutationResponseField } from '@/object-record/utils/getUpdateManyRecordsMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
|
||||
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
|
||||
import { useGetShouldInitializeRecordBoardForUpdateInputs } from '@/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs';
|
||||
import { useRemoveRecordsFromBoard } from '@/object-record/record-board/hooks/useRemoveRecordsFromBoard';
|
||||
import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery';
|
||||
@@ -7,7 +7,7 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { useContext } from 'react';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables';
|
||||
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
|
||||
export const RecordBoardSSESubscribeEffect = () => {
|
||||
const { recordBoardId } = useContext(RecordBoardContext);
|
||||
@@ -13,7 +13,7 @@ export const RecordBoardSSESubscribeEffect = () => {
|
||||
|
||||
const queryId = `record-board-${recordBoardId}`;
|
||||
|
||||
useListenToObjectRecordEventsForQuery({
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar
|
||||
import { useRecordCalendarQueryDateRangeFilter } from '@/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter';
|
||||
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
|
||||
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
|
||||
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
|
||||
@@ -32,7 +32,7 @@ export const RecordCalendarSSESubscribeEffect = () => {
|
||||
|
||||
const queryId = `record-calendar-${recordCalendarId}`;
|
||||
|
||||
useListenToObjectRecordEventsForQuery({
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
|
||||
type RecordShowPageSSESubscribeEffectProps = {
|
||||
objectNameSingular: string;
|
||||
@@ -11,7 +11,7 @@ export const RecordShowPageSSESubscribeEffect = ({
|
||||
}: RecordShowPageSSESubscribeEffectProps) => {
|
||||
const queryId = `record-show-${objectNameSingular}-${recordId}`;
|
||||
|
||||
useListenToObjectRecordEventsForQuery({
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
objectNameSingular,
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
|
||||
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { SSE_TABLE_DEBOUNCE_TIME_IN_MS_TO_AVOID_SSE_OWN_EVENTS_RACE_CONDITION } from '@/object-record/record-table/virtualization/constants/SseTableDebounceTimeInMsToAvoidSseOwnEventsRaceCondition';
|
||||
import { useGetShouldResetTableVirtualizationForUpdateInputs } from '@/object-record/record-table/virtualization/hooks/useGetShouldResetTableVirtualizationForUpdateInputs';
|
||||
import { useResetVirtualizationBecauseDataChanged } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export const RecordTableVirtualizedDataChangedEffect = () => {
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
|
||||
|
||||
@@ -26,7 +26,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
|
||||
|
||||
const queryId = `record-table-virtualized-${objectMetadataItem.nameSingular}`;
|
||||
|
||||
useListenToObjectRecordEventsForQuery({
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
|
||||
@@ -37,11 +37,11 @@ export const SSEEventStreamEffect = () => {
|
||||
const { triggerEventStreamDestroy } = useTriggerEventStreamDestroy();
|
||||
|
||||
useEffect(() => {
|
||||
const isSseClientAvailabble =
|
||||
const isSseClientAvailable =
|
||||
!isCreatingSseEventStream && !isDestroyingEventStream;
|
||||
|
||||
const willCreateEventStream =
|
||||
isSseClientAvailabble &&
|
||||
isSseClientAvailable &&
|
||||
isLoggedIn &&
|
||||
isSseDbEventsEnabled &&
|
||||
isDefined(currentUser) &&
|
||||
@@ -51,7 +51,7 @@ export const SSEEventStreamEffect = () => {
|
||||
isNonEmptyArray(objectMetadataItems);
|
||||
|
||||
const willDestroyEventStream =
|
||||
isSseClientAvailabble &&
|
||||
isSseClientAvailable &&
|
||||
isNonEmptyString(sseEventStreamId) &&
|
||||
shouldDestroyEventStream;
|
||||
|
||||
|
||||
+16
-2
@@ -4,8 +4,8 @@ export const ON_EVENT_SUBSCRIPTION = gql`
|
||||
subscription OnEventSubscription($eventStreamId: String!) {
|
||||
onEventSubscription(eventStreamId: $eventStreamId) {
|
||||
eventStreamId
|
||||
eventWithQueryIdsList {
|
||||
event {
|
||||
objectRecordEventsWithQueryIds {
|
||||
objectRecordEvent {
|
||||
action
|
||||
objectNameSingular
|
||||
recordId
|
||||
@@ -20,6 +20,20 @@ export const ON_EVENT_SUBSCRIPTION = gql`
|
||||
}
|
||||
queryIds
|
||||
}
|
||||
metadataEventsWithQueryIds {
|
||||
metadataEvent {
|
||||
type
|
||||
metadataName
|
||||
recordId
|
||||
properties {
|
||||
updatedFields
|
||||
before
|
||||
after
|
||||
diff
|
||||
}
|
||||
}
|
||||
queryIds
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { dispatchMetadataOperationBrowserEvent } from '@/browser-event/utils/dispatchMetadataOperationBrowserEvent';
|
||||
import { turnSseMetadataEventsToMetadataOperationBrowserEvents } from '@/sse-db-event/utils/turnSseMetadataEventsToMetadataOperationBrowserEvents';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
type AllMetadataName,
|
||||
type MetadataEvent,
|
||||
type MetadataEventWithQueryIds,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const groupSseMetadataEventsByMetadataName = (
|
||||
sseMetadataEvents: MetadataEvent[],
|
||||
): Map<AllMetadataName, MetadataEvent[]> => {
|
||||
const eventsByMetadataName = new Map<AllMetadataName, MetadataEvent[]>();
|
||||
|
||||
for (const event of sseMetadataEvents) {
|
||||
const metadataName = event.metadataName as AllMetadataName;
|
||||
|
||||
const existing = eventsByMetadataName.get(metadataName) ?? [];
|
||||
|
||||
eventsByMetadataName.set(metadataName, [...existing, event]);
|
||||
}
|
||||
|
||||
return eventsByMetadataName;
|
||||
};
|
||||
|
||||
export const useDispatchMetadataEventsFromSseToBrowserEvents = <
|
||||
T extends Record<string, unknown>,
|
||||
>() => {
|
||||
const dispatchMetadataEventsFromSseToBrowserEvents = useCallback(
|
||||
(metadataEventsWithQueryIds: MetadataEventWithQueryIds[]) => {
|
||||
const sseMetadataEvents = metadataEventsWithQueryIds.map(
|
||||
(item) => item.metadataEvent,
|
||||
);
|
||||
|
||||
const eventsByMetadataName =
|
||||
groupSseMetadataEventsByMetadataName(sseMetadataEvents);
|
||||
|
||||
for (const [metadataName, events] of eventsByMetadataName) {
|
||||
const metadataOperationBrowserEvents =
|
||||
turnSseMetadataEventsToMetadataOperationBrowserEvents<T>({
|
||||
metadataName,
|
||||
sseMetadataEvents: events,
|
||||
});
|
||||
|
||||
for (const browserEvent of metadataOperationBrowserEvents) {
|
||||
dispatchMetadataOperationBrowserEvent(browserEvent);
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { dispatchMetadataEventsFromSseToBrowserEvents };
|
||||
};
|
||||
+6
-6
@@ -1,19 +1,19 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { groupObjectRecordSseEventsByObjectMetadataItemNameSingular } from '@/sse-db-event/utils/groupObjectRecordSseEventsByObjectMetadataItemNameSingular';
|
||||
import { turnSseObjectRecordEventsToObjectRecordOperationBrowserEvents } from '@/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type EventWithQueryIds } from '~/generated-metadata/graphql';
|
||||
import { type ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const dispatchObjectRecordEventsFromSseToBrowserEvents = useCallback(
|
||||
(eventsWithQueryIds: EventWithQueryIds[]) => {
|
||||
const objectRecordEvents = eventsWithQueryIds.map((eventWithQueryIds) => {
|
||||
return eventWithQueryIds.event;
|
||||
});
|
||||
(objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[]) => {
|
||||
const objectRecordEvents = objectRecordEventsWithQueryIds.map(
|
||||
(item) => item.objectRecordEvent,
|
||||
);
|
||||
|
||||
const objectRecordEventsByObjectMetadataItemNameSingular =
|
||||
groupObjectRecordSseEventsByObjectMetadataItemNameSingular({
|
||||
|
||||
+8
-3
@@ -2,14 +2,19 @@ import { requiredQueryListenersState } from '@/sse-db-event/states/requiredQuery
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
import {
|
||||
type MetadataGqlOperationSignature,
|
||||
type RecordGqlOperationSignature,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const useListenToObjectRecordEventsForQuery = ({
|
||||
export const useListenToEventsForQuery = ({
|
||||
queryId,
|
||||
operationSignature,
|
||||
}: {
|
||||
queryId: string;
|
||||
operationSignature: RecordGqlOperationSignature;
|
||||
operationSignature:
|
||||
| RecordGqlOperationSignature
|
||||
| MetadataGqlOperationSignature;
|
||||
}) => {
|
||||
const changeQueryIdListenState = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
+56
-6
@@ -1,4 +1,5 @@
|
||||
import { ON_EVENT_SUBSCRIPTION } from '@/sse-db-event/graphql/subscriptions/OnEventSubscription';
|
||||
import { useDispatchMetadataEventsFromSseToBrowserEvents } from '@/sse-db-event/hooks/useDispatchMetadataEventsFromSseToBrowserEvents';
|
||||
import { useDispatchObjectRecordEventsFromSseToBrowserEvents } from '@/sse-db-event/hooks/useDispatchObjectRecordEventsFromSseToBrowserEvents';
|
||||
import { useTriggerOptimisticEffectFromSseEvents } from '@/sse-db-event/hooks/useTriggerOptimisticEffectFromSseEvents';
|
||||
import { disposeFunctionForEventStreamState } from '@/sse-db-event/states/disposeFunctionByEventStreamMapState';
|
||||
@@ -23,6 +24,9 @@ export const useTriggerEventStreamCreation = () => {
|
||||
isCreatingSseEventStreamState,
|
||||
);
|
||||
|
||||
const { dispatchMetadataEventsFromSseToBrowserEvents } =
|
||||
useDispatchMetadataEventsFromSseToBrowserEvents();
|
||||
|
||||
const { dispatchObjectRecordEventsFromSseToBrowserEvents } =
|
||||
useDispatchObjectRecordEventsFromSseToBrowserEvents();
|
||||
|
||||
@@ -73,9 +77,55 @@ export const useTriggerEventStreamCreation = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
next: (
|
||||
value: ExecutionResult<{
|
||||
onEventSubscription: EventSubscription;
|
||||
}>,
|
||||
) => {
|
||||
if (isDefined(value?.errors)) {
|
||||
captureException(
|
||||
new Error(
|
||||
`SSE subscription error: ${value.errors[0]?.message}`,
|
||||
),
|
||||
);
|
||||
set(shouldDestroyEventStreamState, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasReceivedFirstEvent) {
|
||||
hasReceivedFirstEvent = true;
|
||||
set(sseEventStreamReadyState, true);
|
||||
}
|
||||
|
||||
const eventSubscription = value?.data?.onEventSubscription;
|
||||
|
||||
const objectRecordEventsWithQueryIds =
|
||||
eventSubscription?.objectRecordEventsWithQueryIds ?? [];
|
||||
|
||||
const metadataEventsWithQueryIds =
|
||||
eventSubscription?.metadataEventsWithQueryIds ?? [];
|
||||
|
||||
const objectRecordEvents = objectRecordEventsWithQueryIds.map(
|
||||
(item) => item.objectRecordEvent,
|
||||
);
|
||||
|
||||
triggerOptimisticEffectFromSseEvents({
|
||||
objectRecordEvents,
|
||||
});
|
||||
|
||||
dispatchObjectRecordEventsFromSseToBrowserEvents(
|
||||
objectRecordEventsWithQueryIds,
|
||||
);
|
||||
|
||||
dispatchMetadataEventsFromSseToBrowserEvents(
|
||||
metadataEventsWithQueryIds,
|
||||
);
|
||||
},
|
||||
error: (error) => {
|
||||
captureException(error);
|
||||
},
|
||||
complete: () => {},
|
||||
error: () => {},
|
||||
next: () => {},
|
||||
},
|
||||
{
|
||||
message: ({ data, event }) => {
|
||||
@@ -109,12 +159,12 @@ export const useTriggerEventStreamCreation = () => {
|
||||
|
||||
const objectRecordEventsWithQueryIds =
|
||||
result?.data?.onEventSubscription
|
||||
?.eventWithQueryIdsList ?? [];
|
||||
?.objectRecordEventsWithQueryIds ?? [];
|
||||
|
||||
const objectRecordEvents =
|
||||
objectRecordEventsWithQueryIds.map(
|
||||
(eventWithQueryIds) => {
|
||||
return eventWithQueryIds.event;
|
||||
(objectRecordEventWithQueryIds) => {
|
||||
return objectRecordEventWithQueryIds.objectRecordEvent;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -144,9 +194,9 @@ export const useTriggerEventStreamCreation = () => {
|
||||
setIsCreatingSseEventStream(false);
|
||||
},
|
||||
[
|
||||
dispatchMetadataEventsFromSseToBrowserEvents,
|
||||
dispatchObjectRecordEventsFromSseToBrowserEvents,
|
||||
setIsCreatingSseEventStream,
|
||||
|
||||
triggerOptimisticEffectFromSseEvents,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
import { createState } from '@/ui/utilities/state/utils/createState';
|
||||
import {
|
||||
type MetadataGqlOperationSignature,
|
||||
type RecordGqlOperationSignature,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const activeQueryListenersState = createState<
|
||||
{ queryId: string; operationSignature: RecordGqlOperationSignature }[]
|
||||
{
|
||||
queryId: string;
|
||||
operationSignature:
|
||||
| RecordGqlOperationSignature
|
||||
| MetadataGqlOperationSignature;
|
||||
}[]
|
||||
>({
|
||||
key: 'activeQueryListenersState',
|
||||
defaultValue: [],
|
||||
|
||||
+10
-2
@@ -1,8 +1,16 @@
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
import { createState } from '@/ui/utilities/state/utils/createState';
|
||||
import {
|
||||
type MetadataGqlOperationSignature,
|
||||
type RecordGqlOperationSignature,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const requiredQueryListenersState = createState<
|
||||
{ queryId: string; operationSignature: RecordGqlOperationSignature }[]
|
||||
{
|
||||
queryId: string;
|
||||
operationSignature:
|
||||
| RecordGqlOperationSignature
|
||||
| MetadataGqlOperationSignature;
|
||||
}[]
|
||||
>({
|
||||
key: 'requiredQueryListenersState',
|
||||
defaultValue: [],
|
||||
|
||||
+5
-7
@@ -1,22 +1,20 @@
|
||||
import { type ObjectRecordEventsByQueryId } from '@/sse-db-event/types/ObjectRecordEventsByQueryId';
|
||||
import { getObjectRecordEventsForQueryEventName } from '@/sse-db-event/utils/getObjectRecordEventsForQueryEventName';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type EventWithQueryIds } from '~/generated-metadata/graphql';
|
||||
import { type ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
|
||||
|
||||
export const dispatchObjectRecordEventsWithQueryIds = (
|
||||
objectRecordEventsWithQueryIds: EventWithQueryIds[],
|
||||
objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[],
|
||||
) => {
|
||||
const objectRecordEventsByQueryId: ObjectRecordEventsByQueryId = {};
|
||||
|
||||
for (const objectRecordEventWithQueryIds of objectRecordEventsWithQueryIds) {
|
||||
for (const queryId of objectRecordEventWithQueryIds.queryIds) {
|
||||
for (const item of objectRecordEventsWithQueryIds) {
|
||||
for (const queryId of item.queryIds) {
|
||||
if (!isDefined(objectRecordEventsByQueryId[queryId])) {
|
||||
objectRecordEventsByQueryId[queryId] = [];
|
||||
}
|
||||
|
||||
objectRecordEventsByQueryId[queryId].push(
|
||||
objectRecordEventWithQueryIds.event,
|
||||
);
|
||||
objectRecordEventsByQueryId[queryId].push(item.objectRecordEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
MetadataEventAction,
|
||||
type AllMetadataName,
|
||||
type MetadataEvent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const turnSseMetadataEventsToMetadataOperationBrowserEvents = <
|
||||
T extends Record<string, unknown>,
|
||||
>({
|
||||
metadataName,
|
||||
sseMetadataEvents,
|
||||
}: {
|
||||
metadataName: AllMetadataName;
|
||||
sseMetadataEvents: MetadataEvent[];
|
||||
}): MetadataOperationBrowserEventDetail<T>[] => {
|
||||
return sseMetadataEvents
|
||||
.map((event): MetadataOperationBrowserEventDetail<T> | null => {
|
||||
switch (event.type) {
|
||||
case MetadataEventAction.CREATED: {
|
||||
const createdRecord = event.properties.after;
|
||||
|
||||
if (!isDefined(createdRecord)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
metadataName,
|
||||
operation: {
|
||||
type: 'create',
|
||||
createdRecord,
|
||||
},
|
||||
};
|
||||
}
|
||||
case MetadataEventAction.UPDATED: {
|
||||
const updatedRecord = event.properties.after;
|
||||
|
||||
if (!isDefined(updatedRecord)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
metadataName,
|
||||
operation: {
|
||||
type: 'update',
|
||||
updatedRecord,
|
||||
updatedFields: event.properties.updatedFields ?? undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
case MetadataEventAction.DELETED: {
|
||||
return {
|
||||
metadataName,
|
||||
operation: {
|
||||
type: 'delete',
|
||||
deletedRecordId: event.recordId,
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(isDefined);
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { getObjectRecordOperationUpdateInputs } from '@/sse-db-event/utils/getObjectRecordOperationUpdateInputs';
|
||||
import { groupObjectRecordSseEventsByEventType } from '@/sse-db-event/utils/groupObjectRecordSseEventsByEventType';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { useStopWorkflowRunMutation } from '~/generated/graphql';
|
||||
|
||||
export const useStopWorkflowRun = () => {
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
|
||||
export const WorkflowRunSSESubscribeEffect = ({
|
||||
workflowRunId,
|
||||
@@ -8,7 +8,7 @@ export const WorkflowRunSSESubscribeEffect = ({
|
||||
}) => {
|
||||
const queryId = `workflow-run-${workflowRunId}`;
|
||||
|
||||
useListenToObjectRecordEventsForQuery({
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
||||
|
||||
+3
-3
@@ -3,8 +3,8 @@ import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
|
||||
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
|
||||
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { shouldWorkflowRefetchRequestFamilyState } from '@/workflow/states/shouldWorkflowRefetchRequestFamilyState';
|
||||
|
||||
export const WorkflowSSESubscribeEffect = ({
|
||||
@@ -23,7 +23,7 @@ export const WorkflowSSESubscribeEffect = ({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
useListenToObjectRecordEventsForQuery({
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
|
||||
Reference in New Issue
Block a user