Skip to content

Commit 931a5d3

Browse files
committed
refactor(core): harden runtime cache service
1 parent fcf4e5b commit 931a5d3

4 files changed

Lines changed: 81 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,10 @@ elprobe run <package>
354354
Config wrapper; pass a directory string or use PocketMine `Config` directly.
355355
- `File`, `Path`, `FileExtensionTypes` and `FileSystemException` remain part of
356356
the core filesystem direction and will be hardened rather than removed.
357+
- `PluginToolkit`, `PluginComponent` and their toolkit traits remain part of
358+
the core plugin/component direction and will be hardened rather than removed.
359+
- `Cache` is now documented and hardened as a small runtime-only core service:
360+
local memory, TTL support, no persistence and no distributed state.
357361
- Component enable/disable notices now use PocketMine `TextFormat` constants
358362
instead of raw section-sign color codes in the core component logger.
359363

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ The 3.x direction is: EasyLibrary becomes the minimal manager, official libs bec
2525
- A plugin-owned module system with manifests, dependency checks, lifecycle cleanup, services and capabilities.
2626
- Core infrastructure components for the package manager, plugin toolkit,
2727
filesystem helpers, Config Split support and the small runtime cache
28-
component.
28+
component. The cache is runtime-only memory, not persistent storage.
29+
- `PluginToolkit`/`PluginComponent` and `File`/`Path` are kept as long-term
30+
EasyLibrary core directions; they may be refined or moved, not removed.
2931
- Optional Agent Bridge support for network-aware plugins that need PubSub, registry, KV, flags, locks, RPC, runtime events or safe compute services.
3032

3133
## Requirements

changelogs/3.0.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,10 @@ Reload rereads config files for future EasyLibrary operations. It does not:
15451545
use PocketMine `Config` directly for normal config files.
15461546
- `File`, `Path`, `FileExtensionTypes` and `FileSystemException` remain part of
15471547
the core filesystem direction and will be hardened rather than removed.
1548+
- `PluginToolkit`, `PluginComponent` and their toolkit traits remain part of
1549+
the core plugin/component direction and will be hardened rather than removed.
1550+
- `Cache` is now treated as a small runtime-only core service: local memory,
1551+
TTL support, no persistence and no distributed state.
15481552
- Component enable/disable notices now use PocketMine `TextFormat` constants
15491553
instead of raw section-sign color codes in `LibraryComponents`.
15501554

@@ -1596,6 +1600,17 @@ The filesystem area is different: `File` and `Path` are actively useful to the
15961600
core and remain part of the EasyLibrary direction. They may be renamed, moved or
15971601
hardened, but the idea should stay available.
15981602

1603+
The plugin toolkit area follows the same rule. `PluginToolkit`,
1604+
`PluginComponent`, `PluginToolkitTrait`, `PluginComponentsTrait`,
1605+
`ComponentTypesTrait` and `PluginException` may be renamed, moved or hardened,
1606+
but the toolkit/component idea should stay available.
1607+
1608+
The cache area is also kept, but intentionally small. It is useful as a local
1609+
runtime helper for the core and compatibility components, but it should not grow
1610+
into persistent storage, distributed cache or an adapter framework inside the
1611+
EasyLibrary core. If that need appears later, it should be extracted into a
1612+
focused package.
1613+
15991614
The component logging polish is intentionally small but useful for the 3.x
16001615
cleanup: active core output should be generated through PocketMine formatting
16011616
APIs instead of embedded formatting bytes. This keeps console output and future

src/imperazim/components/cache/Cache.php

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,25 @@
99
use imperazim\components\plugin\traits\PluginComponentsTrait;
1010

