From 65557d5f27044bd90c538266fde1e6b91b696f80 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 24 Jan 2024 10:16:05 +0900
Subject: [PATCH] enhance(reversi): more robust matching process

---
 packages/backend/src/core/ReversiService.ts   | 62 +++++++++++++++----
 .../queue/processors/CleanProcessorService.ts |  4 ++
 .../src/server/api/endpoints/reversi/match.ts |  3 +-
 packages/frontend/src/pages/reversi/index.vue |  6 +-
 .../misskey-js/src/autogen/apiClientJSDoc.ts  |  4 +-
 packages/misskey-js/src/autogen/endpoint.ts   |  4 +-
 packages/misskey-js/src/autogen/entities.ts   |  4 +-
 packages/misskey-js/src/autogen/models.ts     |  4 +-
 packages/misskey-js/src/autogen/types.ts      |  6 +-
 9 files changed, 73 insertions(+), 24 deletions(-)

diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 66296f1ed4..1c8635212d 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import * as Redis from 'ioredis';
 import { ModuleRef } from '@nestjs/core';
 import * as Reversi from 'misskey-reversi';
-import { IsNull } from 'typeorm';
+import { IsNull, LessThan } from 'typeorm';
 import type {
 	MiReversiGame,
 	ReversiGamesRepository,
@@ -24,7 +24,7 @@ import { Serialized } from '@/types.js';
 import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
 import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
 
-const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
+const MATCHING_TIMEOUT_MS = 1000 * 10; // 10sec
 
 @Injectable()
 export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@@ -89,11 +89,27 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 	}
 
 	@bindThis
-	public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
+	public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise<MiReversiGame | null> {
 		if (targetUser.id === me.id) {
 			throw new Error('You cannot match yourself.');
 		}
 
+		if (!multiple) {
+			// 既にマッチしている対局が無いか探す(3分以内)
+			const games = await this.reversiGamesRepository.find({
+				where: [
+					{ id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false },
+					{ id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false },
+				],
+				relations: ['user1', 'user2'],
+				order: { id: 'DESC' },
+			});
+			if (games.length > 0) {
+				return games[0];
+			}
+		}
+
+		//#region 相手から既に招待されてないか確認
 		const invitations = await this.redisClient.zrange(
 			`reversi:matchSpecific:${me.id}`,
 			Date.now() - MATCHING_TIMEOUT_MS,
@@ -106,19 +122,35 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 			const game = await this.matched(targetUser.id, me.id);
 
 			return game;
-		} else {
-			this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
+		}
+		//#endregion
 
-			this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
-				user: await this.userEntityService.pack(me, targetUser),
-			});
+		this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
 
-			return null;
-		}
+		this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
+			user: await this.userEntityService.pack(me, targetUser),
+		});
+
+		return null;
 	}
 
 	@bindThis
-	public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
+	public async matchAnyUser(me: MiUser, multiple = false): Promise<MiReversiGame | null> {
+		if (!multiple) {
+			// 既にマッチしている対局が無いか探す(3分以内)
+			const games = await this.reversiGamesRepository.find({
+				where: [
+					{ id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false },
+					{ id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false },
+				],
+				relations: ['user1', 'user2'],
+				order: { id: 'DESC' },
+			});
+			if (games.length > 0) {
+				return games[0];
+			}
+		}
+
 		//#region まず自分宛ての招待を探す
 		const invitations = await this.redisClient.zrange(
 			`reversi:matchSpecific:${me.id}`,
@@ -169,6 +201,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 		await this.redisClient.zrem('reversi:matchAny', user.id);
 	}
 
+	@bindThis
+	public async cleanOutdatedGames() {
+		await this.reversiGamesRepository.delete({
+			id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)),
+			isStarted: false,
+		});
+	}
+
 	@bindThis
 	public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) {
 		const game = await this.get(gameId);
diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts
index e252c5d8a1..17b6c8ba0c 100644
--- a/packages/backend/src/queue/processors/CleanProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanProcessorService.ts
@@ -11,6 +11,7 @@ import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
 import type { Config } from '@/config.js';
+import { ReversiService } from '@/core/ReversiService.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
 
@@ -32,6 +33,7 @@ export class CleanProcessorService {
 		private roleAssignmentsRepository: RoleAssignmentsRepository,
 
 		private queueLoggerService: QueueLoggerService,
+		private reversiService: ReversiService,
 		private idService: IdService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('clean');
@@ -65,6 +67,8 @@ export class CleanProcessorService {
 			});
 		}
 
+		this.reversiService.cleanOutdatedGames();
+
 		this.logger.succ('Cleaned.');
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts
index 1065ce5a89..62682cfb50 100644
--- a/packages/backend/src/server/api/endpoints/reversi/match.ts
+++ b/packages/backend/src/server/api/endpoints/reversi/match.ts
@@ -37,6 +37,7 @@ export const paramDef = {
 	type: 'object',
 	properties: {
 		userId: { type: 'string', format: 'misskey:id', nullable: true },
+		multiple: { type: 'boolean', default: false },
 	},
 	required: [],
 } as const;
@@ -56,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw err;
 			}) : null;
 
-			const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me);
+			const game = target ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) : await this.reversiService.matchAnyUser(me, ps.multiple);
 
 			if (game == null) return;
 
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index 4c6c99ae51..09f742b83e 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -139,7 +139,9 @@ if ($i) {
 	const connection = useStream().useChannel('reversi');
 
 	connection.on('matched', x => {
-		startGame(x.game);
+		if (matchingUser.value != null || matchingAny.value) {
+			startGame(x.game);
+		}
 	});
 
 	connection.on('invited', invitation => {
@@ -222,7 +224,7 @@ async function accept(user) {
 	}
 }
 
-useInterval(matchHeatbeat, 1000 * 10, { immediate: false, afterMounted: true });
+useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
 
 onMounted(() => {
 	misskeyApi('reversi/invitations').then(_invitations => {
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index fe04658deb..4fa42bb919 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2024.2.0-beta.3
- * generatedAt: 2024-01-23T01:22:13.177Z
+ * version: 2024.2.0-beta.4
+ * generatedAt: 2024-01-24T01:14:40.901Z
  */
 
 import type { SwitchCaseResponseType } from '../api.js';
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 0060003031..6b330c7c11 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2024.2.0-beta.3
- * generatedAt: 2024-01-23T01:22:13.175Z
+ * version: 2024.2.0-beta.4
+ * generatedAt: 2024-01-24T01:14:40.899Z
  */
 
 import type {
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 55afcdeb31..8eb284ae9e 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2024.2.0-beta.3
- * generatedAt: 2024-01-23T01:22:13.173Z
+ * version: 2024.2.0-beta.4
+ * generatedAt: 2024-01-24T01:14:40.897Z
  */
 
 import { operations } from './types.js';
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index c94dfaa25e..0a6f534aff 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2024.2.0-beta.3
- * generatedAt: 2024-01-23T01:22:13.172Z
+ * version: 2024.2.0-beta.4
+ * generatedAt: 2024-01-24T01:14:40.896Z
  */
 
 import { components } from './types.js';
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 761304ef28..44421240d0 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -2,8 +2,8 @@
 /* eslint @typescript-eslint/no-explicit-any: 0 */
 
 /*
- * version: 2024.2.0-beta.3
- * generatedAt: 2024-01-23T01:22:13.093Z
+ * version: 2024.2.0-beta.4
+ * generatedAt: 2024-01-24T01:14:40.815Z
  */
 
 /**
@@ -25799,6 +25799,8 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           userId?: string | null;
+          /** @default false */
+          multiple?: boolean;
         };
       };
     };
-- 
GitLab