@@ -18,17 +18,17 @@ export class GroupService {
1818 return null ;
1919 }
2020
21- // Use a more efficient query to find groups with exactly these members
2221 const sortedMemberIds = memberIds . sort ( ) ;
2322
24- // For 2-member groups (DMs), use a more efficient query
23+ // For 2-member groups (DMs), use a precise query that ensures exact match
2524 if ( sortedMemberIds . length === 2 ) {
26- // Use a subquery to find groups where both members are present
25+ // Find groups that are private and have exactly these 2 members
2726 const groups = await this . groupRepository
2827 . createQueryBuilder ( "group" )
2928 . leftJoinAndSelect ( "group.members" , "members" )
3029 . where ( "group.isPrivate = :isPrivate" , { isPrivate : true } )
3130 . andWhere ( ( qb ) => {
31+ // Subquery to find groups where both members are present
3232 const subQuery = qb . subQuery ( )
3333 . select ( "gm.group_id" )
3434 . from ( "group_members" , "gm" )
@@ -42,7 +42,7 @@ export class GroupService {
4242 } )
4343 . getMany ( ) ;
4444
45- // Filter groups that have exactly the same 2 members
45+ // Filter groups that have exactly the same 2 members (no more, no less)
4646 for ( const group of groups ) {
4747 if ( group . members && group . members . length === 2 ) {
4848 const groupMemberIds = group . members . map ( ( m : User ) => m . id ) . sort ( ) ;
@@ -55,15 +55,15 @@ export class GroupService {
5555 }
5656 }
5757
58- // Fallback to general search for other group sizes
59- const groups = await this . groupRepository
58+ // Fallback: get all private groups and filter in memory
59+ const allPrivateGroups = await this . groupRepository
6060 . createQueryBuilder ( "group" )
6161 . leftJoinAndSelect ( "group.members" , "members" )
6262 . where ( "group.isPrivate = :isPrivate" , { isPrivate : true } )
6363 . getMany ( ) ;
6464
6565 // Filter groups that have exactly the same members (order doesn't matter)
66- for ( const group of groups ) {
66+ for ( const group of allPrivateGroups ) {
6767 if ( ! group . members || group . members . length !== sortedMemberIds . length ) {
6868 continue ;
6969 }
@@ -99,16 +99,75 @@ export class GroupService {
9999 bannerUrl ?: string ,
100100 originalMatchParticipants ?: string [ ] ,
101101 ) : Promise < Group > {
102- // For eCurrency Chat groups, check if a DM already exists between these users
103- // This prevents duplicate chat creation in race conditions
102+ // For eCurrency Chat groups, use a transaction to prevent race conditions
104103 if ( isPrivate && ( name . startsWith ( "eCurrency Chat" ) || name . includes ( "eCurrency Chat" ) ) && memberIds . length === 2 ) {
105- const existingDM = await this . findGroupByMembers ( memberIds ) ;
106- if ( existingDM ) {
107- console . log ( `⚠️ DM already exists between users ${ memberIds . join ( ", " ) } , returning existing DM: ${ existingDM . id } ` ) ;
108- return existingDM ;
109- }
104+ return await AppDataSource . transaction ( async ( transactionalEntityManager ) => {
105+ // Check again within transaction to prevent race conditions
106+ const sortedMemberIds = memberIds . sort ( ) ;
107+ const existingGroups = await transactionalEntityManager
108+ . createQueryBuilder ( Group , "group" )
109+ . leftJoinAndSelect ( "group.members" , "members" )
110+ . where ( "group.isPrivate = :isPrivate" , { isPrivate : true } )
111+ . andWhere ( ( qb ) => {
112+ const subQuery = qb . subQuery ( )
113+ . select ( "gm.group_id" )
114+ . from ( "group_members" , "gm" )
115+ . where ( "gm.user_id IN (:...memberIds)" , {
116+ memberIds : sortedMemberIds
117+ } )
118+ . groupBy ( "gm.group_id" )
119+ . having ( "COUNT(DISTINCT gm.user_id) = :memberCount" , { memberCount : 2 } )
120+ . getQuery ( ) ;
121+ return "group.id IN " + subQuery ;
122+ } )
123+ . getMany ( ) ;
124+
125+ // Check if any group has exactly these 2 members
126+ for ( const group of existingGroups ) {
127+ if ( group . members && group . members . length === 2 ) {
128+ const groupMemberIds = group . members . map ( ( m : User ) => m . id ) . sort ( ) ;
129+ if ( groupMemberIds . length === sortedMemberIds . length &&
130+ groupMemberIds . every ( ( id : string , index : number ) => id === sortedMemberIds [ index ] ) ) {
131+ console . log ( `⚠️ DM already exists between users ${ memberIds . join ( ", " ) } , returning existing DM: ${ group . id } ` ) ;
132+ return group ;
133+ }
134+ }
135+ }
136+
137+ // No existing group found, create new one
138+ const members = await transactionalEntityManager . findBy ( User , {
139+ id : In ( memberIds ) ,
140+ } ) ;
141+ if ( members . length !== memberIds . length ) {
142+ throw new Error ( "One or more members not found" ) ;
143+ }
144+
145+ const admins = await transactionalEntityManager . findBy ( User , {
146+ id : In ( adminIds ) ,
147+ } ) ;
148+ if ( admins . length !== adminIds . length ) {
149+ throw new Error ( "One or more admins not found" ) ;
150+ }
151+
152+ const group = transactionalEntityManager . create ( Group , {
153+ name,
154+ description,
155+ owner,
156+ charter,
157+ members,
158+ admins,
159+ participants : members ,
160+ isPrivate,
161+ visibility,
162+ avatarUrl,
163+ bannerUrl,
164+ originalMatchParticipants : originalMatchParticipants || [ ] ,
165+ } ) ;
166+ return await transactionalEntityManager . save ( Group , group ) ;
167+ } ) ;
110168 }
111169
170+ // For non-DM groups, proceed normally
112171 const members = await this . userRepository . findBy ( {
113172 id : In ( memberIds ) ,
114173 } ) ;
0 commit comments