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
@@ -10,6 +10,7 @@ import { FrontComponentResolver } from 'src/engine/metadata-modules/front-compon
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@@ -21,6 +22,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
TokenModule,
PermissionsModule,
FlatFrontComponentModule,
SubscriptionsModule,
],
controllers: [FrontComponentController],
providers: [
@@ -129,10 +129,14 @@ export class FrontComponentService {
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatFrontComponentToCreate.id,
flatEntityMaps: recomputedFlatFrontComponentMaps,
});
const createdFlatFrontComponent = findFlatEntityByIdInFlatEntityMapsOrThrow(
{
flatEntityId: flatFrontComponentToCreate.id,
flatEntityMaps: recomputedFlatFrontComponentMaps,
},
);
return createdFlatFrontComponent;
}
async updateOne({
@@ -200,10 +204,14 @@ export class FrontComponentService {
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatFrontComponentMaps,
});
const updatedFlatFrontComponent = findFlatEntityByIdInFlatEntityMapsOrThrow(
{
flatEntityId: id,
flatEntityMaps: recomputedFlatFrontComponentMaps,
},
);
return updatedFlatFrontComponent;
}
async destroyOne({