shared-framework 0.0.17

Reusable building blocks for HTTP services — Hyper routing, SeaORM data layer, validation, OpenAPI docs, jobs, queues, cache.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
//! Key-value caching with in-memory and Redis backends.
//!
//! Provides the [`StorageAccess`] trait for get/put/invalidate/clear, the
//! [`InMemoryStorageAccess`] process-local map, the [`RedisStorage`] async
//! Redis helper, the region-scoped [`RedisStorageAccess`] JSON cache with TTL,
//! and the [`InMemoryRegionFactory`]/[`RedisRegionFactory`] region registries.
//!
//! Use the in-memory backend for single-process caches and the Redis backend
//! for shared caches; use region factories when separate namespaces are needed.

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

/// Selects which cache backend is configured.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CachingStrategy {
    /// Caching is explicitly disabled.
    Disabled,
    /// Use the Redis backend.
    Redis,
    /// Use the process-local in-memory backend.
    InMemory,
    /// No backend selected.
    None,
}

/// Synchronous key-value cache interface.
///
/// `K` is the key type; `V` is the value type.
pub trait StorageAccess<K, V>: Send + Sync
where
    K: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
    V: Clone + Send + Sync + 'static,
{
    /// Returns the value for `key`, or `None` when absent.
    fn get(&self, key: &K) -> Option<V>;
    /// Stores `value` under `key`, overwriting any existing entry.
    fn put(&self, key: K, value: V);
    /// Removes the entry for `key`, if present.
    fn invalidate(&self, key: &K);
    /// Returns true when a value is present for `key`.
    fn contains(&self, key: &K) -> bool {
        self.get(key).is_some()
    }
    /// Removes all entries.
    fn clear(&self);
}

// ── InMemory ─────────────────────────────────────────────────────────────────

/// Process-local cache backed by a locked hash map.
///
/// `K` is the key type; `V` is the value type. When the map reaches capacity,
/// inserting a new key first removes one arbitrary existing entry.
pub struct InMemoryStorageAccess<K, V>
where
    K: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
    V: Clone + Send + Sync + 'static,
{
    cache: Arc<Mutex<HashMap<K, V>>>,
    max_capacity: usize,
}

impl<K, V> InMemoryStorageAccess<K, V>
where
    K: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
    V: Clone + Send + Sync + 'static,
{
    /// Creates an empty cache holding up to `max_capacity` entries.
    pub fn new(max_capacity: u64) -> Self {
        Self {
            cache: Arc::new(Mutex::new(HashMap::new())),
            max_capacity: max_capacity as usize,
        }
    }

    /// Creates an empty cache with capacity 1024; the region name is ignored.
    pub fn for_region(_region: &str) -> Self {
        Self::new(1024)
    }
}

impl<K, V> StorageAccess<K, V> for InMemoryStorageAccess<K, V>
where
    K: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
    V: Clone + Send + Sync + 'static,
{
    fn get(&self, key: &K) -> Option<V> {
        self.cache.lock().unwrap().get(key).cloned()
    }
    fn put(&self, key: K, value: V) {
        let mut map = self.cache.lock().unwrap();
        if map.len() >= self.max_capacity {
            if let Some(k) = map.keys().next().cloned() {
                map.remove(&k);
            }
        }
        map.insert(key, value);
    }
    fn invalidate(&self, key: &K) {
        self.cache.lock().unwrap().remove(key);
    }
    fn clear(&self) {
        self.cache.lock().unwrap().clear();
    }
}

// ── Redis high-level helper ────────────────────────────────────────────────

use redis::{AsyncCommands, Client, RedisResult};

/// Async Redis helper for strings, hashes, lists, sets, and sorted sets.
///
/// All operations open a multiplexed connection and return Redis errors to the caller.
#[derive(Clone)]
pub struct RedisStorage {
    client: Client,
}

impl RedisStorage {
    /// Connects to the Redis server at `url`.
    pub fn new(url: &str) -> RedisResult<Self> {
        Ok(Self {
            client: Client::open(url)?,
        })
    }

