doll active state <-> doll stream toggle
This commit is contained in:
@@ -34,7 +34,10 @@ describe('DollsService', () => {
|
||||
friendship: {
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
$transaction: jest.fn((callback) => callback(mockPrismaService)),
|
||||
$transaction: jest.fn((callback) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return callback(mockPrismaService);
|
||||
}),
|
||||
user: {
|
||||
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
@@ -72,6 +75,13 @@ describe('DollsService', () => {
|
||||
const createDto = { name: 'New Doll' };
|
||||
const userId = 'user-1';
|
||||
|
||||
// Mock the transaction callback to return the doll
|
||||
jest
|
||||
.spyOn(prismaService, '$transaction')
|
||||
.mockImplementation(async (callback) => {
|
||||
return callback(prismaService);
|
||||
});
|
||||
|
||||
await service.create(userId, createDto);
|
||||
|
||||
expect(prismaService.doll.create).toHaveBeenCalledWith({
|
||||
|
||||
@@ -26,6 +26,12 @@ export class UserBasicDto {
|
||||
required: false,
|
||||
})
|
||||
picture?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "User's active doll",
|
||||
required: false,
|
||||
})
|
||||
activeDoll?: any;
|
||||
}
|
||||
|
||||
export class FriendRequestResponseDto {
|
||||
|
||||
@@ -42,6 +42,20 @@ type FriendRequestWithRelations = FriendRequest & {
|
||||
};
|
||||
import { UsersService } from '../users/users.service';
|
||||
|
||||
type FriendWithDoll = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string | null;
|
||||
picture: string | null;
|
||||
activeDoll?: {
|
||||
id: string;
|
||||
name: string;
|
||||
configuration: any;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} | null;
|
||||
};
|
||||
|
||||
@ApiTags('friends')
|
||||
@Controller('friends')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -299,16 +313,30 @@ export class FriendsController {
|
||||
|
||||
const friendships = await this.friendsService.getFriends(user.id);
|
||||
|
||||
return friendships.map((friendship) => ({
|
||||
return friendships.map((friendship) => {
|
||||
// Need to cast to any because TS doesn't know about the included relation in the service method
|
||||
const friend = friendship.friend as unknown as FriendWithDoll;
|
||||
|
||||
return {
|
||||
id: friendship.id,
|
||||
friend: {
|
||||
id: friendship.friend.id,
|
||||
name: friendship.friend.name,
|
||||
username: friendship.friend.username ?? undefined,
|
||||
picture: friendship.friend.picture ?? undefined,
|
||||
id: friend.id,
|
||||
name: friend.name,
|
||||
username: friend.username ?? undefined,
|
||||
picture: friend.picture ?? undefined,
|
||||
activeDoll: friend.activeDoll
|
||||
? {
|
||||
id: friend.activeDoll.id,
|
||||
name: friend.activeDoll.name,
|
||||
configuration: friend.activeDoll.configuration as unknown,
|
||||
createdAt: friend.activeDoll.createdAt,
|
||||
updatedAt: friend.activeDoll.updatedAt,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
createdAt: friendship.createdAt,
|
||||
}));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':friendId')
|
||||
|
||||
@@ -275,7 +275,11 @@ export class FriendsService {
|
||||
return this.prisma.friendship.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
friend: true,
|
||||
friend: {
|
||||
include: {
|
||||
activeDoll: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
|
||||
1
src/types/socket.d.ts
vendored
1
src/types/socket.d.ts
vendored
@@ -9,6 +9,7 @@ export type AuthenticatedSocket = BaseSocket<
|
||||
{
|
||||
user?: AuthenticatedUser;
|
||||
userId?: string;
|
||||
activeDollId?: string | null;
|
||||
friends?: Set<string>; // Set of friend user IDs
|
||||
}
|
||||
>;
|
||||
|
||||
11
src/users/events/user.events.ts
Normal file
11
src/users/events/user.events.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Doll } from '@prisma/client';
|
||||
|
||||
export const UserEvents = {
|
||||
ACTIVE_DOLL_CHANGED: 'user.active-doll.changed',
|
||||
} as const;
|
||||
|
||||
export interface UserActiveDollChangedEvent {
|
||||
userId: string;
|
||||
dollId: string | null;
|
||||
doll: Doll | null;
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
ForbiddenException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { PrismaService } from '../database/prisma.service';
|
||||
import { User, Prisma } from '@prisma/client';
|
||||
import type { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/client';
|
||||
import { UserEvents } from './events/user.events';
|
||||
|
||||
/**
|
||||
* Interface for creating a user from Keycloak token
|
||||
@@ -56,7 +58,10 @@ export interface CreateUserFromTokenDto {
|
||||
export class UsersService {
|
||||
private readonly logger = new Logger(UsersService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Finds a user or creates one if they don't exist.
|
||||
@@ -429,6 +434,12 @@ export class UsersService {
|
||||
|
||||
this.logger.log(`User ${userId} activated doll ${dollId}`);
|
||||
|
||||
this.eventEmitter.emit(UserEvents.ACTIVE_DOLL_CHANGED, {
|
||||
userId,
|
||||
dollId,
|
||||
doll,
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
@@ -458,6 +469,12 @@ export class UsersService {
|
||||
|
||||
this.logger.log(`User ${userId} deactivated their doll`);
|
||||
|
||||
this.eventEmitter.emit(UserEvents.ACTIVE_DOLL_CHANGED, {
|
||||
userId,
|
||||
dollId: null,
|
||||
doll: null,
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ import type {
|
||||
DollDeletedEvent,
|
||||
} from '../../dolls/events/doll.events';
|
||||
|
||||
import { UserEvents } from '../../users/events/user.events';
|
||||
import type { UserActiveDollChangedEvent } from '../../users/events/user.events';
|
||||
|
||||
const WS_EVENT = {
|
||||
CURSOR_REPORT_POSITION: 'cursor-report-position',
|
||||
FRIEND_REQUEST_RECEIVED: 'friend-request-received',
|
||||
@@ -44,6 +47,7 @@ const WS_EVENT = {
|
||||
FRIEND_DOLL_CREATED: 'friend-doll-created',
|
||||
FRIEND_DOLL_UPDATED: 'friend-doll-updated',
|
||||
FRIEND_DOLL_DELETED: 'friend-doll-deleted',
|
||||
FRIEND_ACTIVE_DOLL_CHANGED: 'friend-active-doll-changed',
|
||||
} as const;
|
||||
|
||||
@WebSocketGateway({
|
||||
@@ -108,6 +112,13 @@ export class StateGateway
|
||||
await this.userSocketService.setSocket(user.id, client.id);
|
||||
client.data.userId = user.id;
|
||||
|
||||
// Sync active doll state to socket
|
||||
const userWithDoll = await this.prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { activeDollId: true },
|
||||
});
|
||||
client.data.activeDollId = userWithDoll?.activeDollId || null;
|
||||
|
||||
// Initialize friends cache using Prisma directly
|
||||
const friends = await this.prisma.friendship.findMany({
|
||||
where: { userId: user.id },
|
||||
@@ -183,6 +194,11 @@ export class StateGateway
|
||||
|
||||
const currentUserId = client.data.userId;
|
||||
|
||||
// Do not broadcast cursor position if user has no active doll
|
||||
if (!client.data.activeDollId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUserId) {
|
||||
this.logger.warn(`Could not find user ID for client ${client.id}`);
|
||||
return;
|
||||
@@ -387,4 +403,49 @@ export class StateGateway
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent(UserEvents.ACTIVE_DOLL_CHANGED)
|
||||
async handleActiveDollChanged(payload: UserActiveDollChangedEvent) {
|
||||
const { userId, dollId, doll } = payload;
|
||||
|
||||
// 1. Update the user's socket data to reflect the change
|
||||
const socketId = await this.userSocketService.getSocket(userId);
|
||||
if (socketId) {
|
||||
const userSocket = this.io.sockets.sockets.get(
|
||||
socketId,
|
||||
) as AuthenticatedSocket;
|
||||
if (userSocket) {
|
||||
userSocket.data.activeDollId = dollId;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Broadcast to friends
|
||||
const friends = await this.prisma.friendship.findMany({
|
||||
where: { userId },
|
||||
select: { friendId: true },
|
||||
});
|
||||
const friendIds = friends.map((f) => f.friendId);
|
||||
|
||||
const friendSockets =
|
||||
await this.userSocketService.getFriendsSockets(friendIds);
|
||||
|
||||
this.logger.log(
|
||||
`Broadcasting friend-active-doll-changed for user ${userId}, doll: ${doll ? doll.id : 'null'} to ${friendSockets.length} friends`,
|
||||
);
|
||||
|
||||
for (const { socketId } of friendSockets) {
|
||||
this.io.to(socketId).emit(WS_EVENT.FRIEND_ACTIVE_DOLL_CHANGED, {
|
||||
friendId: userId,
|
||||
doll: doll
|
||||
? {
|
||||
id: doll.id,
|
||||
name: doll.name,
|
||||
configuration: doll.configuration,
|
||||
createdAt: doll.createdAt,
|
||||
updatedAt: doll.updatedAt,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user