feat: implement real-time database synchronization with SSE and update backend modules
Test Suite / backend-pr-quick (24.15.0) (push) Has been cancelled
Test Suite / backend-full (24.15.0) (push) Has been cancelled
Test Suite / flutter-quality (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-12 16:57:05 +02:00
parent 2dda34d4d2
commit 98ee8a3ad6
11 changed files with 400 additions and 7 deletions
+2
View File
@@ -16,6 +16,7 @@ import { UsersModule } from './users/users.module';
import { UserProductsModule } from './user-products/user-products.module';
import { CategoriesModule } from './categories/categories.module';
import { AiModule } from './ai/ai.module';
import { RealtimeModule } from './realtime/realtime.module';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { RolesGuard } from './auth/roles.guard';
@@ -44,6 +45,7 @@ import { RolesGuard } from './auth/roles.guard';
UserProductsModule,
CategoriesModule,
AiModule,
RealtimeModule,
],
providers: [
{
+24 -2
View File
@@ -1,5 +1,6 @@
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { Prisma, PrismaClient } from '@prisma/client';
import { RealtimeEventsService } from '../realtime/realtime-events.service';
@Injectable()
export class PrismaService
@@ -7,9 +8,30 @@ export class PrismaService
implements OnModuleInit, OnModuleDestroy
{
private readonly logger = new Logger(PrismaService.name);
private readonly writeActions = new Set<string>([
'create',
'update',
'upsert',
'delete',
'createMany',
'updateMany',
'deleteMany',
]);
constructor() {
constructor(private readonly realtimeEvents: RealtimeEventsService) {
super();
const realtimeMiddleware: Prisma.Middleware = async (params, next) => {
const result = await next(params);
if (params.model && this.writeActions.has(params.action)) {
this.realtimeEvents.notifyDatabaseWrite();
}
return result;
};
this.$use(realtimeMiddleware);
}
async onModuleInit() {
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { Subject } from 'rxjs';
export interface DbChangeEvent {
timestamp: string;
}
@Injectable()
export class RealtimeEventsService {
private readonly subject = new Subject<DbChangeEvent>();
private flushTimer: NodeJS.Timeout | null = null;
private hasPendingChanges = false;
readonly events$ = this.subject.asObservable();
notifyDatabaseWrite(): void {
this.hasPendingChanges = true;
if (this.flushTimer) return;
// Coalesce burst writes into one SSE event to reduce client/server load.
this.flushTimer = setTimeout(() => {
this.flushTimer = null;
if (!this.hasPendingChanges) return;
this.hasPendingChanges = false;
this.subject.next({ timestamp: new Date().toISOString() });
}, 400);
}
}
@@ -0,0 +1,28 @@
import { Controller, MessageEvent, Sse } from '@nestjs/common';
import { interval, map, merge, Observable } from 'rxjs';
import { RealtimeEventsService } from './realtime-events.service';
@Controller('events')
export class RealtimeController {
constructor(private readonly realtimeEvents: RealtimeEventsService) {}
@Sse('stream')
stream(): Observable<MessageEvent> {
const changes$ = this.realtimeEvents.events$.pipe(
map((event) => ({
type: 'db-change',
data: event,
})),
);
// Keeps connections alive through proxies and gives clients liveness signal.
const heartbeat$ = interval(20_000).pipe(
map(() => ({
type: 'heartbeat',
data: { timestamp: new Date().toISOString() },
})),
);
return merge(changes$, heartbeat$);
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Global, Module } from '@nestjs/common';
import { RealtimeController } from './realtime.controller';
import { RealtimeEventsService } from './realtime-events.service';
@Global()
@Module({
controllers: [RealtimeController],
providers: [RealtimeEventsService],
exports: [RealtimeEventsService],
})
export class RealtimeModule {}