    /// Connects using `REDIS_URL` (falling back to `REDIS_URI`, then localhost).
    pub fn from_env() -> RedisResult<Self> {
        let url = std::env::var("REDIS_URL")
            .or_else(|_| std::env::var("REDIS_URI"))
            .unwrap_or_else(|_| "redis://127.0.0.1:6379/".to_string());
        Self::new(&url)
    }

    /// Returns the underlying Redis client.
    pub fn client(&self) -> &Client {
        &self.client
    }

    async fn conn(&self) -> RedisResult<redis::aio::MultiplexedConnection> {
        self.client.get_multiplexed_async_connection().await
    }

    // ── Key operations ────────────────────────────────────────────────────

    /// Gets the string value for `key`, or `None` when absent.
    pub async fn get_value(&self, key: &str) -> RedisResult<Option<String>> {
        let mut conn = self.conn().await?;
        conn.get(key).await
    }

    /// Sets the string value for `key`.
    pub async fn set_value(&self, key: &str, value: &str) -> RedisResult<()> {
        let mut conn = self.conn().await?;
        conn.set::<_, _, ()>(key, value).await
    }

    /// Sets the string value for `key` with a TTL of `seconds`.
    pub async fn set_value_with_expiration(&self, key: &str, value: &str, seconds: u64) -> RedisResult<()> {
        let mut conn = self.conn().await?;
        conn.set_ex::<_, _, ()>(key, value, seconds).await
    }

    /// Sets `key` only when absent; returns true when the key was set.
    pub async fn set_value_if_absent(&self, key: &str, value: &str) -> RedisResult<bool> {
        let mut conn = self.conn().await?;
        // SET key value NX — returns OK or nil
        let res: Option<String> = redis::cmd("SET")
            .arg(key)
            .arg(value)
            .arg("NX")
            .query_async(&mut conn)
            .await?;
        Ok(res.is_some())
    }

    /// Deletes `key` and returns the number of keys removed.
    pub async fn delete_key(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.del(key).await
    }

    /// Lazily frees `key` with `UNLINK` and returns the number of keys removed.
    pub async fn unlink_key(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        redis::cmd("UNLINK").arg(key).query_async(&mut conn).await
    }

    /// Deletes all given keys and returns the number removed; returns 0 for an empty list.
    pub async fn delete_keys(&self, keys: &[String]) -> RedisResult<i64> {
        if keys.is_empty() {
            return Ok(0);
        }
        let mut conn = self.conn().await?;
        conn.del(keys).await
    }

    /// Returns true when `key` exists.
    pub async fn key_exists(&self, key: &str) -> RedisResult<bool> {
        let mut conn = self.conn().await?;
        let v: i64 = conn.exists(key).await?;
        Ok(v == 1)
    }

    /// Increments the integer at `key` by 1 and returns the new value.
    pub async fn increment_value(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.incr(key, 1i64).await
    }

