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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aed5fe4178a5814d949531b8352284b152a8ac520963541944584e602ebe0c17
4
- data.tar.gz: 319d4b05843b15e90cb91c4487d6edc670dc3ffda54d26c9a77e09c035b4a843
3
+ metadata.gz: 5749cba84d6dc29c0db85c7c0112f4e8d58ae3c86b506c265d2fec2ea854c407
4
+ data.tar.gz: dc3f78c07157a42550dd77b156d73f6d7ffc4d24dfa4cb957cfb6f59932d88d6
5
5
  SHA512:
6
- metadata.gz: a3a545614963d2b2a4db845a909ad9f2bafb0fea91b543ec9456eebf0a7ddf3a7af2029c8080f357aea6bf0294a9744b672015fbbcfc9b74942b3f524b3ab8d6
7
- data.tar.gz: a7052a46245d890e339ed5e40b3bd78e21a1d29fb45441c86629ed0afaaf2fede1c69e7d77e9d401e1670d66eef55282bab0b63f4d3e7523e37473408a142ffb
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module ExpiringMap
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
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.2.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-03 00:00:00.000000000 Z
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.