From 0471e83b545fc36d82e239b822d0e8c314c7d01b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:37:27 +0000 Subject: [PATCH 1/4] Initial plan From ea677c5b9d43a8919b53a6c75b84b3efcfb05787 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:41:44 +0000 Subject: [PATCH 2/4] fix: preserve topological order by using priority as tie-breaker during DFS Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../slne/surf/surfapi/core/server/hook/HookService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 3c26b785..84d3fef3 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -196,7 +196,11 @@ abstract class HookService { visiting.add(className) val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList() - for (depClassName in dependencies) { + // Sort dependencies by priority to use priority as a tie-breaker + val sortedDeps = dependencies.sortedBy { depClassName -> + hooksByClassName[depClassName]?.priority ?: Short.MAX_VALUE + } + for (depClassName in sortedDeps) { if (depClassName in hooksByClassName) { visit(depClassName) } @@ -208,8 +212,9 @@ abstract class HookService { hooksByClassName[className]?.let { sorted.add(it) } } - validHooks.forEach { (meta, _) -> visit(meta.className) } - return sorted.sortedBy { it.priority } + // Sort validHooks by priority before visiting to use priority as a tie-breaker + validHooks.sortedBy { (_, hook) -> hook.priority }.forEach { (meta, _) -> visit(meta.className) } + return sorted } private fun buildCircularDependencyChain( From 1a1a0d3e64d7513806d731ff46e41d3e3fef9200 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:43:16 +0000 Subject: [PATCH 3/4] refactor: filter dependencies before sorting by priority Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../slne/surf/surfapi/core/server/hook/HookService.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 84d3fef3..5f98d2dc 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -197,13 +197,12 @@ abstract class HookService { val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList() // Sort dependencies by priority to use priority as a tie-breaker - val sortedDeps = dependencies.sortedBy { depClassName -> - hooksByClassName[depClassName]?.priority ?: Short.MAX_VALUE - } + // Only sort dependencies that are actually loaded + val sortedDeps = dependencies + .filter { it in hooksByClassName } + .sortedBy { hooksByClassName[it]!!.priority } for (depClassName in sortedDeps) { - if (depClassName in hooksByClassName) { - visit(depClassName) - } + visit(depClassName) } visiting.remove(className) From 0d30fd4e539f34873337baa0ae90d7b0e6fcaecf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:47:50 +0000 Subject: [PATCH 4/4] refactor: use Kahn's algorithm with priority queue for topological sort Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../surfapi/core/server/hook/HookService.kt | 106 ++++++++++++------ 1 file changed, 70 insertions(+), 36 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 5f98d2dc..e2bfd2a2 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -2,7 +2,9 @@ package dev.slne.surf.surfapi.core.server.hook import com.github.benmanes.caffeine.cache.Caffeine import com.sksamuel.aedile.core.asLoadingCache +import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf import dev.slne.surf.surfapi.core.api.util.requiredService import dev.slne.surf.surfapi.shared.api.hook.Hook @@ -13,6 +15,7 @@ import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import kotlinx.serialization.SerializationException import net.kyori.adventure.text.logger.slf4j.ComponentLogger import java.io.InputStream +import java.util.PriorityQueue abstract class HookService { @@ -179,58 +182,89 @@ abstract class HookService { return emptyList() } - val sorted = mutableListOf() - val visited = mutableSetOf() - val visiting = mutableSetOf() - - fun visit(className: String) { - if (className in visited) return + // Build dependency graph: className -> list of dependents (successors) + // Note: In Kahn's algorithm, edges go from dependency to dependent + val graph = mutableObject2ObjectMapOf>() + for ((meta, _) in validHooks) { + // Ensure all nodes exist in the graph + if (meta.className !in graph) { + graph[meta.className] = mutableListOf() + } + // Add edges from dependencies to this hook + for (depClassName in meta.hookDependencies) { + if (depClassName in hooksByClassName) { + graph.computeIfAbsent(depClassName) { mutableListOf() }.add(meta.className) + } + } + } - if (className in visiting) { - val chain = buildCircularDependencyChain(className, metaByClassName, visiting) - throw IllegalStateException( - "Circular hook dependency detected: ${chain.joinToString(" -> ")}" - ) + // Kahn's algorithm with priority queue for tie-breaking + val incomingEdges = mutableObject2IntMapOf() + for ((vertex, successors) in graph) { + if (vertex !in incomingEdges) { + incomingEdges[vertex] = 0 + } + for (successor in successors) { + incomingEdges.mergeInt(successor, 1, Int::plus) } + } - visiting.add(className) + // Use a priority queue ordered by hook priority (lower priority value = higher priority) + val queue = PriorityQueue(compareBy { className -> + hooksByClassName[className]?.priority ?: Short.MAX_VALUE + }) + + incomingEdges.object2IntEntrySet().fastForEach { entry -> + val vertex = entry.key + val edges = entry.intValue + if (edges == 0) queue += vertex + } - val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList() - // Sort dependencies by priority to use priority as a tie-breaker - // Only sort dependencies that are actually loaded - val sortedDeps = dependencies - .filter { it in hooksByClassName } - .sortedBy { hooksByClassName[it]!!.priority } - for (depClassName in sortedDeps) { - visit(depClassName) - } + val result = mutableObjectListOf() - visiting.remove(className) - visited.add(className) + while (queue.isNotEmpty()) { + val vertex = queue.poll() + hooksByClassName[vertex]?.let { result += it } - hooksByClassName[className]?.let { sorted.add(it) } + for (successor in graph[vertex].orEmpty()) { + incomingEdges.mergeInt(successor, -1, Int::minus) + if (incomingEdges.getInt(successor) == 0) { + queue += successor + } + } } - // Sort validHooks by priority before visiting to use priority as a tie-breaker - validHooks.sortedBy { (_, hook) -> hook.priority }.forEach { (meta, _) -> visit(meta.className) } - return sorted + if (result.size != incomingEdges.size) { + val chain = findCyclicDependency(graph, incomingEdges) + throw IllegalStateException( + "Circular hook dependency detected: ${chain.joinToString(" -> ")}" + ) + } + + return result } - private fun buildCircularDependencyChain( - startClassName: String, - metaByClassName: Map, - visiting: Set + private fun findCyclicDependency( + graph: Map>, + incomingEdges: Map ): List { + // Find a node that still has incoming edges (part of cycle) + val cycleNode = incomingEdges.entries.firstOrNull { it.value > 0 }?.key + ?: return emptyList() + + // Trace back through dependencies to find the cycle + val visited = mutableSetOf() val chain = mutableListOf() - var current = startClassName + var current = cycleNode - while (current !in chain) { + while (current !in visited) { + visited.add(current) chain.add(current) - val deps = metaByClassName[current]?.hookDependencies ?: break - current = deps.firstOrNull { it in visiting } ?: break + // Find a successor that still has incoming edges (part of cycle) + current = graph[current]?.firstOrNull { incomingEdges[it] ?: 0 > 0 } ?: break } - chain.add(startClassName) + chain.add(current) return chain }