    /// Increments the integer at `key` by `delta` and returns the new value.
    pub async fn increment_value_by(&self, key: &str, delta: i64) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.incr(key, delta).await
    }

    /// Decrements the integer at `key` by 1 and returns the new value.
    pub async fn decrement_value(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.decr(key, 1i64).await
    }

    /// Decrements the integer at `key` by `delta` and returns the new value.
    pub async fn decrement_value_by(&self, key: &str, delta: i64) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.decr(key, delta).await
    }

    /// Returns seconds since `key` was last accessed (`OBJECT IDLETIME`).
    pub async fn idle_time(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        redis::cmd("OBJECT")
            .arg("IDLETIME")
            .arg(key)
            .query_async(&mut conn)
            .await
    }

    /// Sets the TTL of `key` to `seconds`; returns true when the timeout was set.
    pub async fn set_expiration(&self, key: &str, seconds: u64) -> RedisResult<bool> {
        let mut conn = self.conn().await?;
        let v: i64 = conn.expire(key, seconds as i64).await?;
        Ok(v == 1)
    }

    /// Returns the TTL of `key` in seconds.
    pub async fn get_time_to_live(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.ttl(key).await
    }

    /// Gets the values for several keys at once; missing keys yield `None`. Empty input returns empty output.
    pub async fn get_multiple_values(&self, keys: &[String]) -> RedisResult<Vec<Option<String>>> {
        if keys.is_empty() {
            return Ok(vec![]);
        }
        let mut conn = self.conn().await?;
        conn.mget(keys).await
    }

    /// Sets several key-value pairs at once with `MSET`; does nothing for empty input.
    pub async fn set_multiple_values(&self, kv: &HashMap<String, String>) -> RedisResult<()> {
        if kv.is_empty() {
            return Ok(());
        }
        let mut conn = self.conn().await?;
        // MSET expects flat list of key, value, key, value
        let mut args: Vec<String> = Vec::with_capacity(kv.len() * 2);
        for (k, v) in kv {
            args.push(k.clone());
            args.push(v.clone());
        }
        // Use pipe for MSET
        redis::cmd("MSET").arg(args).query_async::<()>(&mut conn).await?;
        Ok(())
    }

    // ── Hash operations ───────────────────────────────────────────────────

    /// Gets a hash field value, or `None` when the key or field is absent.
    pub async fn get_hash_value(&self, key: &str, field: &str) -> RedisResult<Option<String>> {
        let mut conn = self.conn().await?;
        conn.hget(key, field).await
    }

    /// Sets a hash field value.
    pub async fn set_hash_value(&self, key: &str, field: &str, value: &str) -> RedisResult<()> {
        let mut conn = self.conn().await?;
        conn.hset::<_, _, _, ()>(key, field, value).await
    }

    /// Deletes a hash field and returns the number of fields removed.
    pub async fn delete_hash_field(&self, key: &str, field: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.hdel(key, field).await
    }

    /// Sets several hash fields at once; does nothing for empty input.
    pub async fn set_hash_values(&self, key: &str, field_values: &HashMap<String, String>) -> RedisResult<()> {
        if field_values.is_empty() {
            return Ok(());
        }
        let mut conn = self.conn().await?;
        // HSET key field value [field value ...]
        let mut cmd = redis::cmd("HSET");
        cmd.arg(key);
        for (f, v) in field_values {
            cmd.arg(f).arg(v);
        }
        cmd.query_async::<()>(&mut conn).await?;
        Ok(())
    }

    /// Sets a hash field only when absent; returns true when the field was set.
    pub async fn set_hash_value_if_absent(&self, key: &str, field: &str, value: &str) -> RedisResult<bool> {
        let mut conn = self.conn().await?;
        let v: i64 = conn.hset_nx(key, field, value).await?;
        Ok(v == 1)
    }

    /// Increments a hash integer field by `delta` and returns the new value.
    pub async fn increment_hash_field(&self, key: &str, field: &str, delta: i64) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.hincr(key, field, delta).await
    }

    /// Decrements a hash integer field by `delta` and returns the new value.
    pub async fn decrement_hash_field(&self, key: &str, field: &str, delta: i64) -> RedisResult<i64> {
        self.increment_hash_field(key, field, -delta).await
    }

    /// Returns all fields and values of a hash.
    pub async fn get_all_hash_fields(&self, key: &str) -> RedisResult<HashMap<String, String>> {
        let mut conn = self.conn().await?;
        conn.hgetall(key).await
    }

    /// Returns all field names of a hash.
    pub async fn get_hash_keys(&self, key: &str) -> RedisResult<Vec<String>> {
        let mut conn = self.conn().await?;
        conn.hkeys(key).await
    }

    /// Returns all values of a hash.
    pub async fn get_hash_values(&self, key: &str) -> RedisResult<Vec<String>> {
        let mut conn = self.conn().await?;
        conn.hvals(key).await
    }

    /// Returns true when a hash field exists.
    pub async fn hash_field_exists(&self, key: &str, field: &str) -> RedisResult<bool> {
        let mut conn = self.conn().await?;
        let v: bool = conn.hexists(key, field).await?;
        Ok(v)
    }

    // ── List operations ───────────────────────────────────────────────────

    /// Prepends `value` to the list at `key` and returns the new length.
    pub async fn push_to_list_start(&self, key: &str, value: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.lpush(key, value).await
    }

    /// Appends `value` to the list at `key` and returns the new length.
    pub async fn push_to_list_end(&self, key: &str, value: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.rpush(key, value).await
    }

    /// Removes and returns the first element of the list, or `None` when empty.
    pub async fn pop_from_list_start(&self, key: &str) -> RedisResult<Option<String>> {
        let mut conn = self.conn().await?;
        conn.lpop(key, None).await
    }

    /// Removes and returns the last element of the list, or `None` when empty.
    pub async fn pop_from_list_end(&self, key: &str) -> RedisResult<Option<String>> {
        let mut conn = self.conn().await?;
        conn.rpop(key, None).await
    }

    /// Returns the length of the list at `key`.
    pub async fn get_list_length(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.llen(key).await
    }

    /// Returns list elements from `start` to `stop` inclusive.
    pub async fn get_list_range(&self, key: &str, start: i64, stop: i64) -> RedisResult<Vec<String>> {
        let mut conn = self.conn().await?;
        conn.lrange(key, start as isize, stop as isize).await
    }

    // ── Set operations ────────────────────────────────────────────────────

    /// Adds `member` to the set and returns the number of members added.
    pub async fn add_to_set(&self, key: &str, member: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.sadd(key, member).await
    }

    /// Removes `member` from the set and returns the number of members removed.
    pub async fn remove_from_set(&self, key: &str, member: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.srem(key, member).await
    }

    /// Returns all members of the set.
    pub async fn get_set_members(&self, key: &str) -> RedisResult<Vec<String>> {
        let mut conn = self.conn().await?;
        conn.smembers(key).await
    }

    /// Returns true when `member` belongs to the set.
    pub async fn is_set_member(&self, key: &str, member: &str) -> RedisResult<bool> {
        let mut conn = self.conn().await?;
        conn.sismember(key, member).await
    }

    /// Returns the number of members in the set.
    pub async fn get_set_size(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.scard(key).await
    }

    // ── Sorted set operations ─────────────────────────────────────────────

    /// Adds `member` with `score` to the sorted set and returns the number of members added.
    pub async fn add_to_sorted_set(&self, key: &str, score: f64, member: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.zadd(key, member, score).await
    }

    /// Returns sorted-set members from `start` to `stop` inclusive, by ascending score.
    pub async fn get_sorted_set_range(&self, key: &str, start: i64, stop: i64) -> RedisResult<Vec<String>> {
        let mut conn = self.conn().await?;
        conn.zrange(key, start as isize, stop as isize).await
    }

    /// Removes `member` from the sorted set and returns the number of members removed.
    pub async fn remove_from_sorted_set(&self, key: &str, member: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.zrem(key, member).await
    }

    /// Returns the score of `member`, or `None` when absent.
    pub async fn get_sorted_set_score(&self, key: &str, member: &str) -> RedisResult<Option<f64>> {
        let mut conn = self.conn().await?;
        conn.zscore(key, member).await
    }

    /// Returns the number of members in the sorted set.
    pub async fn get_sorted_set_size(&self, key: &str) -> RedisResult<i64> {
        let mut conn = self.conn().await?;
        conn.zcard(key).await
    }

    // ── Other key operations ──────────────────────────────────────────────

    /// Removes the expiration from `key`; returns true when a timeout was removed.
    pub async fn remove_expiration(&self, key: &str) -> RedisResult<bool> {
        let mut conn = self.conn().await?;
        let v: i64 = redis::cmd("PERSIST").arg(key).query_async(&mut conn).await?;
        Ok(v == 1)
    }

    /// Renames `old_key` to `new_key`, overwriting any existing destination.
    pub async fn rename_key(&self, old_key: &str, new_key: &str) -> RedisResult<()> {
        let mut conn = self.conn().await?;
        redis::cmd("RENAME").arg(old_key).arg(new_key).query_async::<()>(&mut conn).await?;
        Ok(())
    }

    /// Collects keys matching `pattern` with a non-blocking incremental scan.
    ///
    /// `count` is the `COUNT` hint for each scan round.
    pub async fn scan_keys(&self, pattern: &str, count: usize) -> RedisResult<Vec<String>> {
        let mut conn = self.conn().await?;
        let mut cursor: u64 = 0;
        let mut all = Vec::new();
        loop {
            let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
                .arg(cursor)
                .arg("MATCH")
                .arg(pattern)
                .arg("COUNT")
                .arg(count)
                .query_async(&mut conn)
                .await?;
            all.extend(keys);
            if next_cursor == 0 {
                break;
            }
            cursor = next_cursor;
        }
        Ok(all)
    }

    /// Scans keys matching `pattern` with a count hint of 250.
    pub async fn scan_keys_default(&self, pattern: &str) -> RedisResult<Vec<String>> {
        self.scan_keys(pattern, 250).await
    }

    /// Deprecated alias for [`RedisStorage::scan_keys_default`]; prefer `scan_keys` to avoid blocking Redis.
    #[deprecated(note = "Use scan_keys instead to avoid blocking Redis")]
    pub async fn find_keys(&self, pattern: &str) -> RedisResult<Vec<String>> {
        self.scan_keys_default(pattern).await
    }
}

