Skip to content

Commit 13b5ccf

Browse files
committed
add cacheing for better performance
1 parent 17867fd commit 13b5ccf

File tree

17 files changed

+2139
-14
lines changed

17 files changed

+2139
-14
lines changed

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,15 @@ RATE_LIMIT_LOGIN_ATTEMPTS=5
4040
RATE_LIMIT_LOGIN_WINDOW=900
4141
RATE_LIMIT_API_REQUESTS=100
4242
RATE_LIMIT_API_WINDOW=60
43+
44+
# Cache Configuration
45+
CACHE_ENABLED=1
46+
CACHE_DRIVER=file
47+
CACHE_TTL=300
48+
CACHE_PATH=data/cache
49+
50+
# Redis Configuration (optional, for CACHE_DRIVER=redis)
51+
REDIS_HOST=127.0.0.1
52+
REDIS_PORT=6379
53+
REDIS_PASSWORD=
54+
REDIS_DATABASE=0

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ node_modules/
99
coverage/
1010
logs/
1111
*.log
12+
data/cache/

assets/js/modules/items.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async function selectFeed(feedId) {
6868
updateItemSortButton();
6969
}
7070

71-
// Load items for this feed
71+
// Load items for this feed (this will apply hideReadItems filter if enabled)
7272
await loadFeedItems(feedId);
7373

7474
// Clear item content
@@ -255,21 +255,20 @@ async function markAsRead(itemId) {
255255
method: 'POST'
256256
}));
257257

258-
// Update UI
258+
// Update UI - remove unread class but keep item visible
259259
const itemElement = document.querySelector(`[data-item-id="${itemId}"]`);
260260
if (itemElement) {
261261
itemElement.classList.remove('unread');
262262
}
263263

264-
// Reload feeds to update unread counts
265-
if (typeof loadFeeds === 'function') {
266-
loadFeeds();
267-
}
268-
269-
// If hide read is enabled, reload items (item will disappear from list)
270-
if (window.currentFeedId && window.hideReadItems) {
271-
await loadFeedItems(window.currentFeedId);
272-
}
264+
// Reload feeds to update unread counts
265+
if (typeof loadFeeds === 'function') {
266+
loadFeeds();
267+
}
268+
269+
// Note: We don't reload items here - items will remain visible
270+
// until the user switches to a different feed, at which point
271+
// selectFeed() will reload items with the hideReadItems filter applied
273272
} catch (error) {
274273
console.error('Error marking as read:', error);
275274
}

