philiprehberger-expiring_map 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +14 -0
- data/lib/philiprehberger/expiring_map/map.rb +44 -0
- data/lib/philiprehberger/expiring_map/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5749cba84d6dc29c0db85c7c0112f4e8d58ae3c86b506c265d2fec2ea854c407
|
|
4
|
+
data.tar.gz: dc3f78c07157a42550dd77b156d73f6d7ffc4d24dfa4cb957cfb6f59932d88d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f76d3842b3da9209a8060e6fd6bf885993a158319681effd26a9a5185c67c2bf8b35cf67c28c56589aceff5e5753f05908192f3adaacda130487e36f24a13b67
|
|
7
|
+
data.tar.gz: 43f6e2b1c3e93a476f4f2cc039b7ea40685d59ab0d74a0a60eaa7983e84eced5c362c0379b1fd29d5b9086a89768db6dc523ddf042093006e1fb6211bf614006
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Map#fetch(key, ttl: nil) { block }` atomically memoizes the block result on miss
|
|
14
|
+
|
|
10
15
|
## [0.2.0] - 2026-04-03
|
|
11
16
|
|
|
12
17
|
### Added
|
data/README.md
CHANGED
|
@@ -42,6 +42,19 @@ cache.set(:config, data, ttl: 3600) # expires in 1 hour
|
|
|
42
42
|
cache.ttl(:token) # => remaining seconds
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
### Fetch-or-compute
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
cache = Philiprehberger::ExpiringMap.new(default_ttl: 300)
|
|
49
|
+
|
|
50
|
+
# On miss, the block is evaluated, the result stored, and returned.
|
|
51
|
+
# On hit, the stored value is returned and the block is not called.
|
|
52
|
+
user = cache.fetch(:user_42) { User.find(42) }
|
|
53
|
+
|
|
54
|
+
# Override the TTL for this insert only:
|
|
55
|
+
token = cache.fetch(:token, ttl: 60) { Auth.issue_token }
|
|
56
|
+
```
|
|
57
|
+
|
|
45
58
|
### Max Size with Eviction
|
|
46
59
|
|
|
47
60
|
```ruby
|
|
@@ -115,6 +128,7 @@ cache.select { |_k, v| v > 10 }
|
|
|
115
128
|
| `.new(default_ttl:, max_size:)` | Create a new expiring map |
|
|
116
129
|
| `#set(key, value, ttl:)` | Store a value with optional per-key TTL |
|
|
117
130
|
| `#get(key)` | Retrieve a value, nil if expired or missing |
|
|
131
|
+
| `#fetch(key, ttl:) { block }` | Return stored value or atomically memoize block result on miss |
|
|
118
132
|
| `#set_many(hash, ttl:)` | Bulk insert from a hash |
|
|
119
133
|
| `#get_many(*keys)` | Bulk get returning hash of key => value |
|
|
120
134
|
| `#delete(key)` | Remove and return a value |
|
|
@@ -66,6 +66,50 @@ module Philiprehberger
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Retrieve a value by key, or atomically compute and store it on miss
|
|
70
|
+
#
|
|
71
|
+
# If the key is present and not expired, returns the stored value.
|
|
72
|
+
# Otherwise evaluates the block, stores the result under +key+ with the
|
|
73
|
+
# default TTL (or +ttl:+ override), and returns it. The get/compute/set
|
|
74
|
+
# path runs under the same mutex used by +#get+ and +#set+ so there is
|
|
75
|
+
# no race with concurrent writers.
|
|
76
|
+
#
|
|
77
|
+
# @param key [Object] the key
|
|
78
|
+
# @param ttl [Numeric, nil] TTL in seconds for the inserted value on miss, uses default if nil
|
|
79
|
+
# @yieldreturn [Object] the value to memoize under +key+ on miss
|
|
80
|
+
# @raise [KeyError] if the key is missing/expired and no block is given
|
|
81
|
+
# @return [Object] the stored or freshly computed value
|
|
82
|
+
def fetch(key, ttl: nil)
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
entry = @store[key]
|
|
85
|
+
|
|
86
|
+
if entry && !entry.expired?
|
|
87
|
+
@hits += 1
|
|
88
|
+
return entry.value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if entry&.expired?
|
|
92
|
+
@expirations += 1
|
|
93
|
+
fire_expire(key, entry.value)
|
|
94
|
+
@store.delete(key)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@misses += 1
|
|
98
|
+
|
|
99
|
+
raise KeyError, "key not found: #{key.inspect}" unless block_given?
|
|
100
|
+
|
|
101
|
+
value = yield
|
|
102
|
+
effective_ttl = ttl || @default_ttl
|
|
103
|
+
expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + effective_ttl
|
|
104
|
+
|
|
105
|
+
sweep_expired
|
|
106
|
+
evict_oldest if @max_size && @store.size >= @max_size && !@store.key?(key)
|
|
107
|
+
@store[key] = Entry.new(value, expires_at)
|
|
108
|
+
|
|
109
|
+
value
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
69
113
|
# Delete a key
|
|
70
114
|
#
|
|
71
115
|
# @param key [Object] the key
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-expiring_map
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A thread-safe hash map where each key has its own TTL, with automatic
|
|
14
14
|
expiration, max size eviction, expiration callbacks, and Enumerable support.
|