1111
/**
12-
* Class Cache
13-
* @package imperazim\components\cache
12+
* Runtime-only in-memory cache used by EasyLibrary and compatibility components.
13+
*
14+
* This cache is intentionally small: it does not persist data, does not provide
15+
* distributed state and should not grow into a storage framework.
16+
*
17+
* @phpstan-type CacheEntry array{value: mixed, expires_at: int|null}
1418
*/
1519
final class Cache extends PluginComponent {
1620
use PluginComponentsTrait;
1721

1822
/**
19-
* @var array An associative array where keys represent cache keys and values represent cached data with expiration.
23+
* @var array<string, CacheEntry>
2024
*/
2125
private static array $cache = [];
2226

2327
/**
24-
* Initializes the Cache component.
25-
* @param PluginToolkit $plugin The Plugin.
28+
* Initializes the runtime cache component.
29+
*
30+
* @return array<string, mixed>
2631
*/
2732
public static function init(PluginToolkit $plugin): array {
2833
return [];
@@ -44,45 +49,35 @@ public static function close(PluginToolkit $plugin): void {
4449
public static function put(string $key, mixed $value, ?int $ttl = null): void {
4550
self::$cache[$key] = [
4651
'value' => $value,
47-
'expires_at' => $ttl !== null ? time() + $ttl : null
52+
'expires_at' => $ttl !== null ? self::now() + max(0, $ttl) : null
4853
];
4954
}
5055

5156
/**
52-
* Retrieves cached data by key, returning null if the data is expired or doesn't exist.
53-
* @param string $key The cache key.
54-
* @return mixed|null The cached value or null if expired/not found.
57+
* Retrieves cached data by key, returning null if the entry is expired or missing.
5558
*/
5659
public static function get(string $key): mixed {
57-
if (!isset(self::$cache[$key])) {
60+
if (!array_key_exists($key, self::$cache)) {
5861
return null;
5962
}
6063

61-
$cacheItem = self::$cache[$key];
62-
63-
// Check if the item has expired
64-
if ($cacheItem['expires_at'] !== null && $cacheItem['expires_at'] < time()) {
64+
if (self::isEntryExpired(self::$cache[$key])) {
6565
self::invalidate($key);
6666
return null;
6767
}
6868

69-
return $cacheItem['value'];
69+
return self::$cache[$key]['value'];
7070
}
7171

7272
/**
7373
* Checks if a cache key exists and is not expired.
74-
* @param string $key The cache key.
75-
* @return bool True if the key exists and is not expired, false otherwise.
7674
*/
7775
public static function has(string $key): bool {
78-
if (!isset(self::$cache[$key])) {
76+
if (!array_key_exists($key, self::$cache)) {
7977
return false;
8078
}
8179

82-
$cacheItem = self::$cache[$key];
83-
84-
// Check if the item has expired
85-
if ($cacheItem['expires_at'] !== null && $cacheItem['expires_at'] < time()) {
80+
if (self::isEntryExpired(self::$cache[$key])) {
8681
self::invalidate($key);
8782
return false;
8883
}
@@ -98,9 +93,8 @@ public static function has(string $key): bool {
9893
* @return mixed The cached value or callback result.
9994
*/
10095
public static function remember(string $key, callable $callback, ?int $ttl = null): mixed {
101-
$value = self::get($key);
102-
if ($value !== null) {
103-
return $value;
96+
if (self::has($key)) {
97+
return self::get($key);
10498
}
10599

106100
$value = $callback();
@@ -125,19 +119,21 @@ public static function clear(): void {
125119

126120
/**
127121
* Stores multiple values in the cache with optional TTL.
128-
* @param array $items Associative array of key => value pairs.
122+
*
123+
* @param array<int|string, mixed> $items Associative array of key => value pairs.
129124
* @param int|null $ttl Time to live in seconds. Null for no expiration.
130125
*/
131126
public static function putMany(array $items, ?int $ttl = null): void {
132127
foreach ($items as $key => $value) {
133-
self::put($key, $value, $ttl);
128+
self::put((string) $key, $value, $ttl);
134129
}
135130
}
136131

137132
/**
138133
* Retrieves multiple cached values by keys.
139-
* @param array $keys Array of cache keys.
140-
* @return array Associative array of key => value pairs (null for missing/expired).
134+
*
135+
* @param array<int, string> $keys Array of cache keys.
136+
* @return array<string, mixed|null> Associative array of key => value pairs.
141137
*/
142138
public static function getMany(array $keys): array {
143139
$results = [];
@@ -151,9 +147,9 @@ public static function getMany(array $keys): array {
151147
* Removes expired cache entries.
152148
*/
153149
public static function purgeExpired(): void {
154-
$currentTime = time();
150+
$currentTime = self::now();
155151
foreach (self::$cache as $key => $item) {
156-
if ($item['expires_at'] !== null && $item['expires_at'] < $currentTime) {
152+
if (self::isEntryExpired($item, $currentTime)) {
157153
unset(self::$cache[$key]);
158154
}
159155
}
@@ -222,6 +218,7 @@ public static function exists(string $key): bool {
222218
* @return int Number of cached entries.
223219
*/
224220
public static function size(): int {
221+
self::purgeExpired();
225222
return count(self::$cache);
226223
}
227224

@@ -231,11 +228,11 @@ public static function size(): int {
231228
* @return bool True if the key exists but has expired.
232229
*/
233230
public static function isExpired(string $key): bool {
234-
if (!isset(self::$cache[$key])) {
235-
return false; // Key doesn't exist, so it's not "expired" - it never existed
231+
if (!array_key_exists($key, self::$cache)) {
232+
return false;
236233
}
237-
$item = self::$cache[$key];
238-
return $item['expires_at'] !== null && $item['expires_at'] < time();
234+
235+
return self::isEntryExpired(self::$cache[$key]);
239236
}
240237

241238
/**
@@ -244,48 +241,64 @@ public static function isExpired(string $key): bool {
244241
* @return int|null Remaining TTL in seconds, or null if key doesn't exist or has no expiration.
245242
*/
246243
public static function getRemainingTtl(string $key): ?int {
247-
if (!isset(self::$cache[$key])) {
244+
if (!self::has($key)) {
248245
return null;
249246
}
247+
250248
$item = self::$cache[$key];
251249
if ($item['expires_at'] === null) {
252-
return null; // No expiration
250+
return null;
253251
}
254-
$remaining = $item['expires_at'] - time();
255-
return max(0, $remaining); // Don't return negative values
252+
253+
return max(0, $item['expires_at'] - self::now());
256254
}
257255

258256
/**
259257
* Returns all cache entries as an associative array.
260-
* @return array All cache entries [key => value].
258+
*
259+
* @return array<string, mixed> All active cache entries [key => value].
261260
*/
262261
public static function all(): array {
262+
self::purgeExpired();
263+
263264
$result = [];
264265
foreach (self::$cache as $key => $item) {
265-
if ($item['expires_at'] === null || $item['expires_at'] > time()) {
266-
$result[$key] = $item['value'];
267-
}
266+
$result[$key] = $item['value'];
268267
}
269268
return $result;
270269
}
271270

272271
/**
273272
* Returns cache statistics such as total entries, hits, misses, and expired items.
274-
* @return array Cache statistics.
273+
*
274+
* @return array{total_entries: int, active_entries: int, expired_entries: int}
275275
*/
276276
public static function getStats(): array {
277277
$totalEntries = count(self::$cache);
278278
$expiredEntries = 0;
279+
$currentTime = self::now();
279280

280281
foreach (self::$cache as $key => $item) {
281-
if ($item['expires_at'] !== null && $item['expires_at'] < time()) {
282+
if (self::isEntryExpired($item, $currentTime)) {
282283
$expiredEntries++;
283284
}
284285
}
285286

286287
return [
287288
'total_entries' => $totalEntries,
289+
'active_entries' => $totalEntries - $expiredEntries,
288290
'expired_entries' => $expiredEntries
289291
];
290292
}
291-
}
293+
294+
private static function now(): int {
295+
return time();
296+
}
297+
298+
/**
299+
* @param CacheEntry $entry
300+
*/
301+
private static function isEntryExpired(array $entry, ?int $now = null): bool {
302+
return $entry['expires_at'] !== null && $entry['expires_at'] <= ($now ?? self::now());
303+
}
304+
}

0 commit comments

Comments
 (0)