[SSE] Add backend for SSE subscriptions (#17022)
- moved a few gql types to twenty shared to re-use in server - added a new endpoint, onEventSubscription that expect a streamId to create a connection - two new endpoints to store queries in Redis - updated the existing batch channel to directly use object record type
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
|
||||
import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator';
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
|
||||
export type ObjectRecordSubscriptionEvent = ObjectRecordEvent & {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class EventStreamService {
|
||||
constructor(
|
||||
@InjectCacheStorage(CacheStorageNamespace.EngineSubscriptions)
|
||||
private readonly cacheStorageService: CacheStorageService,
|
||||
) {}
|
||||
|
||||
private getEventStreamKey(
|
||||
workspaceId: string,
|
||||
eventStreamId: string,
|
||||
): string {
|
||||
return `eventStream:${workspaceId}:${eventStreamId}`;
|
||||
}
|
||||
|
||||
@WithLock('eventStreamId')
|
||||
async addQuery({
|
||||
workspaceId,
|
||||
eventStreamId,
|
||||
queryId,
|
||||
operationSignature,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
eventStreamId: string;
|
||||
queryId: string;
|
||||
operationSignature: RecordGqlOperationSignature;
|
||||
}): Promise<void> {
|
||||
const key = this.getEventStreamKey(workspaceId, eventStreamId);
|
||||
const existing =
|
||||
(await this.cacheStorageService.get<
|
||||
Record<string, RecordGqlOperationSignature>
|
||||
>(key)) || {};
|
||||
|
||||
existing[queryId] = operationSignature;
|
||||
|
||||
await this.cacheStorageService.set(key, existing);
|
||||
}
|
||||
|
||||
@WithLock('eventStreamId')
|
||||
async removeQuery({
|
||||
workspaceId,
|
||||
eventStreamId,
|
||||
queryId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
eventStreamId: string;
|
||||
queryId: string;
|
||||
}): Promise<void> {
|
||||
const key = this.getEventStreamKey(workspaceId, eventStreamId);
|
||||
const existing =
|
||||
await this.cacheStorageService.get<
|
||||
Record<string, RecordGqlOperationSignature>
|
||||
>(key);
|
||||
|
||||
if (existing && existing[queryId]) {
|
||||
delete existing[queryId];
|
||||
await this.cacheStorageService.set(key, existing);
|
||||
}
|
||||
}
|
||||
|
||||
async getQueries(
|
||||
workspaceId: string,
|
||||
eventStreamId: string,
|
||||
): Promise<Map<string, RecordGqlOperationSignature>> {
|
||||
const key = this.getEventStreamKey(workspaceId, eventStreamId);
|
||||
const data =
|
||||
await this.cacheStorageService.get<
|
||||
Record<string, RecordGqlOperationSignature>
|
||||
>(key);
|
||||
|
||||
return new Map(Object.entries(data || {}));
|
||||
}
|
||||
|
||||
async matchQueriesWithEvent(
|
||||
queries: Map<string, RecordGqlOperationSignature>,
|
||||
event: ObjectRecordSubscriptionEvent,
|
||||
): Promise<string[]> {
|
||||
const matchedQueryIds: string[] = [];
|
||||
|
||||
for (const [queryId, operationSignature] of queries.entries()) {
|
||||
if (this.isQueryMatchingEvent(operationSignature, event)) {
|
||||
matchedQueryIds.push(queryId);
|
||||
}
|
||||
}
|
||||
|
||||
return matchedQueryIds;
|
||||
}
|
||||
|
||||
private isQueryMatchingEvent(
|
||||
operationSignature: RecordGqlOperationSignature,
|
||||
event: ObjectRecordSubscriptionEvent,
|
||||
): boolean {
|
||||
// to be improved
|
||||
return operationSignature.objectNameSingular === event.objectNameSingular;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user