doll active state <-> doll stream toggle
This commit is contained in:
@@ -34,7 +34,10 @@ describe('DollsService', () => {
|
|||||||
friendship: {
|
friendship: {
|
||||||
findMany: jest.fn().mockResolvedValue([]),
|
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: {
|
user: {
|
||||||
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
|
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||||
},
|
},
|
||||||
@@ -72,6 +75,13 @@ describe('DollsService', () => {
|
|||||||
const createDto = { name: 'New Doll' };
|
const createDto = { name: 'New Doll' };
|
||||||
const userId = 'user-1';
|
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);
|
await service.create(userId, createDto);
|
||||||
|
|
||||||
expect(prismaService.doll.create).toHaveBeenCalledWith({
|
expect(prismaService.doll.create).toHaveBeenCalledWith({
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ export class UserBasicDto {
|
|||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
picture?: string;
|
picture?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "User's active doll",
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
activeDoll?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FriendRequestResponseDto {
|
export class FriendRequestResponseDto {
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ type FriendRequestWithRelations = FriendRequest & {
|
|||||||
};
|
};
|
||||||
import { UsersService } from '../users/users.service';
|
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')
|
@ApiTags('friends')
|
||||||
@Controller('friends')
|
@Controller('friends')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@@ -299,16 +313,30 @@ export class FriendsController {
|
|||||||
|
|
||||||
const friendships = await this.friendsService.getFriends(user.id);
|
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,
|
id: friendship.id,
|
||||||
friend: {
|
friend: {
|
||||||
id: friendship.friend.id,
|
id: friend.id,
|
||||||
name: friendship.friend.name,
|
name: friend.name,
|
||||||
username: friendship.friend.username ?? undefined,
|
username: friend.username ?? undefined,
|
||||||
picture: friendship.friend.picture ?? 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,
|
createdAt: friendship.createdAt,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':friendId')
|
@Delete(':friendId')
|
||||||
|
|||||||
@@ -275,7 +275,11 @@ export class FriendsService {
|
|||||||
return this.prisma.friendship.findMany({
|
return this.prisma.friendship.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
include: {
|
include: {
|
||||||
friend: true,
|
friend: {
|
||||||
|
include: {
|
||||||
|
activeDoll: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
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;
|
user?: AuthenticatedUser;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
activeDollId?: string | null;
|
||||||
friends?: Set<string>; // Set of friend user IDs
|
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,
|
ForbiddenException,
|
||||||
Logger,
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { PrismaService } from '../database/prisma.service';
|
import { PrismaService } from '../database/prisma.service';
|
||||||
import { User, Prisma } from '@prisma/client';
|
import { User, Prisma } from '@prisma/client';
|
||||||
import type { UpdateUserDto } from './dto/update-user.dto';
|
import type { UpdateUserDto } from './dto/update-user.dto';
|
||||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/client';
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/client';
|
||||||
|
import { UserEvents } from './events/user.events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for creating a user from Keycloak token
|
* Interface for creating a user from Keycloak token
|
||||||
@@ -56,7 +58,10 @@ export interface CreateUserFromTokenDto {
|
|||||||
export class UsersService {
|
export class UsersService {
|
||||||
private readonly logger = new Logger(UsersService.name);
|
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.
|
* 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.logger.log(`User ${userId} activated doll ${dollId}`);
|
||||||
|
|
||||||
|
this.eventEmitter.emit(UserEvents.ACTIVE_DOLL_CHANGED, {
|
||||||
|
userId,
|
||||||
|
dollId,
|
||||||
|
doll,
|
||||||
|
});
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,6 +469,12 @@ export class UsersService {
|
|||||||
|
|
||||||
this.logger.log(`User ${userId} deactivated their doll`);
|
this.logger.log(`User ${userId} deactivated their doll`);
|
||||||
|
|
||||||
|
this.eventEmitter.emit(UserEvents.ACTIVE_DOLL_CHANGED, {
|
||||||
|
userId,
|
||||||
|
dollId: null,
|
||||||
|
doll: null,
|
||||||
|
});
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ import type {
|
|||||||
DollDeletedEvent,
|
DollDeletedEvent,
|
||||||
} from '../../dolls/events/doll.events';
|
} from '../../dolls/events/doll.events';
|
||||||
|
|
||||||
|
import { UserEvents } from '../../users/events/user.events';
|
||||||
|
import type { UserActiveDollChangedEvent } from '../../users/events/user.events';
|
||||||
|
|
||||||
const WS_EVENT = {
|
const WS_EVENT = {
|
||||||
CURSOR_REPORT_POSITION: 'cursor-report-position',
|
CURSOR_REPORT_POSITION: 'cursor-report-position',
|
||||||
FRIEND_REQUEST_RECEIVED: 'friend-request-received',
|
FRIEND_REQUEST_RECEIVED: 'friend-request-received',
|
||||||
@@ -44,6 +47,7 @@ const WS_EVENT = {
|
|||||||
FRIEND_DOLL_CREATED: 'friend-doll-created',
|
FRIEND_DOLL_CREATED: 'friend-doll-created',
|
||||||
FRIEND_DOLL_UPDATED: 'friend-doll-updated',
|
FRIEND_DOLL_UPDATED: 'friend-doll-updated',
|
||||||
FRIEND_DOLL_DELETED: 'friend-doll-deleted',
|
FRIEND_DOLL_DELETED: 'friend-doll-deleted',
|
||||||
|
FRIEND_ACTIVE_DOLL_CHANGED: 'friend-active-doll-changed',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
@@ -108,6 +112,13 @@ export class StateGateway
|
|||||||
await this.userSocketService.setSocket(user.id, client.id);
|
await this.userSocketService.setSocket(user.id, client.id);
|
||||||
client.data.userId = user.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
|
// Initialize friends cache using Prisma directly
|
||||||
const friends = await this.prisma.friendship.findMany({
|
const friends = await this.prisma.friendship.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
@@ -183,6 +194,11 @@ export class StateGateway
|
|||||||
|
|
||||||
const currentUserId = client.data.userId;
|
const currentUserId = client.data.userId;
|
||||||
|
|
||||||
|
// Do not broadcast cursor position if user has no active doll
|
||||||
|
if (!client.data.activeDollId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentUserId) {
|
if (!currentUserId) {
|
||||||
this.logger.warn(`Could not find user ID for client ${client.id}`);
|
this.logger.warn(`Could not find user ID for client ${client.id}`);
|
||||||
return;
|
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