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';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME =
|
||||
'object-record-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]);
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
|
||||
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
export const useListenToObjectRecordOperationBrowserEvent = ({
|
||||
onObjectRecordOperationBrowserEvent,
|
||||
objectMetadataItemId,
|
||||
operationTypes,
|
||||
}: {
|
||||
onObjectRecordOperationBrowserEvent: (
|
||||
detail: ObjectRecordOperationBrowserEventDetail,
|
||||
) => void;
|
||||
objectMetadataItemId?: string;
|
||||
operationTypes?: ObjectRecordOperation['type'][];
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const handleObjectRecordOperationEvent = (event: Event) => {
|
||||
const detail = (
|
||||
event as CustomEvent<ObjectRecordOperationBrowserEventDetail>
|
||||
).detail;
|
||||
|
||||
if (
|
||||
isDefined(objectMetadataItemId) &&
|
||||
detail.objectMetadataItem.id !== objectMetadataItemId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isNonEmptyArray(operationTypes) &&
|
||||
!operationTypes.includes(detail.operation.type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onObjectRecordOperationBrowserEvent(detail);
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME,
|
||||
handleObjectRecordOperationEvent,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME,
|
||||
handleObjectRecordOperationEvent,
|
||||
);
|
||||
};
|
||||
}, [
|
||||
objectMetadataItemId,
|
||||
onObjectRecordOperationBrowserEvent,
|
||||
operationTypes,
|
||||
]);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
|
||||
|
||||
export type ObjectRecordOperationBrowserEventDetail = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
operation: ObjectRecordOperation;
|
||||
};
|
||||
+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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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,
|
||||
) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME, {
|
||||
detail,
|
||||
}),
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user