Lines 97.50% 39 / 40
Methods 80.00% 4 / 5
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 add 100.00% 8 / 8 100.00% 1 / 1 2
 remove 100.00% 11 / 11 100.00% 1 / 1 4
 findMatching 92.30% 12 / 13 0.00% 0 / 1 6.02
 getRoutingStats 100.00% 7 / 7 100.00% 1 / 1 1
 buildCacheKey 100.00% 1 / 1 100.00% 1 / 1 1
13final class SelectorIndex
14{
15    private const int CACHE_LIMIT = 10_000;
16
17    /** @var array<string, IndexedSelector> */
18    private array $entries = [];
19
20    /** @var array<string, IndexedSelector> */
21    private array $patterns = [];
22
23    /** @var array<string, array<string, SubscriptionId>> */
24    private array $exact = [];
25
26    /** @var array<string, SubscriptionIdCollection> */
27    private array $cache = [];
28
29    private int $cacheHits = 0;
30
31    private int $cacheMisses = 0;
32
33    public function add(SubscriptionId $subscriptionId, SubscriptionKind $kind, Selector $selector): void
34    {
35        $this->remove($subscriptionId);
36
37        $entry = new IndexedSelector($subscriptionId, $kind, $selector);
38        $this->entries[$subscriptionId->value] = $entry;
39        $exactKey = $selector->findExactPattern();
40
41        if ($exactKey === null) {
42            $this->patterns[$subscriptionId->value] = $entry;
43        } else {
44            $this->exact[$this->buildCacheKey($kind, $exactKey)][$subscriptionId->value] = $subscriptionId;
45        }
46
47        $this->cache = [];
48    }
49
50    public function remove(SubscriptionId $subscriptionId): void
51    {
52        $entry = $this->entries[$subscriptionId->value] ?? null;
53
54        if ($entry === null) {
55            return;
56        }
57
58        unset($this->entries[$subscriptionId->value], $this->patterns[$subscriptionId->value]);
59
60        $exactKey = $entry->selector->findExactPattern();
61
62        if ($exactKey !== null) {
63            $cacheKey = $this->buildCacheKey($entry->kind, $exactKey);
64            unset($this->exact[$cacheKey][$subscriptionId->value]);
65
66            if (($this->exact[$cacheKey] ?? null) === []) {
67                unset($this->exact[$cacheKey]);
68            }
69        }
70
71        $this->cache = [];
72    }
73
74    public function findMatching(SubscriptionKind $kind, string $key): SubscriptionIdCollection
75    {
76        $cacheKey = $this->buildCacheKey($kind, $key);
77        $cached = $this->cache[$cacheKey] ?? null;
78
79        if ($cached !== null) {
80            ++$this->cacheHits;
81
82            return $cached;
83        }
84
85        ++$this->cacheMisses;
86
87        $matched = array_values($this->exact[$cacheKey] ?? []);
88
89        foreach ($this->patterns as $entry) {
90            if ($entry->kind === $kind && $entry->selector->matches($key)) {
91                $matched[] = $entry->subscriptionId;
92            }
93        }
94
95        if (\count($this->cache) >= self::CACHE_LIMIT) {
96            $this->cache = [];
97        }
98
99        return $this->cache[$cacheKey] = SubscriptionIdCollection::fromList($matched);
100    }
101
102    public function getRoutingStats(): RoutingStats
103    {
104        return new RoutingStats(
105            subscriptions: \count($this->entries),
106            patterns: \count($this->patterns),
107            cachedKeys: \count($this->cache),
108            cacheHits: $this->cacheHits,
109            cacheMisses: $this->cacheMisses,
110        );
111    }
112
113    private function buildCacheKey(SubscriptionKind $kind, string $key): string
114    {
115        return $kind->value . "\0" . $key;
116    }
117}