summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-03 00:47:45 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 00:42:23 +0900
commit8529d652372c5c85e9624e23c4fc1a67bebb00b2 (patch)
tree10811505b0f6d24f9d6bedaa7fac41bec5b8f92a
parent790dcf942a3acab1268d46e3fc22be25a4ee02d5 (diff)
downloadLunaticChat-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>
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt38
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt20
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt25
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())
+ }
}