Publish batch events for sse subscriptions (#16943)
- publish batch events to avoid multiple redis / graphql sse publications - add event to the endpoint output
This commit is contained in:
@@ -4292,12 +4292,13 @@ export enum SubscriptionInterval {
|
||||
|
||||
export type SubscriptionMatch = {
|
||||
__typename?: 'SubscriptionMatch';
|
||||
id: Scalars['String'];
|
||||
event: OnDbEvent;
|
||||
subscriptionIds: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SubscriptionMatches = {
|
||||
__typename?: 'SubscriptionMatches';
|
||||
subscriptions: Array<SubscriptionMatch>;
|
||||
matches: Array<SubscriptionMatch>;
|
||||
};
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
|
||||
@@ -4084,12 +4084,13 @@ export enum SubscriptionInterval {
|
||||
|
||||
export type SubscriptionMatch = {
|
||||
__typename?: 'SubscriptionMatch';
|
||||
id: Scalars['String'];
|
||||
event: OnDbEvent;
|
||||
subscriptionIds: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SubscriptionMatches = {
|
||||
__typename?: 'SubscriptionMatches';
|
||||
subscriptions: Array<SubscriptionMatch>;
|
||||
matches: Array<SubscriptionMatch>;
|
||||
};
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
@@ -5112,7 +5113,7 @@ export type OnSubscriptionMatchSubscriptionVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type OnSubscriptionMatchSubscription = { __typename?: 'Subscription', onSubscriptionMatch?: { __typename?: 'SubscriptionMatches', subscriptions: Array<{ __typename?: 'SubscriptionMatch', id: string }> } | null };
|
||||
export type OnSubscriptionMatchSubscription = { __typename?: 'Subscription', onSubscriptionMatch?: { __typename?: 'SubscriptionMatches', matches: Array<{ __typename?: 'SubscriptionMatch', subscriptionIds: Array<string>, event: { __typename?: 'OnDbEvent', action: DatabaseEventAction, objectNameSingular: string, eventDate: string, record: any, updatedFields?: Array<string> | null } }> } | null };
|
||||
|
||||
export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null };
|
||||
|
||||
@@ -5783,8 +5784,15 @@ export type OnDbEventSubscriptionResult = Apollo.SubscriptionResult<OnDbEventSub
|
||||
export const OnSubscriptionMatchDocument = gql`
|
||||
subscription OnSubscriptionMatch($subscriptions: [SubscriptionInput!]!) {
|
||||
onSubscriptionMatch(subscriptions: $subscriptions) {
|
||||
subscriptions {
|
||||
id
|
||||
matches {
|
||||
subscriptionIds
|
||||
event {
|
||||
action
|
||||
objectNameSingular
|
||||
eventDate
|
||||
record
|
||||
updatedFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-8
@@ -5,6 +5,7 @@ import { print } from 'graphql';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type SubscriptionMatches } from '~/generated/graphql';
|
||||
|
||||
export const SubscriptionProviderEffect = () => {
|
||||
const subscriptionRegistry = useRecoilValue(subscriptionRegistryState);
|
||||
@@ -31,21 +32,23 @@ export const SubscriptionProviderEffect = () => {
|
||||
{
|
||||
next: (value) => {
|
||||
const data = value.data as {
|
||||
onSubscriptionMatch: { subscriptions: { id: string }[] };
|
||||
onSubscriptionMatch: SubscriptionMatches;
|
||||
} | null;
|
||||
|
||||
if (!data?.onSubscriptionMatch?.subscriptions) {
|
||||
if (!data?.onSubscriptionMatch?.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const subscription of data.onSubscriptionMatch.subscriptions) {
|
||||
const entry = subscriptionRegistry.get(subscription.id);
|
||||
for (const match of data.onSubscriptionMatch.matches) {
|
||||
for (const subscriptionId of match.subscriptionIds) {
|
||||
const entry = subscriptionRegistry.get(subscriptionId);
|
||||
|
||||
if (!isDefined(entry)) {
|
||||
continue;
|
||||
if (!isDefined(entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.onRefetch();
|
||||
}
|
||||
|
||||
entry.onRefetch();
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
|
||||
+9
-2
@@ -3,8 +3,15 @@ import { gql } from '@apollo/client';
|
||||
export const ON_SUBSCRIPTION_MATCH = gql`
|
||||
subscription OnSubscriptionMatch($subscriptions: [SubscriptionInput!]!) {
|
||||
onSubscriptionMatch(subscriptions: $subscriptions) {
|
||||
subscriptions {
|
||||
id
|
||||
matches {
|
||||
subscriptionIds
|
||||
event {
|
||||
action
|
||||
objectNameSingular
|
||||
eventDate
|
||||
record
|
||||
updatedFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { OnDbEventDTO } from './on-db-event.dto';
|
||||
|
||||
@ObjectType('SubscriptionMatch')
|
||||
export class SubscriptionMatchDTO {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
@Field(() => [String])
|
||||
subscriptionIds: string[];
|
||||
|
||||
@Field(() => OnDbEventDTO)
|
||||
event: OnDbEventDTO;
|
||||
}
|
||||
|
||||
@ObjectType('SubscriptionMatches')
|
||||
export class SubscriptionMatchesDTO {
|
||||
@Field(() => [SubscriptionMatchDTO])
|
||||
subscriptions: SubscriptionMatchDTO[];
|
||||
matches: SubscriptionMatchDTO[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum SubscriptionChannel {
|
||||
DATABASE_EVENT_CHANNEL = 'DATABASE_EVENT_CHANNEL',
|
||||
DATABASE_BATCH_EVENTS_CHANNEL = 'DATABASE_BATCH_EVENTS_CHANNEL',
|
||||
SERVERLESS_FUNCTION_LOGS_CHANNEL = 'SERVERLESS_FUNCTION_LOGS_CHANNEL',
|
||||
}
|
||||
|
||||
+26
-21
@@ -61,36 +61,41 @@ export class WorkspaceEventEmitterResolver {
|
||||
nullable: true,
|
||||
resolve: async function (
|
||||
this: WorkspaceEventEmitterResolver,
|
||||
payload: { onDbEvent: OnDbEventDTO },
|
||||
payload: { onDbEvents: OnDbEventDTO[] },
|
||||
args: { subscriptions: SubscriptionInput[] },
|
||||
context: { req: { workspace: { id: string } } },
|
||||
): Promise<SubscriptionMatchesDTO> {
|
||||
const workspaceId = context.req.workspace.id;
|
||||
|
||||
const matchedSubscriptionIds = await Promise.all(
|
||||
args.subscriptions.map(async (subscription) => {
|
||||
const matches =
|
||||
await this.subscriptionService.isSubscriptionMatchingEvent(
|
||||
subscription,
|
||||
payload.onDbEvent,
|
||||
workspaceId,
|
||||
);
|
||||
const matches: { subscriptionIds: string[]; event: OnDbEventDTO }[] = [];
|
||||
|
||||
return matches ? subscription.id : null;
|
||||
}),
|
||||
);
|
||||
for (const event of payload.onDbEvents) {
|
||||
const matchedSubscriptionIds = await Promise.all(
|
||||
args.subscriptions.map(async (subscription) => {
|
||||
const isMatch =
|
||||
await this.subscriptionService.isSubscriptionMatchingEvent(
|
||||
subscription,
|
||||
event,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const filteredIds = matchedSubscriptionIds.filter(
|
||||
(id): id is string => id !== null,
|
||||
);
|
||||
return isMatch ? subscription.id : null;
|
||||
}),
|
||||
);
|
||||
|
||||
if (filteredIds.length === 0) {
|
||||
return { subscriptions: [] };
|
||||
const filteredIds = matchedSubscriptionIds.filter(
|
||||
(id): id is string => id !== null,
|
||||
);
|
||||
|
||||
if (filteredIds.length > 0) {
|
||||
matches.push({
|
||||
subscriptionIds: filteredIds,
|
||||
event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscriptions: filteredIds.map((id) => ({ id })),
|
||||
};
|
||||
return { matches };
|
||||
},
|
||||
})
|
||||
onSubscriptionMatch(
|
||||
@@ -99,7 +104,7 @@ export class WorkspaceEventEmitterResolver {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
) {
|
||||
return this.subscriptionService.subscribe({
|
||||
channel: SubscriptionChannel.DATABASE_EVENT_CHANNEL,
|
||||
channel: SubscriptionChannel.DATABASE_BATCH_EVENTS_CHANNEL,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
+22
-11
@@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { transformEventToWebhookEvent } from 'src/engine/core-modules/webhook/utils/transform-event-to-webhook-event';
|
||||
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
|
||||
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
|
||||
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceEventEmitterService {
|
||||
@@ -16,25 +16,36 @@ export class WorkspaceEventEmitterService {
|
||||
): Promise<void> {
|
||||
const [nameSingular, operation] = workspaceEventBatch.name.split('.');
|
||||
|
||||
const batchEvents = [];
|
||||
|
||||
for (const eventData of workspaceEventBatch.events) {
|
||||
const { record, updatedFields } = transformEventToWebhookEvent({
|
||||
eventName: workspaceEventBatch.name,
|
||||
event: eventData,
|
||||
});
|
||||
|
||||
const event = {
|
||||
action: operation,
|
||||
objectNameSingular: nameSingular,
|
||||
eventDate: new Date(),
|
||||
record,
|
||||
...(updatedFields && { updatedFields }),
|
||||
};
|
||||
|
||||
batchEvents.push(event);
|
||||
|
||||
// Publish individual events to legacy channel (onDbEvent)
|
||||
await this.subscriptionService.publish({
|
||||
channel: SubscriptionChannel.DATABASE_EVENT_CHANNEL,
|
||||
workspaceId: workspaceEventBatch.workspaceId,
|
||||
payload: {
|
||||
onDbEvent: {
|
||||
action: operation,
|
||||
objectNameSingular: nameSingular,
|
||||
eventDate: new Date(),
|
||||
record,
|
||||
...(updatedFields && { updatedFields }),
|
||||
},
|
||||
},
|
||||
payload: { onDbEvent: event },
|
||||
});
|
||||
}
|
||||
|
||||
await this.subscriptionService.publish({
|
||||
channel: SubscriptionChannel.DATABASE_BATCH_EVENTS_CHANNEL,
|
||||
workspaceId: workspaceEventBatch.workspaceId,
|
||||
payload: { onDbEvents: batchEvents },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user