diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-03 00:47:45 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 00:42:23 +0900 |
| commit | 8529d652372c5c85e9624e23c4fc1a67bebb00b2 (patch) | |
| tree | 10811505b0f6d24f9d6bedaa7fac41bec5b8f92a | |
| parent | 790dcf942a3acab1268d46e3fc22be25a4ee02d5 (diff) | |
| download | LunaticChat-8529d652372c5c85e9624e23c4fc1a67bebb00b2.tar.gz LunaticChat-8529d652372c5c85e9624e23c4fc1a67bebb00b2.tar.bz2 LunaticChat-8529d652372c5c85e9624e23c4fc1a67bebb00b2.zip | |
perf: answer membership questions without copying member lists
isMember went through getChannelMembers, which copies a channel's whole
member list defensively - to then run any{} over it and throw the copy
away. getPlayerChannels did that once per channel, so asking "which
channels is this player in" allocated an ArrayList per channel and scanned
every one. It runs on join, and on every /lc channel join when a
membership limit is configured.
ChannelManager now answers both directly against the live lists: isMember
scans in place, and channelIdsOf walks the membership map once. No copies,
and getPlayerChannels no longer enumerates all channels separately to
cross-reference them.
Not done: the reviewed suggestion was a playerId -> channelIds reverse
index for O(1) lookups. That means a second record of who is in what,
updated by hand at five mutation sites, where drift shows up as a wrong
membership answer rather than a crash. The allocation was the real cost
here, and removing it does not introduce a fact stored twice.
Co-Authored-By: Claude <noreply@anthropic.com>
3 files changed, 65 insertions, 18 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt index 47780fb..07b5662 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt @@ -168,6 +168,44 @@ class ChannelManager( } /** + * Answers whether a player belongs to a channel without handing out the member list. + * + * [getChannelMembers] copies the list defensively, which is the wrong price to pay for a + * question that only needs to scan it. + * + * @param channelId The ID of the channel. + * @param playerId The UUID of the player. + * @return Result containing true if the player is a member. + * @throws ChannelNotFoundException if the channel does not exist. + */ + fun isMember( + channelId: String, + playerId: UUID, + ): Result<Boolean> { + channelsCache[channelId] + ?: return Result.failure(ChannelNotFoundException(channelId)) + + val members = membersCache[channelId] ?: return Result.success(false) + return Result.success(members.any { it.playerId == playerId }) + } + + /** + * Returns the ids of every existing channel the player belongs to. + * + * Walks the membership lists once in place; asking per channel meant copying every channel's + * member list to answer a question about one player. + * + * @param playerId The UUID of the player. + */ + fun channelIdsOf(playerId: UUID): List<String> = + membersCache + .asSequence() + .filter { (channelId, members) -> + channelsCache.containsKey(channelId) && members.any { it.playerId == playerId } + }.map { it.key } + .toList() + + /** * Adds a member to a channel. * * @param channelId The ID of the channel. diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt index 3833376..6ce2672 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt @@ -45,10 +45,7 @@ class ChannelMembershipManager( fun isMember( playerId: UUID, channelId: String, - ): Result<Boolean> = - channelManager.getChannelMembers(channelId).map { members -> - members.any { it.playerId == playerId } - } + ): Result<Boolean> = channelManager.isMember(channelId, playerId) /** * Gets the role of a member in a channel. @@ -338,18 +335,5 @@ class ChannelMembershipManager( * @param playerId The UUID of the player. * @return Result containing a list of channel IDs where the player is a member. */ - fun getPlayerChannels(playerId: UUID): Result<List<String>> { - val allChannels = - channelManager.getAllChannels().getOrElse { - return Result.failure(it) - } - - val playerChannels = - allChannels - .filter { channel -> - isMember(playerId, channel.id).getOrElse { false } - }.map { it.id } - - return Result.success(playerChannels) - } + fun getPlayerChannels(playerId: UUID): Result<List<String>> = Result.success(channelManager.channelIdsOf(playerId)) } diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt index 28e1c82..02c4d1a 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt @@ -472,4 +472,29 @@ class ChannelMembershipManagerTest { assertIs<ChannelPlayerBannedException>(result.exceptionOrNull()) } + + @Test + fun `isMember should fail for a channel that does not exist`() { + val (membership, _, _) = createManagers() + + val result = membership.isMember(createTestUUID(1), "no-such-ch") + + assertIs<ChannelNotFoundException>(result.exceptionOrNull()) + } + + @Test + fun `getPlayerChannels should not report a deleted channel`() { + val ownerId = createTestUUID(1) + val playerId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "keep-ch", name = "Keep", ownerId = ownerId)) + channelManager.createChannel(createTestChannel(id = "drop-ch", name = "Drop", ownerId = ownerId)) + membership.joinChannel(playerId, "keep-ch") + channelManager.setPlayerChannel(playerId, null) + membership.joinChannel(playerId, "drop-ch") + + channelManager.deleteChannel("drop-ch", ownerId) + + assertEquals(listOf("keep-ch"), membership.getPlayerChannels(playerId).getOrThrow()) + } } |
