From 363bb0400ce8bf81ee7d97095f3f8b17771db45c Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Mon, 25 Aug 2025 06:09:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=ED=94=BC=EB=B2=84=ED=83=80=EC=9E=84?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=EC=A0=90=EC=88=98=EA=B0=80?= =?UTF-8?q?=20=EB=91=90=EB=B0=B0=EA=B0=80=20=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/snackgame/core/domain/Snackgame.kt | 17 +++++++++++++ .../core/service/dto/StreaksRequest.kt | 25 ++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt index 73be78c..f786cbe 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt @@ -4,6 +4,7 @@ import com.snackgame.server.game.metadata.Metadata.SNACK_GAME import com.snackgame.server.game.session.domain.Session import com.snackgame.server.game.snackgame.core.domain.item.FeverTime import com.snackgame.server.game.snackgame.core.domain.snack.Snack +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithFever import java.time.Duration import javax.persistence.Convert import javax.persistence.Embedded @@ -32,6 +33,7 @@ open class Snackgame( this.score = score } + //todo : 제거 예정 fun remove(streak: Streak) { val removedSnacks = board.removeSnacksIn(streak) increaseScore(streak.length) @@ -41,6 +43,21 @@ open class Snackgame( } } + fun remove(streakWithFever: StreakWithFever) { + val streak = streakWithFever.streak + val removedSnacks = board.removeSnacksIn(streak) + + val serverIsFever = feverTime?.isActive(streakWithFever.occurredAt) == true + val isValid = streakWithFever.clientIsFever && serverIsFever + + val multiplier = if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER + increaseScore(streak.length * multiplier) + + if (removedSnacks.any(Snack::isGolden)) { + this.board = board.reset() + } + } + fun removeBomb(streak: Streak) { val removedSnacks = board.bombSnacksIn(streak) increaseScore(removedSnacks.size) diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt b/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt index 51671ec..f9ff013 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt @@ -3,12 +3,29 @@ package com.snackgame.server.game.snackgame.core.service.dto import com.fasterxml.jackson.annotation.JsonCreator import com.snackgame.server.game.snackgame.core.domain.Coordinate import com.snackgame.server.game.snackgame.core.domain.Streak +import java.time.LocalDateTime data class StreaksRequest @JsonCreator constructor( - val streaks: List> + val streaks: List ) { + fun toStreaks(now: LocalDateTime = LocalDateTime.now()): List = + streaks.map { it.toDomain(now) } +} - fun toStreaks(): List = streaks.map { streak -> - Streak.of(streak.map { Coordinate(it.y, it.x) }) - } +data class StreakWithMeta( + val coordinates: List, + val isFever: Boolean +) { + fun toDomain(now: LocalDateTime): StreakWithFever = + StreakWithFever( + streak = Streak.of(coordinates.map { Coordinate(it.y, it.x) }), + clientIsFever = isFever, + occurredAt = now + ) } + +data class StreakWithFever( + val streak: Streak, + val clientIsFever: Boolean, + val occurredAt: LocalDateTime +) \ No newline at end of file From a76212d10fc29f1e0b413320325e8167669c3108 Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Mon, 25 Aug 2025 06:19:20 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=20refactor:=20=EB=B0=94=EB=80=90=20dto?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20BIZ=20=EB=B0=8F=20test=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/snackgame/biz/domain/SnackgameBiz.kt | 30 +++++++++++++++++++ .../snackgame/biz/domain/SnackgameBizV2.kt | 29 ++++++++++++++++++ .../core/service/SnackgameServiceTest.kt | 30 +++++++++++++++++-- 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt index 3ff312d..89ac2fa 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt @@ -6,12 +6,17 @@ import com.snackgame.server.game.snackgame.core.domain.Board import com.snackgame.server.game.snackgame.core.domain.BoardConverter import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_HEIGHT import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_WIDTH +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.FEVER_MULTIPLIER +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.NORMAL_MULTIPLIER import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SESSION_TIME import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SPARE_TIME import com.snackgame.server.game.snackgame.core.domain.Streak +import com.snackgame.server.game.snackgame.core.domain.item.FeverTime import com.snackgame.server.game.snackgame.core.domain.snack.Snack +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithFever import java.time.Duration import javax.persistence.Convert +import javax.persistence.Embedded import javax.persistence.Entity import javax.persistence.Lob @@ -33,6 +38,11 @@ open class SnackgameBiz( this.score = score } + @Embedded + var feverTime: FeverTime? = null + private set + + fun remove(streak: Streak) { val removedSnacks = board.removeSnacksIn(streak) this.score += removedSnacks.size @@ -41,5 +51,25 @@ open class SnackgameBiz( } } + fun remove(streakWithFever: StreakWithFever) { + val streak = streakWithFever.streak + val removedSnacks = board.removeSnacksIn(streak) + + val serverIsFever = feverTime?.isActive(streakWithFever.occurredAt) == true + val isValid = streakWithFever.clientIsFever && serverIsFever + + val multiplier = if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER + increaseScore(streak.length * multiplier) + + if (removedSnacks.any(Snack::isGolden)) { + this.board = board.reset() + } + } + + private fun increaseScore(earn: Int) { + val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER + this.score += earn * multiplier + } + override val metadata = SNACK_GAME_BIZ } diff --git a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt index 0401262..146368a 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt @@ -6,12 +6,17 @@ import com.snackgame.server.game.snackgame.core.domain.Board import com.snackgame.server.game.snackgame.core.domain.BoardConverter import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_HEIGHT import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_WIDTH +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.FEVER_MULTIPLIER +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.NORMAL_MULTIPLIER import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SESSION_TIME import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SPARE_TIME import com.snackgame.server.game.snackgame.core.domain.Streak +import com.snackgame.server.game.snackgame.core.domain.item.FeverTime import com.snackgame.server.game.snackgame.core.domain.snack.Snack +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithFever import java.time.Duration import javax.persistence.Convert +import javax.persistence.Embedded import javax.persistence.Entity import javax.persistence.Lob @@ -28,6 +33,10 @@ open class SnackgameBizV2( var board = board private set + @Embedded + var feverTime: FeverTime? = null + private set + fun remove(streak: Streak) { val removedSnacks = board.removeSnacksIn(streak) this.score += removedSnacks.size @@ -36,5 +45,25 @@ open class SnackgameBizV2( } } + fun remove(streakWithFever: StreakWithFever) { + val streak = streakWithFever.streak + val removedSnacks = board.removeSnacksIn(streak) + + val serverIsFever = feverTime?.isActive(streakWithFever.occurredAt) == true + val isValid = streakWithFever.clientIsFever && serverIsFever + + val multiplier = if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER + increaseScore(streak.length * multiplier) + + if (removedSnacks.any(Snack::isGolden)) { + this.board = board.reset() + } + } + + private fun increaseScore(earn: Int) { + val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER + this.score += earn * multiplier + } + override val metadata = SNACK_GAME_BIZ_V2 } diff --git a/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt b/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt index 2d2c0d1..ea02c2c 100644 --- a/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt +++ b/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt @@ -2,10 +2,10 @@ package com.snackgame.server.game.snackgame.core.service -import com.snackgame.server.fixture.SeasonFixture import com.snackgame.server.game.snackgame.core.domain.Snackgame import com.snackgame.server.game.snackgame.core.domain.SnackgameRepository import com.snackgame.server.game.snackgame.core.service.dto.CoordinateRequest +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithMeta import com.snackgame.server.game.snackgame.core.service.dto.StreaksRequest import com.snackgame.server.game.snackgame.fixture.BoardFixture import com.snackgame.server.game.snackgame.fixture.ItemFixture @@ -39,7 +39,19 @@ class SnackgameServiceTest { CoordinateRequest(0, 0) ) - snackgameService.removeStreaks(땡칠().id, game.sessionId, StreaksRequest(listOf(coordinates))) + + snackgameService.removeStreaks( + 땡칠().id, + game.sessionId, + StreaksRequest( + listOf( + StreakWithMeta( + coordinates = coordinates, + isFever = false + ) + ) + ) + ) val found = snackgameRepository.findByOwnerIdAndSessionId(땡칠().id, game.sessionId)!! assertThat(found.score).isEqualTo(2) @@ -64,7 +76,19 @@ class SnackgameServiceTest { ) snackgameService.useFeverTime(땡칠().id, game.sessionId) - snackgameService.removeStreaks(땡칠().id, game.sessionId, StreaksRequest(listOf(coordinates))) + + snackgameService.removeStreaks( + 땡칠().id, + game.sessionId, + StreaksRequest( + listOf( + StreakWithMeta( + coordinates = coordinates, + isFever = false + ) + ) + ) + ) val found = snackgameRepository.findByOwnerIdAndSessionId(땡칠().id, game.sessionId)!! assertThat(found.score).isEqualTo(4) From 8956299e785cbb17248065f9901726ad969adc34 Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Wed, 27 Aug 2025 01:13:38 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=ED=94=BC=EB=B2=84=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=A4=91=20=EC=9D=BC=EC=8B=9C=EC=A0=95=EC=A7=80=20=EC=8B=9C=20?= =?UTF-8?q?=ED=94=BC=EB=B2=84=ED=83=80=EC=9E=84=EB=8F=84=20=EB=A9=88?= =?UTF-8?q?=EC=B6=94=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/session/event/SessionEndEvent.kt | 6 ++--- .../game/session/event/SessionPauseEvent.kt | 21 +++++++++++++++ .../game/session/event/SessionResumeEvent.kt | 21 +++++++++++++++ .../snackgame/core/domain/item/FeverTime.kt | 22 ++++++++++++++-- .../core/domain/item/FeverTimeListener.kt | 26 +++++++++++++++++++ .../core/service/SnackgameService.kt | 4 +++ .../core/service/SnackgameServiceTest.kt | 17 ++++++++++++ 7 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/snackgame/server/game/session/event/SessionPauseEvent.kt create mode 100644 src/main/java/com/snackgame/server/game/session/event/SessionResumeEvent.kt create mode 100644 src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt index 9b65a59..a36f459 100644 --- a/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt +++ b/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt @@ -5,10 +5,10 @@ import com.snackgame.server.game.session.domain.Session data class SessionEndEvent( val metadata: Metadata, - val ownerId: Long, - val sessionId: Long, + override val ownerId: Long, + override val sessionId: Long, val score: Int -) { +) : SessionStateEvent { companion object { fun of(session: Session): SessionEndEvent { diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionPauseEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionPauseEvent.kt new file mode 100644 index 0000000..0383da1 --- /dev/null +++ b/src/main/java/com/snackgame/server/game/session/event/SessionPauseEvent.kt @@ -0,0 +1,21 @@ +package com.snackgame.server.game.session.event + +import com.snackgame.server.game.session.domain.Session +import java.time.LocalDateTime + +data class SessionPauseEvent( + override val sessionId: Long, + override val ownerId: Long, + val occurredAt: LocalDateTime +) : SessionStateEvent { + + companion object { + fun of(session: Session): SessionPauseEvent { + return SessionPauseEvent( + session.sessionId, + session.ownerId, + LocalDateTime.now() + ) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionResumeEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionResumeEvent.kt new file mode 100644 index 0000000..f993a83 --- /dev/null +++ b/src/main/java/com/snackgame/server/game/session/event/SessionResumeEvent.kt @@ -0,0 +1,21 @@ +package com.snackgame.server.game.session.event + +import com.snackgame.server.game.session.domain.Session +import java.time.LocalDateTime + +data class SessionResumeEvent( + override val sessionId: Long, + override val ownerId: Long, + val occurredAt: LocalDateTime +) : SessionStateEvent { + + companion object { + fun of(session: Session): SessionResumeEvent { + return SessionResumeEvent( + session.sessionId, + session.ownerId, + LocalDateTime.now() + ) + } + } +} diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt index ed77a4c..ead7eaf 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt @@ -6,10 +6,28 @@ import javax.persistence.Embeddable @Embeddable class FeverTime( - private val feverStartedAt: LocalDateTime? = null + private var feverStartedAt: LocalDateTime? = null, + private var feverPausedAt: LocalDateTime? = null ) { fun isActive(now: LocalDateTime = LocalDateTime.now()): Boolean { - return feverStartedAt != null && Duration.between(feverStartedAt, now) < DURATION + if (feverStartedAt == null) return false + val effectiveStart = feverPausedAt?.let { feverStartedAt!!.plus(Duration.between(it, now)) } ?: feverStartedAt + return Duration.between(effectiveStart, now) < DURATION + } + + fun pause() { + if (feverStartedAt != null && feverPausedAt == null) { + feverPausedAt = LocalDateTime.now() + } + } + + fun resume() { + if (feverStartedAt != null && feverPausedAt != null) { + val now = LocalDateTime.now() + val pausedDuration = Duration.between(feverPausedAt, now) + feverStartedAt = feverStartedAt!!.plus(pausedDuration) + feverPausedAt = null + } } companion object { diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt new file mode 100644 index 0000000..76bcab4 --- /dev/null +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt @@ -0,0 +1,26 @@ +package com.snackgame.server.game.snackgame.core.domain.item + +import com.snackgame.server.game.session.event.SessionPauseEvent +import com.snackgame.server.game.session.event.SessionResumeEvent +import com.snackgame.server.game.snackgame.core.domain.SnackgameRepository +import com.snackgame.server.game.snackgame.core.domain.getBy +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class FeverTimeListener( + private val snackgameRepository: SnackgameRepository +) { + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + fun onSessionPaused(event: SessionPauseEvent) { + val game = snackgameRepository.getBy(event.ownerId, event.sessionId) + game.feverTime?.pause() + } + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + fun onSessionResumed(event: SessionResumeEvent) { + val game = snackgameRepository.getBy(event.ownerId, event.sessionId) + game.feverTime?.resume() + } +} \ No newline at end of file diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt b/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt index 3473a33..c89752b 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt @@ -2,6 +2,8 @@ package com.snackgame.server.game.snackgame.core.service import com.snackgame.server.game.session.event.SessionEndEvent +import com.snackgame.server.game.session.event.SessionPauseEvent +import com.snackgame.server.game.session.event.SessionResumeEvent import com.snackgame.server.game.snackgame.core.domain.Snackgame import com.snackgame.server.game.snackgame.core.domain.SnackgameRepository import com.snackgame.server.game.snackgame.core.domain.Streak @@ -78,6 +80,7 @@ class SnackgameService( val game = snackGameRepository.getBy(memberId, sessionId) game.pause() + eventPublisher.publishEvent(SessionPauseEvent.of(game)) return SnackgameResponse.of(game) } @@ -87,6 +90,7 @@ class SnackgameService( val game = snackGameRepository.getBy(memberId, sessionId) game.resume() + eventPublisher.publishEvent(SessionResumeEvent.of(game)) return SnackgameResponse.of(game) } diff --git a/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt b/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt index ea02c2c..0384d49 100644 --- a/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt +++ b/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt @@ -15,6 +15,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDateTime @ServiceTest class SnackgameServiceTest { @@ -93,4 +94,20 @@ class SnackgameServiceTest { val found = snackgameRepository.findByOwnerIdAndSessionId(땡칠().id, game.sessionId)!! assertThat(found.score).isEqualTo(4) } + + @Test + fun `피버타임 pause 후 resume 시 남은 시간이 유지된다`() { + val game = snackgameRepository.save(Snackgame(1L, BoardFixture.TWO_BY_FOUR())) + + game.startFeverTime() + val feverTime = game.feverTime!! + + Thread.sleep(1000) + snackgameService.pause(game.ownerId, game.sessionId) + + snackgameService.resume(game.ownerId, game.sessionId) + + val activeAfterResume = feverTime.isActive(LocalDateTime.now().plusSeconds(28)) + assertThat(activeAfterResume).isTrue() + } } From d4e137f969ce7804f7bc9fb83d7d3ad1bdcbc8f3 Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Wed, 27 Aug 2025 01:14:04 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EB=AC=B6=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/game/session/event/SessionStateEvent.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/com/snackgame/server/game/session/event/SessionStateEvent.kt diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionStateEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionStateEvent.kt new file mode 100644 index 0000000..2862c5b --- /dev/null +++ b/src/main/java/com/snackgame/server/game/session/event/SessionStateEvent.kt @@ -0,0 +1,6 @@ +package com.snackgame.server.game.session.event + +interface SessionStateEvent { + val sessionId: Long + val ownerId: Long +}