// ── Redis second-level cache ───────────────────────────────────────────────

/// Region-scoped Redis cache storing JSON-serialized values with a TTL.
///
/// Each region namespaces its keys with a per-region prefix; entries expire
/// after the configured TTL (default 3600 seconds).
pub struct RedisStorageAccess {
    storage: RedisStorage,
    prefix: String,
    ttl_seconds: u64,
}

impl RedisStorageAccess {
    /// Creates a region cache over the given storage with the default 3600-second TTL.
    pub fn new(storage: RedisStorage, region_name: &str) -> Self {
        Self {
            storage,
            prefix: format!("hibernate:cache:{}:", region_name),
            ttl_seconds: 3600,
        }
    }

    /// Creates a region cache from a Redis URL and region name.
    pub fn from_url(url: &str, region_name: &str) -> RedisResult<Self> {
        Ok(Self::new(RedisStorage::new(url)?, region_name))
    }

    /// Sets the TTL applied to written entries.
    pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
        self.ttl_seconds = ttl_seconds;
        self
    }

    fn build_key<K: ToString>(&self, key: &K) -> String {
        format!("{}{}", self.prefix, key.to_string())
    }

    fn serialize<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
        // JSON bytes for portability.
        serde_json::to_vec(value)
    }

    fn deserialize<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, serde_json::Error> {
        serde_json::from_slice(bytes)
    }

    // ── async API (primary) ────────────────────────────────────────────

    /// Returns true when a cached entry exists for `key`.
    pub async fn contains_async<K: ToString + Send + Sync>(&self, key: &K) -> RedisResult<bool> {
        self.storage.key_exists(&self.build_key(key)).await
    }

    /// Reads and JSON-decodes the entry for `key`; returns `None` when absent or undecodable.
    ///
    /// `K` is the key type; `V` is the value type.
    pub async fn get_from_cache_async<K, V>(&self, key: &K) -> RedisResult<Option<V>>
    where
        K: ToString + Send + Sync,
        V: serde::de::DeserializeOwned,
    {
        let raw: Option<Vec<u8>> = {
            let mut conn = self.storage.conn().await?;
            let k = self.build_key(key);
            conn.get(k).await?
        };
        match raw {
            None => Ok(None),
            Some(bytes) => match Self::deserialize::<V>(&bytes) {
                Ok(v) => Ok(Some(v)),
                Err(_) => Ok(None),
            },
        }
    }

    /// JSON-encodes `value` and stores it under `key` with the region TTL.
    ///
    /// `K` is the key type; `V` is the value type. Returns a Redis error when serialization fails.
    pub async fn put_into_cache_async<K, V>(&self, key: &K, value: &V) -> RedisResult<()>
    where
        K: ToString + Send + Sync,
        V: serde::Serialize,
    {
        let bytes = Self::serialize(value).map_err(|e| {
            redis::RedisError::from((
                redis::ErrorKind::Io,
                "serialization failed",
                e.to_string(),
            ))
        })?;
        let mut conn = self.storage.conn().await?;
        let k = self.build_key(key);
        // Use SETEX via `set_ex`
        conn.set_ex::<_, _, ()>(k, bytes, self.ttl_seconds).await
    }

    /// Removes the entry for `key`, if present.
    pub async fn remove_from_cache_async<K: ToString + Send + Sync>(&self, key: &K) -> RedisResult<()> {
        self.storage.unlink_key(&self.build_key(key)).await.map(|_| ())
    }

    /// Removes all entries in this region via scan plus unlink.
    pub async fn clear_cache_async(&self) -> RedisResult<()> {
        let pattern = format!("{}*", self.prefix);
        let keys = self.storage.scan_keys(&pattern, 750).await?;
        if keys.is_empty() {
            return Ok(());
        }
        let mut conn = self.storage.conn().await?;
        for key in keys {
            let _: () = redis::cmd("UNLINK").arg(key).query_async(&mut conn).await?;
        }
        Ok(())
    }

    // ── sync wrappers for `StorageAccess` trait (blocking) ─────────────

    fn block_on<F: Future>(fut: F) -> F::Output {
        // If we're inside a tokio runtime, block_in_place; otherwise block_on a new runtime.
        if let Ok(handle) = tokio::runtime::Handle::try_current() {
            tokio::task::block_in_place(|| handle.block_on(fut))
        } else {
            tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .unwrap()
                .block_on(fut)
        }
    }
}

