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:
Raphaël Bosi
2026-02-18 12:26:20 +01:00
committed by GitHub
parent e3753bf822
commit 2455c859b4
76 changed files with 1636 additions and 173 deletions
@@ -1,7 +1,8 @@
import { Field, InputType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
import { type RecordOrMetadataGqlOperationSignature } from 'src/engine/subscriptions/types/event-stream-data.type';
@InputType()
export class AddQuerySubscriptionInput {
@@ -12,5 +13,5 @@ export class AddQuerySubscriptionInput {
queryId: string;
@Field(() => GraphQLJSON)
operationSignature: RecordGqlOperationSignature;
operationSignature: RecordOrMetadataGqlOperationSignature;
}
@@ -1,14 +1,24 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { MetadataEventDTO } from './metadata-event.dto';
import { ObjectRecordEventDTO } from './object-record-event.dto';
@ObjectType('EventWithQueryIds')
export class EventWithQueryIdsDTO {
@ObjectType('ObjectRecordEventWithQueryIds')
export class ObjectRecordEventWithQueryIdsDTO {
@Field(() => [String])
queryIds: string[];
@Field(() => ObjectRecordEventDTO)
event: ObjectRecordEventDTO;
objectRecordEvent: ObjectRecordEventDTO;
}
@ObjectType('MetadataEventWithQueryIds')
export class MetadataEventWithQueryIdsDTO {
@Field(() => [String])
queryIds: string[];
@Field(() => MetadataEventDTO)
metadataEvent: MetadataEventDTO;
}
@ObjectType('EventSubscription')
@@ -16,6 +26,9 @@ export class EventSubscriptionDTO {
@Field(() => String)
eventStreamId: string;
@Field(() => [EventWithQueryIdsDTO])
eventWithQueryIdsList: EventWithQueryIdsDTO[];
@Field(() => [ObjectRecordEventWithQueryIdsDTO])
objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIdsDTO[];
@Field(() => [MetadataEventWithQueryIdsDTO])
metadataEventsWithQueryIds: MetadataEventWithQueryIdsDTO[];
}
@@ -0,0 +1,19 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { ObjectRecordEventPropertiesDTO } from 'src/engine/subscriptions/dtos/object-record-event-properties.dto';
import { MetadataEventAction } from 'src/engine/subscriptions/enums/metadata-event-action.enum';
@ObjectType('MetadataEvent')
export class MetadataEventDTO {
@Field(() => MetadataEventAction)
type: MetadataEventAction;
@Field(() => String)
metadataName: string;
@Field(() => String)
recordId: string;
@Field(() => ObjectRecordEventPropertiesDTO)
properties: ObjectRecordEventPropertiesDTO;
}