feat: bootstrap from Fabric monorepo
This commit is contained in:
129
src/realtime/realtime.gateway.ts
Normal file
129
src/realtime/realtime.gateway.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
ConnectedSocket,
|
||||
MessageBody,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
SubscribeMessage,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
|
||||
@WebSocketGateway({
|
||||
namespace: '/realtime',
|
||||
cors: {
|
||||
origin: '*',
|
||||
},
|
||||
})
|
||||
export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer()
|
||||
server!: Server;
|
||||
|
||||
private readonly logger = new Logger(RealtimeGateway.name);
|
||||
private readonly onlineUsers = new Set<string>();
|
||||
|
||||
private userIdFromClient(client: Socket): string {
|
||||
const authUser = client.handshake.auth?.userId;
|
||||
const headerUser = client.handshake.headers['x-user-id'];
|
||||
const userId = typeof authUser === 'string' ? authUser : Array.isArray(headerUser) ? headerUser[0] : headerUser;
|
||||
return userId && typeof userId === 'string' && userId.trim() !== '' ? userId : `anon:${client.id}`;
|
||||
}
|
||||
|
||||
handleConnection(client: Socket): void {
|
||||
const expected = process.env.FABRIC_API_KEY;
|
||||
if (!expected) {
|
||||
client.disconnect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const authKey = client.handshake.auth?.apiKey;
|
||||
const headerKey = client.handshake.headers['x-api-key'];
|
||||
const apiKey = typeof authKey === 'string' ? authKey : Array.isArray(headerKey) ? headerKey[0] : headerKey;
|
||||
|
||||
if (apiKey !== expected) {
|
||||
this.logger.warn(`socket rejected: ${client.id}`);
|
||||
client.disconnect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`socket connected: ${client.id}`);
|
||||
|
||||
const userId = this.userIdFromClient(client);
|
||||
client.data.userId = userId;
|
||||
this.onlineUsers.add(userId);
|
||||
this.server.emit('presence.online', {
|
||||
userId,
|
||||
onlineCount: this.onlineUsers.size,
|
||||
occurredAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
handleDisconnect(client: Socket): void {
|
||||
this.logger.log(`socket disconnected: ${client.id}`);
|
||||
|
||||
const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`;
|
||||
this.onlineUsers.delete(userId);
|
||||
this.server.emit('presence.offline', {
|
||||
userId,
|
||||
onlineCount: this.onlineUsers.size,
|
||||
occurredAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
@SubscribeMessage('join_channel')
|
||||
joinChannel(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() body: { channelId?: string },
|
||||
): { ok: boolean } {
|
||||
if (!body?.channelId) return { ok: false };
|
||||
client.join(`channel:${body.channelId}`);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@SubscribeMessage('leave_channel')
|
||||
leaveChannel(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() body: { channelId?: string },
|
||||
): { ok: boolean } {
|
||||
if (!body?.channelId) return { ok: false };
|
||||
client.leave(`channel:${body.channelId}`);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@SubscribeMessage('typing.start')
|
||||
typingStart(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() body: { channelId?: string },
|
||||
): { ok: boolean } {
|
||||
if (!body?.channelId) return { ok: false };
|
||||
|
||||
const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`;
|
||||
this.server.to(`channel:${body.channelId}`).emit('typing.start', {
|
||||
channelId: body.channelId,
|
||||
userId,
|
||||
occurredAt: new Date().toISOString(),
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@SubscribeMessage('typing.stop')
|
||||
typingStop(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() body: { channelId?: string },
|
||||
): { ok: boolean } {
|
||||
if (!body?.channelId) return { ok: false };
|
||||
|
||||
const userId = typeof client.data.userId === 'string' ? client.data.userId : `anon:${client.id}`;
|
||||
this.server.to(`channel:${body.channelId}`).emit('typing.stop', {
|
||||
channelId: body.channelId,
|
||||
userId,
|
||||
occurredAt: new Date().toISOString(),
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
emitChannelEvent(channelId: string, event: string, data: Record<string, unknown>): void {
|
||||
this.server.to(`channel:${channelId}`).emit(event, data);
|
||||
}
|
||||
}
|
||||
9
src/realtime/realtime.module.ts
Normal file
9
src/realtime/realtime.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { RealtimeGateway } from './realtime.gateway';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [RealtimeGateway],
|
||||
exports: [RealtimeGateway],
|
||||
})
|
||||
export class RealtimeModule {}
|
||||
Reference in New Issue
Block a user