impl<K, V> StorageAccess<K, V> for RedisStorageAccess
where
    K: std::hash::Hash + Eq + Clone + ToString + Send + Sync + 'static,
    V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
{
    fn get(&self, key: &K) -> Option<V> {
        Self::block_on(self.get_from_cache_async(key)).ok().flatten()
    }

    fn put(&self, key: K, value: V) {
        let _ = Self::block_on(self.put_into_cache_async(&key, &value));
    }

    fn invalidate(&self, key: &K) {
        let _ = Self::block_on(self.remove_from_cache_async(key));
    }

    fn clear(&self) {
        let _ = Self::block_on(self.clear_cache_async());
    }
}

// ── Region factory ─────────────────────────────────────────────────────────

use std::collections::hash_map::Entry;

/// Registry of named in-memory regions sharing one factory.
///
/// Each region is a separate [`InMemoryStorageAccess`] over serialized bytes.
pub struct InMemoryRegionFactory {
    regions: Mutex<HashMap<String, Arc<InMemoryStorageAccess<String, Vec<u8>>>>>,
}

impl InMemoryRegionFactory {
    /// Creates an empty region registry.
    pub fn new() -> Self {
        Self {
            regions: Mutex::new(HashMap::new()),
        }
    }

    /// Returns the region for `region_name`, creating it on first use.
    pub fn get_or_create(&self, region_name: &str) -> Arc<InMemoryStorageAccess<String, Vec<u8>>> {
        let mut map = self.regions.lock().unwrap();
        match map.entry(region_name.to_string()) {
            Entry::Occupied(o) => o.get().clone(),
            Entry::Vacant(v) => {
                let access = Arc::new(InMemoryStorageAccess::for_region(region_name));
                v.insert(access.clone());
                access
            }
        }
    }