src/Cache.php

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
3+
namespace PhpRss;
4+
5+
use PhpRss\Logger;
6+
7+
/**
8+
* Caching layer for application data.
9+
*
10+
* Provides caching functionality with support for both file-based (SQLite-compatible)
11+
* and Redis backends. Automatically handles serialization, expiration, and cache invalidation.
12+
*
13+
* Cache keys follow the pattern: "prefix:key" (e.g., "feed_counts:123")
14+
*/
15+
class Cache
16+
{
17+
/** @var Cache\CacheInterface|null Cache implementation instance */
18+
private static ?Cache\CacheInterface $instance = null;
19+
20+
/**
21+
* Get the cache instance.
22+
*
23+
* Initializes the cache backend based on configuration (file-based or Redis).
24+
* Falls back to file cache if Redis is unavailable or disabled.
25+
*
26+
* @return Cache\CacheInterface Cache implementation
27+
*/
28+
private static function getInstance(): Cache\CacheInterface
29+
{
30+
if (self::$instance === null) {
31+
// Check if caching is enabled
32+
if (!Config::get('cache.enabled', true)) {
33+
// Return a no-op cache if disabled
34+
self::$instance = new Cache\NullCache();
35+
return self::$instance;
36+
}
37+
38+
$driver = Config::get('cache.driver', 'file');
39+
40+
if ($driver === 'redis' && extension_loaded('redis')) {
41+
try {
42+
self::$instance = new Cache\RedisCache();
43+
} catch (\Exception $e) {
44+
// Fallback to file cache if Redis fails
45+
Logger::warning("Redis cache failed, falling back to file cache", [
46+
'error' => $e->getMessage()
47+
]);
48+
self::$instance = new Cache\FileCache();
49+
}
50+
} else {
51+
self::$instance = new Cache\FileCache();
52+
}
53+
}
54+
55+
return self::$instance;
56+
}
57+
58+
/**
59+
* Get a value from the cache.
60+
*
61+
* @param string $key Cache key
62+
* @param mixed $default Default value if key doesn't exist
63+
* @return mixed Cached value or default
64+
*/
65+
public static function get(string $key, $default = null)
66+
{
67+
return self::getInstance()->get($key, $default);
68+
}
69+
70+
/**
71+
* Store a value in the cache.
72+
*
73+
* @param string $key Cache key
74+
* @param mixed $value Value to cache
75+
* @param int|null $ttl Time to live in seconds (null = use default)
76+
* @return bool True on success
77+
*/
78+
public static function set(string $key, $value, ?int $ttl = null): bool
79+
{
80+
return self::getInstance()->set($key, $value, $ttl);
81+
}
82+
83+
/**
84+
* Delete a value from the cache.
85+
*
86+
* @param string $key Cache key
87+
* @return bool True if key was deleted
88+
*/
89+
public static function delete(string $key): bool
90+
{
91+
return self::getInstance()->delete($key);
92+
}
93+
94+
/**
95+
* Check if a key exists in the cache.
96+
*
97+
* @param string $key Cache key
98+
* @return bool True if key exists
99+
*/
100+
public static function has(string $key): bool
101+
{
102+
return self::getInstance()->has($key);
103+
}
104+
105+
/**
106+
* Clear all cache entries with a given prefix.
107+
*
108+
* @param string $prefix Key prefix (e.g., "feed_counts")
109+
* @return int Number of keys cleared
110+
*/
111+
public static function clear(string $prefix): int
112+
{
113+
return self::getInstance()->clear($prefix);
114+
}
115+
116+
/**
117+
* Invalidate cache for a specific feed.
118+
*
119+
* Clears all cache entries related to a feed (counts, metadata, etc.).
120+
*
121+
* @param int $feedId Feed ID
122+
* @return void
123+
*/
124+
public static function invalidateFeed(int $feedId): void
125+
{
126+
self::delete("feed_counts:{$feedId}");
127+
self::delete("feed_metadata:{$feedId}");
128+
self::clear("user_feeds:"); // Clear user feed lists (they contain counts)
129+
}
130+
131+
/**
132+
* Invalidate cache for a user's feeds.
133+
*
134+
* Clears cached feed lists for a specific user.
135+
*
136+
* @param int $userId User ID
137+
* @return void
138+
*/
139+
public static function invalidateUserFeeds(int $userId): void
140+
{
141+
self::delete("user_feeds:{$userId}");
142+
self::delete("user_feeds:{$userId}:hide_no_unread");
143+
}
144+
145+
/**
146+
* Get or remember a value from cache.
147+
*
148+
* If the key exists, returns cached value. Otherwise, executes callback,
149+
* caches the result, and returns it.
150+
*
151+
* If caching is disabled, always executes callback and returns result without caching.
152+
*
153+
* @param string $key Cache key
154+
* @param callable $callback Callback to generate value if not cached
155+
* @param int|null $ttl Time to live in seconds
156+
* @return mixed Cached or generated value
157+
*/
158+
public static function remember(string $key, callable $callback, ?int $ttl = null)
159+
{
160+
// If caching is disabled, just execute callback
161+
if (!Config::get('cache.enabled', true)) {
162+
return $callback();
163+
}
164+
165+
if (self::has($key)) {
166+
return self::get($key);
167+
}
168+
169+
$value = $callback();
170+
self::set($key, $value, $ttl);
171+
return $value;
172+
}
173+
}

src/Cache/CacheInterface.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace PhpRss\Cache;
4+
5+
/**
6+
* Cache interface for different cache backends.
7+
*/
8+
interface CacheInterface
9+
{
10+
/**
11+
* Get a value from the cache.
12+
*
13+
* @param string $key Cache key
14+
* @param mixed $default Default value if key doesn't exist
15+
* @return mixed Cached value or default
16+
*/
17+
public function get(string $key, $default = null);
18+
19+
/**
20+
* Store a value in the cache.
21+
*
22+
* @param string $key Cache key
23+
* @param mixed $value Value to cache
24+
* @param int|null $ttl Time to live in seconds (null = use default)
25+
* @return bool True on success
26+
*/
27+
public function set(string $key, $value, ?int $ttl = null): bool;
28+
29+
/**
30+
* Delete a value from the cache.
31+
*
32+
* @param string $key Cache key
33+
* @return bool True if key was deleted
34+
*/
35+
public function delete(string $key): bool;
36+
37+
/**
38+
* Check if a key exists in the cache.
39+
*
40+
* @param string $key Cache key
41+
* @return bool True if key exists
42+
*/
43+
public function has(string $key): bool;
44+
45+
/**
46+
* Clear all cache entries with a given prefix.
47+
*
48+
* @param string $prefix Key prefix
49+
* @return int Number of keys cleared
50+
*/
51+
public function clear(string $prefix): int;
52+
}

0 commit comments

Comments
 (0)