diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 0e59d0308d..0d5f989c11 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -234,10 +234,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { map: Reversi.maps.eighteight.data, bw: 'random', isLlotheo: false, - }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); + }).then(x => this.reversiGamesRepository.findOneOrFail({ + where: { id: x.identifiers[0].id }, + relations: ['user1', 'user2'], + })); this.cacheGame(game); - const packed = await this.reversiGameEntityService.packDetail(game, { id: parentId }); + const packed = await this.reversiGameEntityService.packDetail(game); this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed }); return game; @@ -267,6 +270,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { .returning('*') .execute() .then((response) => response.raw[0]); + // キャッシュ効率化のためにユーザー情報は再利用 + updatedGame.user1 = game.user1; + updatedGame.user2 = game.user2; this.cacheGame(updatedGame); //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 @@ -314,6 +320,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { .returning('*') .execute() .then((response) => response.raw[0]); + // キャッシュ効率化のためにユーザー情報は再利用 + updatedGame.user1 = game.user1; + updatedGame.user2 = game.user2; this.cacheGame(updatedGame); this.globalEventService.publishReversiGameStream(game.id, 'ended', { @@ -483,14 +492,36 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { public async get(id: MiReversiGame['id']): Promise { const cached = await this.redisClient.get(`reversi:game:cache:${id}`); if (cached != null) { + // TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい const parsed = JSON.parse(cached) as Serialized; return { ...parsed, startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null, endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null, + user1: parsed.user1 != null ? { + ...parsed.user1, + avatar: null, + banner: null, + updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null, + lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null, + lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null, + movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null, + } : null, + user2: parsed.user2 != null ? { + ...parsed.user2, + avatar: null, + banner: null, + updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null, + lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null, + lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null, + movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null, + } : null, }; } else { - const game = await this.reversiGamesRepository.findOneBy({ id }); + const game = await this.reversiGamesRepository.findOne({ + where: { id }, + relations: ['user1', 'user2'], + }); if (game == null) return null; this.cacheGame(game); diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts index bcb0fd5a6f..6c89a70599 100644 --- a/packages/backend/src/core/entities/ReversiGameEntityService.ts +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -9,7 +9,6 @@ import type { ReversiGamesRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; import type { MiReversiGame } from '@/models/ReversiGame.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; @@ -29,10 +28,14 @@ export class ReversiGameEntityService { @bindThis public async packDetail( src: MiReversiGame['id'] | MiReversiGame, - me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); + const users = await Promise.all([ + this.userEntityService.pack(game.user1 ?? game.user1Id), + this.userEntityService.pack(game.user2 ?? game.user2Id), + ]); + return await awaitAll({ id: game.id, createdAt: this.idService.parse(game.id).date.toISOString(), @@ -46,10 +49,10 @@ export class ReversiGameEntityService { user2Ready: game.user2Ready, user1Id: game.user1Id, user2Id: game.user2Id, - user1: this.userEntityService.pack(game.user1Id, me), - user2: this.userEntityService.pack(game.user2Id, me), + user1: users[0], + user2: users[1], winnerId: game.winnerId, - winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, + winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null, surrenderedUserId: game.surrenderedUserId, timeoutUserId: game.timeoutUserId, black: game.black, @@ -66,18 +69,21 @@ export class ReversiGameEntityService { @bindThis public packDetailMany( xs: MiReversiGame[], - me?: { id: MiUser['id'] } | null | undefined, ) { - return Promise.all(xs.map(x => this.packDetail(x, me))); + return Promise.all(xs.map(x => this.packDetail(x))); } @bindThis public async packLite( src: MiReversiGame['id'] | MiReversiGame, - me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); + const users = await Promise.all([ + this.userEntityService.pack(game.user1 ?? game.user1Id), + this.userEntityService.pack(game.user2 ?? game.user2Id), + ]); + return await awaitAll({ id: game.id, createdAt: this.idService.parse(game.id).date.toISOString(), @@ -85,16 +91,12 @@ export class ReversiGameEntityService { endedAt: game.endedAt && game.endedAt.toISOString(), isStarted: game.isStarted, isEnded: game.isEnded, - form1: game.form1, - form2: game.form2, - user1Ready: game.user1Ready, - user2Ready: game.user2Ready, user1Id: game.user1Id, user2Id: game.user2Id, - user1: this.userEntityService.pack(game.user1Id, me), - user2: this.userEntityService.pack(game.user2Id, me), + user1: users[0], + user2: users[1], winnerId: game.winnerId, - winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, + winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null, surrenderedUserId: game.surrenderedUserId, timeoutUserId: game.timeoutUserId, black: game.black, @@ -109,9 +111,8 @@ export class ReversiGameEntityService { @bindThis public packLiteMany( xs: MiReversiGame[], - me?: { id: MiUser['id'] } | null | undefined, ) { - return Promise.all(xs.map(x => this.packLite(x, me))); + return Promise.all(xs.map(x => this.packLite(x))); } } diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts index 6e8b79c03d..8061d84ad6 100644 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -34,22 +34,6 @@ export const packedReversiGameLiteSchema = { type: 'boolean', optional: false, nullable: false, }, - form1: { - type: 'object', - optional: false, nullable: true, - }, - form2: { - type: 'object', - optional: false, nullable: true, - }, - user1Ready: { - type: 'boolean', - optional: false, nullable: false, - }, - user2Ready: { - type: 'boolean', - optional: false, nullable: false, - }, user1Id: { type: 'string', optional: false, nullable: false, @@ -149,11 +133,11 @@ export const packedReversiGameDetailedSchema = { optional: false, nullable: false, }, form1: { - type: 'object', + type: 'any', optional: false, nullable: true, }, form2: { - type: 'object', + type: 'any', optional: false, nullable: true, }, user1Ready: { diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts index 5322cd0987..f28fe5d987 100644 --- a/packages/backend/src/server/api/endpoints/reversi/games.ts +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -43,7 +43,9 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) - .andWhere('game.isStarted = TRUE'); + .andWhere('game.isStarted = TRUE') + .innerJoinAndSelect('game.user1', 'user1') + .innerJoinAndSelect('game.user2', 'user2'); if (ps.my && me) { query.andWhere(new Brackets(qb => { @@ -55,7 +57,7 @@ export default class extends Endpoint { // eslint- const games = await query.take(ps.limit).getMany(); - return await this.reversiGameEntityService.packLiteMany(games, me); + return await this.reversiGameEntityService.packLiteMany(games); }); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index da5a3409ef..1065ce5a89 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -60,7 +60,7 @@ export default class extends Endpoint { // eslint- if (game == null) return; - return await this.reversiGameEntityService.packDetail(game, me); + return await this.reversiGameEntityService.packDetail(game); }); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts index de571053e1..86645ea4b4 100644 --- a/packages/backend/src/server/api/endpoints/reversi/show-game.ts +++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts @@ -48,7 +48,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchGame); } - return await this.reversiGameEntityService.packDetail(game, me); + return await this.reversiGameEntityService.packDetail(game); }); } } diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts index df92137f51..820c80006b 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -42,7 +42,7 @@ class ReversiGameChannel extends Channel { case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'cancel': this.cancelGame(); break; case 'putStone': this.putStone(body.pos, body.id); break; - case 'checkState': this.checkState(body.crc32); break; + case 'resync': this.resync(body.crc32); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; } } @@ -76,12 +76,10 @@ class ReversiGameChannel extends Channel { } @bindThis - private async checkState(crc32: string | number) { - if (crc32 != null) return; - + private async resync(crc32: string | number) { const game = await this.reversiService.checkCrc(this.gameId!, crc32); if (game) { - this.send('rescue', game); + this.send('resynced', game); } } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 361a4931eb..cfac5cd9d4 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -277,7 +277,11 @@ export type Serialized = { ? (string | null) : T[K] extends Record ? Serialized - : T[K]; + : T[K] extends (Record | null) + ? (Serialized | null) + : T[K] extends (Record | undefined) + ? (Serialized | undefined) + : T[K]; }; export type FilterUnionByProperty< diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 4d4450ed7d..d492296c16 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -163,7 +163,7 @@ const $i = signinRequired(); const props = defineProps<{ game: Misskey.entities.ReversiGameDetailed; - connection: Misskey.ChannelConnection; + connection?: Misskey.ChannelConnection | null; }>(); const showBoardLabels = ref(false); @@ -240,10 +240,10 @@ watch(logPos, (v) => { if (game.value.isStarted && !game.value.isEnded) { useInterval(() => { - if (game.value.isEnded) return; + if (game.value.isEnded || props.connection == null) return; const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString(); if (_DEV_) console.log('crc32', crc32); - props.connection.send('checkState', { + props.connection.send('resync', { crc32: crc32, }); }, 10000, { immediate: false, afterMounted: true }); @@ -267,7 +267,7 @@ function putStone(pos) { }); const id = Math.random().toString(36).slice(2); - props.connection.send('putStone', { + props.connection!.send('putStone', { pos: pos, id, }); @@ -283,22 +283,24 @@ const myTurnTimerRmain = ref(game.value.timeLimitForEachTurn); const opTurnTimerRmain = ref(game.value.timeLimitForEachTurn); const TIMER_INTERVAL_SEC = 3; -useInterval(() => { - if (myTurnTimerRmain.value > 0) { - myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC); - } - if (opTurnTimerRmain.value > 0) { - opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC); - } - - if (iAmPlayer.value) { - if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) { - props.connection.send('claimTimeIsUp', {}); +if (!props.game.isEnded) { + useInterval(() => { + if (myTurnTimerRmain.value > 0) { + myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC); + } + if (opTurnTimerRmain.value > 0) { + opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC); } - } -}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true }); -function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { + if (iAmPlayer.value) { + if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) { + props.connection!.send('claimTimeIsUp', {}); + } + } + }, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true }); +} + +async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { game.value.logs = Reversi.Serializer.serializeLogs([ ...Reversi.Serializer.deserializeLogs(game.value.logs), log, @@ -309,17 +311,25 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { if (log.id == null || !appliedOps.includes(log.id)) { switch (log.operation) { case 'put': { + sound.playUrl('/client-assets/reversi/put.mp3', { + volume: 1, + playbackRate: 1, + }); + + if (log.player !== engine.value.turn) { // = desyncが発生している + const _game = await misskeyApi('reversi/show-game', { + gameId: props.game.id, + }); + restoreGame(_game); + return; + } + engine.value.putStone(log.pos); triggerRef(engine); myTurnTimerRmain.value = game.value.timeLimitForEachTurn; opTurnTimerRmain.value = game.value.timeLimitForEachTurn; - sound.playUrl('/client-assets/reversi/put.mp3', { - volume: 1, - playbackRate: 1, - }); - checkEnd(); break; } @@ -366,9 +376,7 @@ function checkEnd() { } } -function onStreamRescue(_game) { - console.log('rescue'); - +function restoreGame(_game) { game.value = deepClone(_game); engine.value = Reversi.Serializer.restoreGame({ @@ -384,6 +392,12 @@ function onStreamRescue(_game) { checkEnd(); } +function onStreamResynced(_game) { + console.log('resynced'); + + restoreGame(_game); +} + async function surrender() { const { canceled } = await os.confirm({ type: 'warning', @@ -434,27 +448,35 @@ function share() { } onMounted(() => { - props.connection.on('log', onStreamLog); - props.connection.on('rescue', onStreamRescue); - props.connection.on('ended', onStreamEnded); + if (props.connection != null) { + props.connection.on('log', onStreamLog); + props.connection.on('resynced', onStreamResynced); + props.connection.on('ended', onStreamEnded); + } }); onActivated(() => { - props.connection.on('log', onStreamLog); - props.connection.on('rescue', onStreamRescue); - props.connection.on('ended', onStreamEnded); + if (props.connection != null) { + props.connection.on('log', onStreamLog); + props.connection.on('resynced', onStreamResynced); + props.connection.on('ended', onStreamEnded); + } }); onDeactivated(() => { - props.connection.off('log', onStreamLog); - props.connection.off('rescue', onStreamRescue); - props.connection.off('ended', onStreamEnded); + if (props.connection != null) { + props.connection.off('log', onStreamLog); + props.connection.off('resynced', onStreamResynced); + props.connection.off('ended', onStreamEnded); + } }); onUnmounted(() => { - props.connection.off('log', onStreamLog); - props.connection.off('rescue', onStreamRescue); - props.connection.off('ended', onStreamEnded); + if (props.connection != null) { + props.connection.off('log', onStreamLog); + props.connection.off('resynced', onStreamResynced); + props.connection.off('ended', onStreamEnded); + } }); diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 0bdbfbcf54..d1e4103919 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -47,23 +47,25 @@ async function fetchGame() { if (connection.value) { connection.value.dispose(); } - connection.value = useStream().useChannel('reversiGame', { - gameId: game.value.id, - }); - connection.value.on('started', x => { - game.value = x.game; - }); - connection.value.on('canceled', x => { - connection.value?.dispose(); + if (!game.value.isEnded) { + connection.value = useStream().useChannel('reversiGame', { + gameId: game.value.id, + }); + connection.value.on('started', x => { + game.value = x.game; + }); + connection.value.on('canceled', x => { + connection.value?.dispose(); - if (x.userId !== $i.id) { - os.alert({ - type: 'warning', - text: i18n.ts._reversi.gameCanceled, - }); - router.push('/reversi'); - } - }); + if (x.userId !== $i.id) { + os.alert({ + type: 'warning', + text: i18n.ts._reversi.gameCanceled, + }); + router.push('/reversi'); + } + }); + } } onMounted(() => { diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 65d6dd19dc..d81444e5df 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.2 - * generatedAt: 2024-01-22T02:39:29.673Z + * generatedAt: 2024-01-22T06:08:45.879Z */ 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 0959abfe96..69f02b899f 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.2 - * generatedAt: 2024-01-22T02:39:29.671Z + * generatedAt: 2024-01-22T06:08:45.877Z */ import type { diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index f67414b8e0..5d46ea6611 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.2 - * generatedAt: 2024-01-22T02:39:29.669Z + * generatedAt: 2024-01-22T06:08:45.876Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 034a54c0eb..3e795f2b86 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.2 - * generatedAt: 2024-01-22T02:39:29.668Z + * generatedAt: 2024-01-22T06:08:45.875Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index a086abf033..271ca41159 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3,7 +3,7 @@ /* * version: 2024.2.0-beta.2 - * generatedAt: 2024-01-22T02:39:29.585Z + * generatedAt: 2024-01-22T06:08:45.796Z */ /** @@ -4469,10 +4469,6 @@ export type components = { endedAt: string | null; isStarted: boolean; isEnded: boolean; - form1: Record | null; - form2: Record | null; - user1Ready: boolean; - user2Ready: boolean; /** Format: id */ user1Id: string; /** Format: id */