    /// Clears every region in the registry.
    pub fn clear_all(&self) {
        let map = self.regions.lock().unwrap();
        for access in map.values() {
            access.clear();
        }
    }
}

impl Default for InMemoryRegionFactory {
    fn default() -> Self {
        Self::new()
    }
}

/// Registry of named Redis regions sharing one [`RedisStorage`].
pub struct RedisRegionFactory {
    storage: RedisStorage,
    regions: Mutex<HashMap<String, Arc<RedisStorageAccess>>>,
}

impl RedisRegionFactory {
    /// Creates a region registry over the given storage.
    pub fn new(storage: RedisStorage) -> Self {
        Self {
            storage,
            regions: Mutex::new(HashMap::new()),
        }
    }

    /// Creates a region registry from a Redis URL.
    pub fn from_url(url: &str) -> RedisResult<Self> {
        Ok(Self::new(RedisStorage::new(url)?))
    }

    /// Returns the region for `region_name`, creating it on first use.
    pub fn get_or_create(&self, region_name: &str) -> Arc<RedisStorageAccess> {
        let mut map = self.regions.lock().unwrap();
        match map.entry(region_name.to_string()) {
            Entry::Occupied(o) => o.get().clone(),
            Entry::Vacant(v) => {
                let access = Arc::new(RedisStorageAccess::new(self.storage.clone(), region_name));
                v.insert(access.clone());
                access
            }
        }
    }
}