[SSE] Event stream TTL refresh (#17337)

This PR update the redis subscription iterator wrapper with an heartbeat
system. Heartbeat interval are based on event stream TTL. Those refresh
the event stream TTL and active streams in redis.

I also cleaned a few functions that were not used anymore.

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Thomas Trompette
2026-01-22 14:32:59 +01:00
committed by GitHub
parent 6b63a28cd2
commit 8bf266626c
5 changed files with 144 additions and 80 deletions
@@ -15,7 +15,6 @@ import {
EventStreamExceptionCode,
} from 'src/engine/subscriptions/event-stream.exception';
import { type EventStreamData } from 'src/engine/subscriptions/types/event-stream-data.type';
import { type ObjectRecordSubscriptionEvent } from 'src/engine/subscriptions/types/object-record-subscription-event.type';
@Injectable()
export class EventStreamService {
@@ -131,15 +130,6 @@ export class EventStreamService {
return result;
}
async getStreamData(
workspaceId: string,
eventStreamChannelId: string,
): Promise<EventStreamData | undefined> {
const key = this.getEventStreamKey(workspaceId, eventStreamChannelId);
return this.cacheStorageService.get<EventStreamData>(key);
}
async isAuthorized({
workspaceId,
eventStreamChannelId,
@@ -214,40 +204,25 @@ export class EventStreamService {
}
}
async getQueries(
workspaceId: string,
eventStreamId: string,
): Promise<Map<string, RecordGqlOperationSignature>> {
const streamData = await this.getStreamData(workspaceId, eventStreamId);
async refreshEventStreamTTL({
workspaceId,
eventStreamChannelId,
}: {
workspaceId: string;
eventStreamChannelId: string;
}): Promise<boolean> {
const eventStreamKey = this.getEventStreamKey(
workspaceId,
eventStreamChannelId,
);
const activeStreamsKey = this.getActiveStreamsKey(workspaceId);
if (!isDefined(streamData)) {
return new Map();
}
const [eventStreamRefreshed, activeStreamsRefreshed] = await Promise.all([
this.cacheStorageService.expire(eventStreamKey, EVENT_STREAM_TTL_MS),
this.cacheStorageService.expire(activeStreamsKey, EVENT_STREAM_TTL_MS),
]);
return new Map(Object.entries(streamData.queries));
}
matchQueriesWithEvent(
queries: Record<string, RecordGqlOperationSignature>,
event: ObjectRecordSubscriptionEvent,
): string[] {
const matchedQueryIds: string[] = [];
for (const [queryId, operationSignature] of Object.entries(queries)) {
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;
return eventStreamRefreshed && activeStreamsRefreshed;
}
private getEventStreamKey(
@@ -260,4 +235,13 @@ export class EventStreamService {
private getActiveStreamsKey(workspaceId: string): string {
return `workspace:${workspaceId}:activeStreams`;
}
private async getStreamData(
workspaceId: string,
eventStreamChannelId: string,
): Promise<EventStreamData | undefined> {
const key = this.getEventStreamKey(workspaceId, eventStreamChannelId);
return this.cacheStorageService.get<EventStreamData>(key);
}
}