feat: implement real-time database synchronization with SSE and update backend modules
This commit is contained in:
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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$);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
Reference in New Issue
Block a user