safe_memoize 1.3.0 → 1.5.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/.github/FUNDING.yml +15 -0
- data/CHANGELOG.md +19 -0
- data/README.md +177 -0
- data/ROADMAP.md +21 -0
- data/lib/safe_memoize/class_methods.rb +162 -18
- data/lib/safe_memoize/public_methods.rb +31 -0
- data/lib/safe_memoize/version.rb +1 -1
- data/lib/safe_memoize.rb +3 -0
- data/sig/safe_memoize.rbs +14 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ce254347308d6a616bc17df194ff5e91f9c151f6606d3b77963a963c8a3186c
|
|
4
|
+
data.tar.gz: 7399a03d13d297b798cfc60716376129c0162697ae845e5420409c0da33ecf68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a034c60f1f200e3971be9828563d43b406e1fc1feef4d2e044f880456ac2d1e0b7cf98b3d1e3292fb2641bb8c5e9d3e21c387f1a0041ae8996fa004db863d92
|
|
7
|
+
data.tar.gz: 1f72d605f086951abc9bff9a2dc452ecece2694cd7ad27a118589ba8c48b4e02fb6a35b97271e0007e1dde55ff1af798bf278fabedc07b2c0b74d955af270a56
|
data/.github/FUNDING.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github: [eclectic-coding] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
4
|
+
patreon: # Replace with a single Patreon username
|
|
5
|
+
open_collective: # Replace with a single Open Collective username
|
|
6
|
+
ko_fi: # Replace with a single Ko-fi username
|
|
7
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
8
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
9
|
+
liberapay: # Replace with a single Liberapay username
|
|
10
|
+
issuehunt: # Replace with a single IssueHunt username
|
|
11
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
12
|
+
polar: # Replace with a single Polar username
|
|
13
|
+
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
14
|
+
thanks_dev: # Replace with a single thanks.dev username
|
|
15
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between min
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.5.0] - 2026-06-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `group:` option on `memoize` — assigns a method to a named invalidation group (`memoize :find, group: :database`). Groups are stored on the class and survive re-memoization; a method can belong to at most one group at a time (re-memoizing with a different group moves it). Accepts any non-empty Symbol or String. Can be set as a class default via `safe_memoize_options group: :my_group`.
|
|
16
|
+
- `reset_memo_group(group_name)` instance method — clears all per-instance cached entries for every method in the named group in a single call; each evicted entry fires the `:on_evict` hook. A no-op for unknown groups.
|
|
17
|
+
- `reset_shared_memo_group(group_name)` class method — the shared-cache equivalent of `reset_memo_group`; clears all shared-cache entries for every method in the group that was memoized with `shared: true`.
|
|
18
|
+
- `memo_group_methods(group_name)` instance method — returns the array of method names belonging to the given group on the instance's class (empty array for unknown groups).
|
|
19
|
+
- `memo_groups` instance method — returns all group names registered on the instance's class.
|
|
20
|
+
- `safe_memo_group_methods(group_name)` class method — class-level equivalent of `memo_group_methods`.
|
|
21
|
+
- `safe_memo_groups` class method — class-level equivalent of `memo_groups`.
|
|
22
|
+
|
|
23
|
+
## [1.4.0] - 2026-06-02
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `safe_memoize_options(**opts)` class-level macro — sets default options for every subsequent `memoize` call on the class. Per-call options take precedence; class defaults take precedence over global `SafeMemoize.configure` defaults. Accepts all `memoize` options except mode-switch options (`shared:`, `fiber_local:`, `ractor_safe:`, `shared_cache:`), which must be specified per call. Call with no arguments to clear class-level defaults.
|
|
28
|
+
- `copy_on_read: true` option on `memoize` — returns a `dup` (or `deep_dup` when available, e.g. ActiveRecord objects) of the cached value on every read, preventing callers from mutating shared cached state. `nil` and frozen values are returned as-is. Works across all cache paths (per-instance, LRU, shared, fiber-local, and external store). Incompatible with `ractor_safe:` (ractor-safe values are always frozen; use that guarantee instead). Can be set as a class default via `safe_memoize_options copy_on_read: true`.
|
|
29
|
+
|
|
11
30
|
## [1.3.0] - 2026-05-28
|
|
12
31
|
|
|
13
32
|
### Added
|
data/README.md
CHANGED
|
@@ -77,6 +77,9 @@ SafeMemoize uses Ruby's `prepend` mechanism. When you call `memoize :method_name
|
|
|
77
77
|
- [Named shared caches via `shared_cache: "name"` — cross-class cache sharing backed by a globally-registered store](#named-shared-caches)
|
|
78
78
|
- [Automatic cache busting via `cache_bust:` — version-token-based invalidation; works with ActiveRecord `updated_at` and any comparable value](#automatic-cache-busting)
|
|
79
79
|
- [Plugin / extension architecture — `SafeMemoize::Extension` DSL for adding custom `memoize` options and global lifecycle handlers without monkey-patching](#plugin--extension-architecture)
|
|
80
|
+
- [Per-class default options via `safe_memoize_options` — set TTL, max size, copy-on-read, and other defaults for every `memoize` call on the class without repeating them](#per-class-default-options-safe_memoize_options)
|
|
81
|
+
- [Copy-on-read via `copy_on_read: true` — returns a `dup`/`deep_dup` on every cache read to protect shared cached state from caller mutation](#copy-on-read)
|
|
82
|
+
- [Cache invalidation groups via `group:` — tag related methods with a group name and bust them all with a single `reset_memo_group` call](#cache-invalidation-groups)
|
|
80
83
|
|
|
81
84
|
## Installation
|
|
82
85
|
|
|
@@ -192,6 +195,79 @@ obj.reset_all_memos # Clears all memoized values
|
|
|
192
195
|
|
|
193
196
|
[↑ Back to features](#features)
|
|
194
197
|
|
|
198
|
+
### Cache invalidation groups
|
|
199
|
+
|
|
200
|
+
Tag related methods with `group:` and bust them all at once with a single `reset_memo_group` call:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
class RepoService
|
|
204
|
+
prepend SafeMemoize
|
|
205
|
+
|
|
206
|
+
def find_user(id) = db.query("SELECT * FROM users WHERE id=?", id)
|
|
207
|
+
def find_post(id) = db.query("SELECT * FROM posts WHERE id=?", id)
|
|
208
|
+
def site_config = db.query("SELECT * FROM config LIMIT 1")
|
|
209
|
+
|
|
210
|
+
memoize :find_user, group: :database
|
|
211
|
+
memoize :find_post, group: :database
|
|
212
|
+
memoize :site_config # no group — unaffected by group reset
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
svc = RepoService.new
|
|
216
|
+
svc.find_user(1)
|
|
217
|
+
svc.find_post(42)
|
|
218
|
+
svc.site_config
|
|
219
|
+
|
|
220
|
+
svc.reset_memo_group(:database) # invalidates find_user and find_post only
|
|
221
|
+
svc.memoized?(:site_config) # => true — unaffected
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
For `shared: true` methods, use the class method:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
class CatalogService
|
|
228
|
+
prepend SafeMemoize
|
|
229
|
+
|
|
230
|
+
def products = fetch_all_products
|
|
231
|
+
def categories = fetch_all_categories
|
|
232
|
+
|
|
233
|
+
memoize :products, shared: true, group: :catalog
|
|
234
|
+
memoize :categories, shared: true, group: :catalog
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
CatalogService.reset_shared_memo_group(:catalog) # clears shared cache for both methods
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### Introspection
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
svc.memo_groups # => [:database] — all groups on the class
|
|
244
|
+
svc.memo_group_methods(:database) # => [:find_user, :find_post]
|
|
245
|
+
CatalogService.safe_memo_groups # => [:catalog]
|
|
246
|
+
CatalogService.safe_memo_group_methods(:catalog) # => [:products, :categories]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Class-wide group default
|
|
250
|
+
|
|
251
|
+
Use `safe_memoize_options` to assign all subsequently memoized methods to the same group:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
class ApiClient
|
|
255
|
+
prepend SafeMemoize
|
|
256
|
+
safe_memoize_options group: :api
|
|
257
|
+
|
|
258
|
+
def users = http.get("/users")
|
|
259
|
+
def orders = http.get("/orders")
|
|
260
|
+
|
|
261
|
+
memoize :users # group: :api
|
|
262
|
+
memoize :orders # group: :api
|
|
263
|
+
memoize :health, group: nil # override — no group
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A method belongs to at most one group at a time; re-memoizing with a different `group:` moves it.
|
|
268
|
+
|
|
269
|
+
[↑ Back to features](#features)
|
|
270
|
+
|
|
195
271
|
### Lifecycle hooks
|
|
196
272
|
|
|
197
273
|
Register callbacks that fire when cached entries are evicted or expire.
|
|
@@ -1372,6 +1448,83 @@ end
|
|
|
1372
1448
|
|
|
1373
1449
|
[↑ Back to features](#features)
|
|
1374
1450
|
|
|
1451
|
+
## Per-class default options (`safe_memoize_options`)
|
|
1452
|
+
|
|
1453
|
+
`safe_memoize_options` sets option defaults for every `memoize` call on the class, eliminating repetition when many methods share the same TTL, LRU cap, or other option. Per-call options still take precedence; class defaults take precedence over global `SafeMemoize.configure` defaults.
|
|
1454
|
+
|
|
1455
|
+
```ruby
|
|
1456
|
+
class ApiClient
|
|
1457
|
+
prepend SafeMemoize
|
|
1458
|
+
safe_memoize_options ttl: 60, max_size: 200, copy_on_read: true
|
|
1459
|
+
|
|
1460
|
+
def fetch(id) = http.get(id)
|
|
1461
|
+
memoize :fetch # uses ttl: 60, max_size: 200, copy_on_read: true
|
|
1462
|
+
|
|
1463
|
+
def list = http.get("/all")
|
|
1464
|
+
memoize :list, ttl: 300 # uses max_size: 200, copy_on_read: true; ttl: 300 overrides
|
|
1465
|
+
end
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
Accepted options are the same as `memoize` minus the mode-switch options (`shared:`, `fiber_local:`, `ractor_safe:`, `shared_cache:`), which must be specified per call because they change the entire execution path:
|
|
1469
|
+
|
|
1470
|
+
```ruby
|
|
1471
|
+
safe_memoize_options(
|
|
1472
|
+
ttl: 60,
|
|
1473
|
+
max_size: 100,
|
|
1474
|
+
ttl_refresh: true,
|
|
1475
|
+
copy_on_read: true,
|
|
1476
|
+
namespace: "v2",
|
|
1477
|
+
if: ->(v) { v.present? },
|
|
1478
|
+
cache_bust: :updated_at
|
|
1479
|
+
)
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
Call with no arguments to clear all class-level defaults:
|
|
1483
|
+
|
|
1484
|
+
```ruby
|
|
1485
|
+
MyClass.safe_memoize_options # clears — subsequent memoize calls use global config or per-call options only
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
[↑ Back to features](#features)
|
|
1489
|
+
|
|
1490
|
+
## Copy-on-read
|
|
1491
|
+
|
|
1492
|
+
Pass `copy_on_read: true` to `memoize` to return a `dup` (or `deep_dup` when available, e.g. ActiveRecord objects) of the stored value on every cache read. This prevents callers from mutating the shared cached object:
|
|
1493
|
+
|
|
1494
|
+
```ruby
|
|
1495
|
+
class ConfigService
|
|
1496
|
+
prepend SafeMemoize
|
|
1497
|
+
|
|
1498
|
+
def settings = {host: "localhost", port: 8080}
|
|
1499
|
+
memoize :settings, copy_on_read: true
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
svc = ConfigService.new
|
|
1503
|
+
result = svc.settings
|
|
1504
|
+
result[:host] = "mutated" # only affects the caller's copy
|
|
1505
|
+
|
|
1506
|
+
svc.settings[:host] # => "localhost" — cache is unaffected
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
`nil` and frozen values are returned as-is (no dup attempted). `copy_on_read:` works across all cache paths: per-instance hash, LRU (`max_size:`), class-level shared (`shared: true`), fiber-local (`fiber_local: true`), and external stores. It is incompatible with `ractor_safe: true` (ractor-safe values are always frozen; rely on that guarantee instead).
|
|
1510
|
+
|
|
1511
|
+
Set it as a class-wide default with `safe_memoize_options`:
|
|
1512
|
+
|
|
1513
|
+
```ruby
|
|
1514
|
+
class ReportService
|
|
1515
|
+
prepend SafeMemoize
|
|
1516
|
+
safe_memoize_options copy_on_read: true
|
|
1517
|
+
|
|
1518
|
+
def summary = build_summary
|
|
1519
|
+
memoize :summary
|
|
1520
|
+
|
|
1521
|
+
def details = build_details
|
|
1522
|
+
memoize :details
|
|
1523
|
+
end
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
[↑ Back to features](#features)
|
|
1527
|
+
|
|
1375
1528
|
## Ractor-safe shared cache
|
|
1376
1529
|
|
|
1377
1530
|
Pass `ractor_safe: true` (together with `shared: true`) to replace the `Mutex`-backed class-level shared cache with a supervisor `Ractor` that owns the mutable cache hash. All reads and writes are serialised through message passing, so the cache is safe to use from multiple Ractors.
|
|
@@ -1531,6 +1684,8 @@ Anything **not** listed here — internal modules, private methods, `@__safe_mem
|
|
|
1531
1684
|
| `namespace:` | `String \| nil` | `nil` | Namespace prefix prepended to the cache key's first element; must not contain `:`; takes precedence over the class-level and global namespace |
|
|
1532
1685
|
| `shared_cache:` | `String \| nil` | `nil` | Name of a globally-registered shared store; incompatible with `shared:`, `store:`, `fiber_local:`, `ractor_safe:`, and `max_size:` |
|
|
1533
1686
|
| `cache_bust:` | `Proc \| Symbol \| nil` | `nil` | Version-token callable; invoked on the instance at each lookup; token is folded into the key; incompatible with `key:` |
|
|
1687
|
+
| `copy_on_read:` | `Boolean` | `false` | Return a `dup`/`deep_dup` of the cached value on every read; protects shared state from caller mutation; nil and frozen values pass through; incompatible with `ractor_safe:` |
|
|
1688
|
+
| `group:` | `Symbol \| String \| nil` | `nil` | Assigns the method to a named invalidation group; call `reset_memo_group` / `reset_shared_memo_group` to bust all methods in the group at once; a method belongs to at most one group |
|
|
1534
1689
|
| *(extension options)* | any | — | Unknown kwargs are validated against registered extensions; raise `ArgumentError` if unclaimed |
|
|
1535
1690
|
|
|
1536
1691
|
### `memoize_all` options (class method)
|
|
@@ -1544,6 +1699,12 @@ All `memoize` option keys above, plus:
|
|
|
1544
1699
|
| `include_protected:` | `Boolean` | `false` |
|
|
1545
1700
|
| `include_private:` | `Boolean` | `false` |
|
|
1546
1701
|
|
|
1702
|
+
### `safe_memoize_options` (class method)
|
|
1703
|
+
|
|
1704
|
+
| Option key | Type | Default | Notes |
|
|
1705
|
+
|---|---|---|---|
|
|
1706
|
+
| any `memoize` key except mode-switches | — | — | Accepts `ttl:`, `max_size:`, `ttl_refresh:`, `if:`, `unless:`, `key:`, `cache_bust:`, `copy_on_read:`, `namespace:`, `store:`, `group:`; raises `ArgumentError` for `shared:`, `fiber_local:`, `ractor_safe:`, `shared_cache:` |
|
|
1707
|
+
|
|
1547
1708
|
### Instance methods (public)
|
|
1548
1709
|
|
|
1549
1710
|
**Inspection**
|
|
@@ -1564,10 +1725,18 @@ All `memoize` option keys above, plus:
|
|
|
1564
1725
|
| Method | Returns |
|
|
1565
1726
|
|---|---|
|
|
1566
1727
|
| `reset_memo(method_name, *args, **kwargs)` | `nil` |
|
|
1728
|
+
| `reset_memo_group(group_name)` | `nil` |
|
|
1567
1729
|
| `reset_all_memos` | `nil` |
|
|
1568
1730
|
| `memo_touch(method_name, *args, ttl: nil, **kwargs)` | `Boolean` |
|
|
1569
1731
|
| `memo_refresh(method_name, *args, **kwargs)` | cached value |
|
|
1570
1732
|
|
|
1733
|
+
**Group introspection**
|
|
1734
|
+
|
|
1735
|
+
| Method | Returns |
|
|
1736
|
+
|---|---|
|
|
1737
|
+
| `memo_groups` | `Array<Symbol>` — all group names on the class |
|
|
1738
|
+
| `memo_group_methods(group_name)` | `Array<Symbol>` — methods in the group |
|
|
1739
|
+
|
|
1571
1740
|
**Warm-up and persistence**
|
|
1572
1741
|
|
|
1573
1742
|
| Method | Returns |
|
|
@@ -1619,11 +1788,19 @@ All `memoize` option keys above, plus:
|
|
|
1619
1788
|
|---|---|
|
|
1620
1789
|
| `reset_shared_memo(method_name, *args, **kwargs)` | `nil` |
|
|
1621
1790
|
| `reset_all_shared_memos` | `nil` |
|
|
1791
|
+
| `reset_shared_memo_group(group_name)` | `nil` |
|
|
1622
1792
|
| `shared_memoized?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1623
1793
|
| `shared_memo_count(method_name = nil)` | `Integer` |
|
|
1624
1794
|
| `shared_memo_age(method_name, *args, **kwargs)` | `Numeric \| nil` |
|
|
1625
1795
|
| `shared_memo_stale?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1626
1796
|
|
|
1797
|
+
### Group class methods (available on any class that uses `group:`)
|
|
1798
|
+
|
|
1799
|
+
| Method | Returns |
|
|
1800
|
+
|---|---|
|
|
1801
|
+
| `safe_memo_groups` | `Array<Symbol>` — all group names on the class |
|
|
1802
|
+
| `safe_memo_group_methods(group_name)` | `Array<Symbol>` — methods belonging to the group |
|
|
1803
|
+
|
|
1627
1804
|
**Ractor-safe shared cache (added when any method uses `ractor_safe: true`)**
|
|
1628
1805
|
|
|
1629
1806
|
| Method | Returns |
|
data/ROADMAP.md
CHANGED
|
@@ -4,6 +4,27 @@ This document tracks the planned evolution of SafeMemoize through v1.0.0 and bey
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v1.6.0 — Resilience
|
|
8
|
+
|
|
9
|
+
*Goal: make external-store memoization resilient to infrastructure failures.*
|
|
10
|
+
|
|
11
|
+
| Feature | Description | Status |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Circuit breaker for external stores | When a `store:` adapter raises on `read` or `write`, automatically fall back to the per-instance in-process hash rather than propagating the exception; configurable error threshold and recovery probe interval | Planned |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## v1.7.0 — Advanced Store Features
|
|
18
|
+
|
|
19
|
+
*Goal: multi-process performance patterns for high-traffic deployments.*
|
|
20
|
+
|
|
21
|
+
| Feature | Description | Status |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Multi-level (L1/L2) caching | `store: [memory_store, redis_store]` — check in-process first, fall back to the remote store on miss, and promote to L1 on read; each level can have independent TTL and eviction settings | Planned |
|
|
24
|
+
| Stampede protection | Probabilistic early expiry (XFetch algorithm) for external stores; recomputes slightly before a TTL expires to prevent multiple processes hitting a cold miss simultaneously | Planned |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
7
28
|
## v2.0.0 — Next Generation (Long Horizon)
|
|
8
29
|
|
|
9
30
|
*Goal: incorporate real-world usage feedback, clean up accumulated API surface, and open a path for advanced extension.*
|
|
@@ -37,7 +37,7 @@ module SafeMemoize
|
|
|
37
37
|
# a supervisor +Ractor+ rather than a +Mutex+-protected ivar, making it accessible
|
|
38
38
|
# from worker Ractors. Requires +shared: true+. Cached values are deep-frozen via
|
|
39
39
|
# +Ractor.make_shareable+. Incompatible with +if:+, +unless:+, +max_size:+,
|
|
40
|
-
# +ttl_refresh:+, +key:+, and +
|
|
40
|
+
# +ttl_refresh:+, +key:+, +store:+, and +copy_on_read:+.
|
|
41
41
|
# @param namespace [String, nil] prefix prepended to every cache key for this method,
|
|
42
42
|
# scoping it to a logical partition. Takes precedence over both the class-level
|
|
43
43
|
# {#safe_memoize_namespace} and the global {SafeMemoize::Configuration#namespace}.
|
|
@@ -59,6 +59,16 @@ module SafeMemoize
|
|
|
59
59
|
# {SafeMemoize.register_shared_cache} before the class is loaded to supply a custom
|
|
60
60
|
# adapter. Incompatible with +shared:+, +store:+, +fiber_local:+, +ractor_safe:+,
|
|
61
61
|
# and +max_size:+. Composes naturally with +namespace:+, +ttl:+, +if:+, and +key:+.
|
|
62
|
+
# @param copy_on_read [Boolean] when +true+, every cache read returns a +dup+ (or
|
|
63
|
+
# +deep_dup+ when available) of the stored value rather than the cached object
|
|
64
|
+
# itself. Prevents callers from mutating shared cached state. Frozen and +nil+
|
|
65
|
+
# values are returned as-is. Incompatible with +ractor_safe:+ (ractor values are
|
|
66
|
+
# always frozen; use that guarantee instead).
|
|
67
|
+
# @param group [Symbol, String, nil] assigns the method to a named invalidation
|
|
68
|
+
# group. Call {PublicMethods#reset_memo_group} on an instance (or
|
|
69
|
+
# {.reset_shared_memo_group} for shared-mode methods) to bust every method in the
|
|
70
|
+
# group at once. A method may belong to at most one group; re-memoizing with a
|
|
71
|
+
# different group moves it. Must be a non-empty Symbol or String.
|
|
62
72
|
# @return [void]
|
|
63
73
|
# @raise [ArgumentError] if the method does not exist, or option values are invalid
|
|
64
74
|
#
|
|
@@ -73,10 +83,13 @@ module SafeMemoize
|
|
|
73
83
|
# @example Conditional — only cache successful responses
|
|
74
84
|
# memoize :fetch, if: ->(v) { v[:status] == 200 }
|
|
75
85
|
#
|
|
86
|
+
# @example Copy-on-read — protect mutable cached config
|
|
87
|
+
# memoize :config, copy_on_read: true
|
|
88
|
+
#
|
|
76
89
|
# @example With a custom store
|
|
77
90
|
# STORE = SafeMemoize::Stores::Memory.new
|
|
78
91
|
# memoize :fetch, store: STORE, ttl: 300
|
|
79
|
-
def memoize(method_name, ttl:
|
|
92
|
+
def memoize(method_name, ttl: UNSET, max_size: UNSET, ttl_refresh: UNSET, if: UNSET, unless: UNSET, shared: UNSET, key: UNSET, store: UNSET, fiber_local: UNSET, ractor_safe: UNSET, namespace: UNSET, shared_cache: UNSET, cache_bust: UNSET, copy_on_read: UNSET, group: UNSET, **extension_options)
|
|
80
93
|
method_name = method_name.to_sym
|
|
81
94
|
|
|
82
95
|
unless method_defined?(method_name) || private_method_defined?(method_name) || protected_method_defined?(method_name)
|
|
@@ -102,18 +115,48 @@ module SafeMemoize
|
|
|
102
115
|
cache_bust = injected[:cache_bust] if injected.key?(:cache_bust)
|
|
103
116
|
end
|
|
104
117
|
|
|
118
|
+
# :if and :unless are reserved Ruby keywords; use binding to extract them
|
|
119
|
+
cond_if = binding.local_variable_get(:if)
|
|
120
|
+
cond_unless = binding.local_variable_get(:unless)
|
|
121
|
+
|
|
122
|
+
# Apply class-level defaults (safe_memoize_options) for any still-unset options
|
|
123
|
+
if (cls_defaults = __safe_memoize_defaults__)
|
|
124
|
+
ttl = cls_defaults[:ttl] if ttl.equal?(UNSET) && cls_defaults.key?(:ttl)
|
|
125
|
+
max_size = cls_defaults[:max_size] if max_size.equal?(UNSET) && cls_defaults.key?(:max_size)
|
|
126
|
+
ttl_refresh = cls_defaults[:ttl_refresh] if ttl_refresh.equal?(UNSET) && cls_defaults.key?(:ttl_refresh)
|
|
127
|
+
cond_if = cls_defaults[:if] if cond_if.equal?(UNSET) && cls_defaults.key?(:if)
|
|
128
|
+
cond_unless = cls_defaults[:unless] if cond_unless.equal?(UNSET) && cls_defaults.key?(:unless)
|
|
129
|
+
key = cls_defaults[:key] if key.equal?(UNSET) && cls_defaults.key?(:key)
|
|
130
|
+
cache_bust = cls_defaults[:cache_bust] if cache_bust.equal?(UNSET) && cls_defaults.key?(:cache_bust)
|
|
131
|
+
copy_on_read = cls_defaults[:copy_on_read] if copy_on_read.equal?(UNSET) && cls_defaults.key?(:copy_on_read)
|
|
132
|
+
namespace = cls_defaults[:namespace] if namespace.equal?(UNSET) && cls_defaults.key?(:namespace)
|
|
133
|
+
store = cls_defaults[:store] if store.equal?(UNSET) && cls_defaults.key?(:store)
|
|
134
|
+
group = cls_defaults[:group] if group.equal?(UNSET) && cls_defaults.key?(:group)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Normalize remaining UNSET to original per-call defaults
|
|
138
|
+
ttl = nil if ttl.equal?(UNSET)
|
|
139
|
+
max_size = nil if max_size.equal?(UNSET)
|
|
140
|
+
ttl_refresh = false if ttl_refresh.equal?(UNSET)
|
|
141
|
+
shared = false if shared.equal?(UNSET)
|
|
142
|
+
key = nil if key.equal?(UNSET)
|
|
143
|
+
store = nil if store.equal?(UNSET)
|
|
144
|
+
fiber_local = false if fiber_local.equal?(UNSET)
|
|
145
|
+
ractor_safe = false if ractor_safe.equal?(UNSET)
|
|
146
|
+
namespace = nil if namespace.equal?(UNSET)
|
|
147
|
+
shared_cache = nil if shared_cache.equal?(UNSET)
|
|
148
|
+
cache_bust = nil if cache_bust.equal?(UNSET)
|
|
149
|
+
copy_on_read = false if copy_on_read.equal?(UNSET)
|
|
150
|
+
group = nil if group.equal?(UNSET)
|
|
151
|
+
cond_if = nil if cond_if.equal?(UNSET)
|
|
152
|
+
cond_unless = nil if cond_unless.equal?(UNSET)
|
|
153
|
+
|
|
105
154
|
visibility = memoized_method_visibility(method_name)
|
|
106
155
|
|
|
107
156
|
config = SafeMemoize.configuration
|
|
108
157
|
ttl = config.default_ttl if ttl.nil?
|
|
109
158
|
max_size = config.default_max_size if max_size.nil?
|
|
110
159
|
|
|
111
|
-
# :if and :unless are reserved Ruby keywords, so they can't be referenced
|
|
112
|
-
# as local variables directly. binding.local_variable_get is the only way
|
|
113
|
-
# to read keyword arguments with those names inside the method body.
|
|
114
|
-
cond_if = binding.local_variable_get(:if)
|
|
115
|
-
cond_unless = binding.local_variable_get(:unless)
|
|
116
|
-
|
|
117
160
|
ttl = if ttl.nil?
|
|
118
161
|
nil
|
|
119
162
|
else
|
|
@@ -167,6 +210,7 @@ module SafeMemoize
|
|
|
167
210
|
raise ArgumentError, "ractor_safe: is incompatible with ttl_refresh:" if ttl_refresh
|
|
168
211
|
raise ArgumentError, "ractor_safe: is incompatible with key:" if key
|
|
169
212
|
raise ArgumentError, "ractor_safe: is incompatible with store:" if store
|
|
213
|
+
raise ArgumentError, "ractor_safe: is incompatible with copy_on_read:" if copy_on_read
|
|
170
214
|
end
|
|
171
215
|
|
|
172
216
|
if namespace
|
|
@@ -176,6 +220,15 @@ module SafeMemoize
|
|
|
176
220
|
__safe_memo_method_namespaces__[method_name] = namespace
|
|
177
221
|
end
|
|
178
222
|
|
|
223
|
+
if group
|
|
224
|
+
unless group.is_a?(Symbol) || group.is_a?(String)
|
|
225
|
+
raise ArgumentError, "group: must be a Symbol or String (got #{group.class})"
|
|
226
|
+
end
|
|
227
|
+
group = group.to_sym
|
|
228
|
+
raise ArgumentError, "group: must not be empty" if group.empty?
|
|
229
|
+
__safe_memo_register_group__(method_name, group)
|
|
230
|
+
end
|
|
231
|
+
|
|
179
232
|
if shared_cache
|
|
180
233
|
raise ArgumentError, "shared_cache: must be a String (got #{shared_cache.class})" unless shared_cache.is_a?(String)
|
|
181
234
|
raise ArgumentError, "shared_cache: must not be empty" if shared_cache.empty?
|
|
@@ -217,6 +270,18 @@ module SafeMemoize
|
|
|
217
270
|
->(result) { !cond_unless.call(result) }
|
|
218
271
|
end
|
|
219
272
|
|
|
273
|
+
# Build a value-duplication function for copy_on_read: true.
|
|
274
|
+
# Frozen and nil values are returned as-is; deep_dup is preferred when available
|
|
275
|
+
# (e.g. ActiveRecord objects) so nested mutable structures are also protected.
|
|
276
|
+
dup_fn = if copy_on_read
|
|
277
|
+
lambda do |v|
|
|
278
|
+
return v if v.nil? || v.frozen?
|
|
279
|
+
v.respond_to?(:deep_dup) ? v.deep_dup : v.dup
|
|
280
|
+
end
|
|
281
|
+
else
|
|
282
|
+
->(v) { v }
|
|
283
|
+
end
|
|
284
|
+
|
|
220
285
|
if effective_store
|
|
221
286
|
miss = SafeMemoize::Stores::Base::MISS
|
|
222
287
|
|
|
@@ -231,7 +296,7 @@ module SafeMemoize
|
|
|
231
296
|
effective_store.write(cache_key, cached, expires_in: ttl) if ttl_refresh
|
|
232
297
|
record_cache_hit(cache_key)
|
|
233
298
|
call_memo_hooks(:on_hit, cache_key, {value: cached, expires_at: nil, cached_at: nil})
|
|
234
|
-
return cached
|
|
299
|
+
return dup_fn.call(cached)
|
|
235
300
|
end
|
|
236
301
|
|
|
237
302
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -249,7 +314,7 @@ module SafeMemoize
|
|
|
249
314
|
record_cache_miss(cache_key, elapsed_time)
|
|
250
315
|
call_memo_hooks(:on_miss, cache_key, {value: value, expires_at: nil, cached_at: now})
|
|
251
316
|
|
|
252
|
-
value
|
|
317
|
+
dup_fn.call(value)
|
|
253
318
|
end
|
|
254
319
|
|
|
255
320
|
send(visibility, method_name)
|
|
@@ -278,7 +343,7 @@ module SafeMemoize
|
|
|
278
343
|
record[:expires_at] = memo_expires_at(ttl) if ttl_refresh
|
|
279
344
|
record_cache_hit(cache_key)
|
|
280
345
|
call_memo_hooks(:on_hit, cache_key, record)
|
|
281
|
-
memo_record_value(record)
|
|
346
|
+
dup_fn.call(memo_record_value(record))
|
|
282
347
|
else
|
|
283
348
|
call_memo_hooks(:on_expire, cache_key, record) if record
|
|
284
349
|
|
|
@@ -312,7 +377,7 @@ module SafeMemoize
|
|
|
312
377
|
record_cache_miss(cache_key, elapsed_time)
|
|
313
378
|
call_memo_hooks(:on_miss, cache_key, new_record)
|
|
314
379
|
|
|
315
|
-
value
|
|
380
|
+
dup_fn.call(value)
|
|
316
381
|
end
|
|
317
382
|
end
|
|
318
383
|
|
|
@@ -356,7 +421,7 @@ module SafeMemoize
|
|
|
356
421
|
record[:expires_at] = memo_expires_at(ttl) if ttl_refresh
|
|
357
422
|
record_cache_hit(cache_key)
|
|
358
423
|
call_memo_hooks(:on_hit, cache_key, record)
|
|
359
|
-
record[:value]
|
|
424
|
+
dup_fn.call(record[:value])
|
|
360
425
|
else
|
|
361
426
|
call_memo_hooks(:on_expire, cache_key, record) if record && !record_live
|
|
362
427
|
|
|
@@ -388,7 +453,7 @@ module SafeMemoize
|
|
|
388
453
|
record_cache_miss(cache_key, elapsed_time)
|
|
389
454
|
call_memo_hooks(:on_miss, cache_key, new_record)
|
|
390
455
|
|
|
391
|
-
value
|
|
456
|
+
dup_fn.call(value)
|
|
392
457
|
end
|
|
393
458
|
end
|
|
394
459
|
end
|
|
@@ -417,7 +482,7 @@ module SafeMemoize
|
|
|
417
482
|
record[:expires_at] = memo_expires_at(ttl) if ttl_refresh
|
|
418
483
|
record_cache_hit(cache_key)
|
|
419
484
|
call_memo_hooks(:on_hit, cache_key, record)
|
|
420
|
-
memo_record_value(record)
|
|
485
|
+
dup_fn.call(memo_record_value(record))
|
|
421
486
|
else
|
|
422
487
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
423
488
|
value = Adapters::OpenTelemetry.trace(SafeMemoize.configuration.opentelemetry_tracer, method_name, self.class.name) { super(*args, **kwargs) }
|
|
@@ -434,7 +499,7 @@ module SafeMemoize
|
|
|
434
499
|
record_cache_miss(cache_key, elapsed_time)
|
|
435
500
|
call_memo_hooks(:on_miss, cache_key, new_record)
|
|
436
501
|
|
|
437
|
-
value
|
|
502
|
+
dup_fn.call(value)
|
|
438
503
|
end
|
|
439
504
|
end
|
|
440
505
|
else
|
|
@@ -442,7 +507,7 @@ module SafeMemoize
|
|
|
442
507
|
if (record = memo_cache_record(cache_key))
|
|
443
508
|
record_cache_hit(cache_key)
|
|
444
509
|
call_memo_hooks(:on_hit, cache_key, record)
|
|
445
|
-
return memo_record_value(record)
|
|
510
|
+
return dup_fn.call(memo_record_value(record))
|
|
446
511
|
end
|
|
447
512
|
|
|
448
513
|
# Cache miss - compute and store
|
|
@@ -459,7 +524,7 @@ module SafeMemoize
|
|
|
459
524
|
call_memo_hooks(:on_miss, cache_key, new_record)
|
|
460
525
|
end
|
|
461
526
|
|
|
462
|
-
result
|
|
527
|
+
dup_fn.call(result)
|
|
463
528
|
end
|
|
464
529
|
end
|
|
465
530
|
|
|
@@ -520,6 +585,41 @@ module SafeMemoize
|
|
|
520
585
|
@__safe_memoize_namespace__ = ns
|
|
521
586
|
end
|
|
522
587
|
|
|
588
|
+
# Sets class-wide default options applied to every subsequent {#memoize} call
|
|
589
|
+
# on this class. Per-call options take precedence; class defaults take
|
|
590
|
+
# precedence over global {SafeMemoize::Configuration} defaults.
|
|
591
|
+
#
|
|
592
|
+
# Call with no arguments (or an empty hash) to clear all class-level defaults.
|
|
593
|
+
#
|
|
594
|
+
# @example Apply a TTL and LRU cap to every memoized method on the class
|
|
595
|
+
# class ApiClient
|
|
596
|
+
# prepend SafeMemoize
|
|
597
|
+
# safe_memoize_options ttl: 60, max_size: 200
|
|
598
|
+
#
|
|
599
|
+
# def fetch(id) = http.get(id)
|
|
600
|
+
# memoize :fetch # uses ttl: 60, max_size: 200
|
|
601
|
+
#
|
|
602
|
+
# def list = http.get("/all")
|
|
603
|
+
# memoize :list, ttl: 300 # uses max_size: 200, ttl: 300
|
|
604
|
+
# end
|
|
605
|
+
#
|
|
606
|
+
# @example Protect all cached values from mutation
|
|
607
|
+
# safe_memoize_options copy_on_read: true
|
|
608
|
+
#
|
|
609
|
+
# @param opts [Hash] any subset of {#memoize} options except mode-switch options
|
|
610
|
+
# (+shared:+, +fiber_local:+, +ractor_safe:+, +shared_cache:+)
|
|
611
|
+
# @return [Hash] the stored defaults
|
|
612
|
+
# @raise [ArgumentError] for disallowed options
|
|
613
|
+
def safe_memoize_options(**opts)
|
|
614
|
+
disallowed = %i[shared fiber_local ractor_safe shared_cache]
|
|
615
|
+
bad = opts.keys & disallowed
|
|
616
|
+
unless bad.empty?
|
|
617
|
+
raise ArgumentError,
|
|
618
|
+
"safe_memoize_options does not accept #{bad.map { |k| ":#{k}" }.join(", ")} — pass mode-switch options per memoize call"
|
|
619
|
+
end
|
|
620
|
+
@__safe_memoize_defaults__ = opts.empty? ? nil : opts
|
|
621
|
+
end
|
|
622
|
+
|
|
523
623
|
# Memoizes every eligible public instance method defined directly on the class.
|
|
524
624
|
#
|
|
525
625
|
# Accepts all options that {#memoize} accepts, plus +:except:+ and +:only:+.
|
|
@@ -679,6 +779,35 @@ module SafeMemoize
|
|
|
679
779
|
end
|
|
680
780
|
end
|
|
681
781
|
|
|
782
|
+
# Clears all shared-cache entries for every method in the given group.
|
|
783
|
+
#
|
|
784
|
+
# Only affects methods memoized with +shared: true+. For per-instance cache
|
|
785
|
+
# invalidation use {PublicMethods#reset_memo_group} on the instance.
|
|
786
|
+
#
|
|
787
|
+
# @param group_name [Symbol, String]
|
|
788
|
+
# @return [void]
|
|
789
|
+
def reset_shared_memo_group(group_name)
|
|
790
|
+
group_name = group_name.to_sym
|
|
791
|
+
(__safe_memo_groups__[group_name] || []).each { |m| reset_shared_memo(m) }
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Returns the method names belonging to the given invalidation group, or an
|
|
795
|
+
# empty array when the group is unknown.
|
|
796
|
+
#
|
|
797
|
+
# @param group_name [Symbol, String]
|
|
798
|
+
# @return [Array<Symbol>]
|
|
799
|
+
def safe_memo_group_methods(group_name)
|
|
800
|
+
group_name = group_name.to_sym
|
|
801
|
+
(__safe_memo_groups__[group_name] || []).dup
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# Returns all group names registered on this class.
|
|
805
|
+
#
|
|
806
|
+
# @return [Array<Symbol>]
|
|
807
|
+
def safe_memo_groups
|
|
808
|
+
__safe_memo_groups__.keys
|
|
809
|
+
end
|
|
810
|
+
|
|
682
811
|
private
|
|
683
812
|
|
|
684
813
|
def __safe_memo_shared_cache__
|
|
@@ -705,6 +834,21 @@ module SafeMemoize
|
|
|
705
834
|
@__safe_memo_class_cache_bust_generators__ ||= {}
|
|
706
835
|
end
|
|
707
836
|
|
|
837
|
+
def __safe_memoize_defaults__
|
|
838
|
+
@__safe_memoize_defaults__
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def __safe_memo_groups__
|
|
842
|
+
@__safe_memo_groups__ ||= {}
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def __safe_memo_register_group__(method_name, group)
|
|
846
|
+
groups = __safe_memo_groups__
|
|
847
|
+
# Remove method from any prior group it belonged to
|
|
848
|
+
groups.each_value { |methods| methods.delete(method_name) }
|
|
849
|
+
(groups[group] ||= []) << method_name
|
|
850
|
+
end
|
|
851
|
+
|
|
708
852
|
# Resolves the effective first-element key sym for a given bare method name,
|
|
709
853
|
# applying the active namespace. Used by class-level cache operations where
|
|
710
854
|
# instance methods (compute_cache_key) are unavailable.
|
|
@@ -359,6 +359,37 @@ module SafeMemoize
|
|
|
359
359
|
end
|
|
360
360
|
end
|
|
361
361
|
|
|
362
|
+
# Clears all per-instance cached entries for every method belonging to the
|
|
363
|
+
# given invalidation group (declared via +memoize :method, group: :name+).
|
|
364
|
+
#
|
|
365
|
+
# A no-op when the group is unknown or has no members. Each evicted entry
|
|
366
|
+
# fires the +:on_evict+ hook. For shared-mode methods use the class-level
|
|
367
|
+
# {ClassMethods.reset_shared_memo_group} instead.
|
|
368
|
+
#
|
|
369
|
+
# @param group_name [Symbol, String]
|
|
370
|
+
# @return [void]
|
|
371
|
+
def reset_memo_group(group_name)
|
|
372
|
+
group_name = group_name.to_sym
|
|
373
|
+
(self.class.send(:__safe_memo_groups__)[group_name] || []).each { |m| reset_memo(m) }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Returns the method names belonging to the given invalidation group on
|
|
377
|
+
# this instance's class, or an empty array when the group is unknown.
|
|
378
|
+
#
|
|
379
|
+
# @param group_name [Symbol, String]
|
|
380
|
+
# @return [Array<Symbol>]
|
|
381
|
+
def memo_group_methods(group_name)
|
|
382
|
+
group_name = group_name.to_sym
|
|
383
|
+
(self.class.send(:__safe_memo_groups__)[group_name] || []).dup
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Returns all invalidation group names registered on this instance's class.
|
|
387
|
+
#
|
|
388
|
+
# @return [Array<Symbol>]
|
|
389
|
+
def memo_groups
|
|
390
|
+
self.class.send(:__safe_memo_groups__).keys
|
|
391
|
+
end
|
|
392
|
+
|
|
362
393
|
# Clears all cached entries for every method on this instance.
|
|
363
394
|
# Each evicted entry fires the +:on_evict+ hook.
|
|
364
395
|
#
|
data/lib/safe_memoize/version.rb
CHANGED
data/lib/safe_memoize.rb
CHANGED
|
@@ -55,6 +55,9 @@ module SafeMemoize
|
|
|
55
55
|
# Rescue this to catch any error raised by the library itself.
|
|
56
56
|
class Error < StandardError; end
|
|
57
57
|
|
|
58
|
+
# @api private — sentinel distinguishing "not passed" from explicit nil/false in memoize kwargs
|
|
59
|
+
UNSET = Object.new.freeze
|
|
60
|
+
|
|
58
61
|
# @api private
|
|
59
62
|
SHARED_CACHE_REGISTRY = {}
|
|
60
63
|
# @api private
|
data/sig/safe_memoize.rbs
CHANGED
|
@@ -17,7 +17,9 @@ module SafeMemoize
|
|
|
17
17
|
@__safe_memo_shared_cache__: Hash[memo_key, memo_record]?
|
|
18
18
|
@__safe_memo_shared_mutex__: Mutex?
|
|
19
19
|
@__safe_memo_shared_lru_order__: Hash[Symbol, Hash[memo_key, true]]?
|
|
20
|
+
@__safe_memo_groups__: Hash[Symbol, Array[Symbol]]?
|
|
20
21
|
|
|
22
|
+
UNSET: untyped
|
|
21
23
|
SHARED_CACHE_REGISTRY: Hash[String, Stores::Base]
|
|
22
24
|
SHARED_CACHE_MUTEX: Mutex
|
|
23
25
|
EXTENSION_REGISTRY: Hash[Symbol, untyped]
|
|
@@ -64,18 +66,22 @@ module SafeMemoize
|
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
module ClassMethods
|
|
67
|
-
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?store: Stores::Base?, ?fiber_local: bool, ?ractor_safe: bool, ?namespace: String?, ?shared_cache: String?, ?cache_bust: (^() -> untyped) | Symbol | nil, **untyped extension_options) -> void
|
|
69
|
+
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?store: Stores::Base?, ?fiber_local: bool, ?ractor_safe: bool, ?namespace: String?, ?shared_cache: String?, ?cache_bust: (^() -> untyped) | Symbol | nil, ?copy_on_read: bool, ?group: Symbol | String | nil, **untyped extension_options) -> void
|
|
68
70
|
def safe_memoize_store: () -> Stores::Base?
|
|
69
71
|
def safe_memoize_store=: (Stores::Base?) -> Stores::Base?
|
|
70
72
|
def safe_memoize_namespace: () -> String?
|
|
71
73
|
def safe_memoize_namespace=: (String?) -> String?
|
|
72
|
-
def
|
|
74
|
+
def safe_memoize_options: (**untyped opts) -> Hash[Symbol, untyped]?
|
|
75
|
+
def memoize_all: (?except: Array[Symbol | String], ?only: Array[Symbol | String], ?include_protected: bool, ?include_private: bool, ?ttl: Numeric?, ?max_size: Integer?, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?fiber_local: bool, ?namespace: String?, ?shared_cache: String?, ?copy_on_read: bool) -> void
|
|
73
76
|
def reset_shared_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
74
77
|
def reset_all_shared_memos: () -> void
|
|
75
78
|
def shared_memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
76
79
|
def shared_memo_count: (?Symbol | String method_name) -> Integer
|
|
77
80
|
def shared_memo_age: (Symbol | String method_name, *untyped args, **untyped kwargs) -> Float?
|
|
78
81
|
def shared_memo_stale?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
82
|
+
def reset_shared_memo_group: (Symbol | String group_name) -> void
|
|
83
|
+
def safe_memo_group_methods: (Symbol | String group_name) -> Array[Symbol]
|
|
84
|
+
def safe_memo_groups: () -> Array[Symbol]
|
|
79
85
|
|
|
80
86
|
private
|
|
81
87
|
|
|
@@ -85,6 +91,9 @@ module SafeMemoize
|
|
|
85
91
|
def __safe_memo_class_key_generators__: () -> Hash[Symbol, Proc]
|
|
86
92
|
def __safe_memo_method_namespaces__: () -> Hash[Symbol, String]
|
|
87
93
|
def __safe_memo_class_cache_bust_generators__: () -> Hash[Symbol, Proc | Symbol]
|
|
94
|
+
def __safe_memoize_defaults__: () -> Hash[Symbol, untyped]?
|
|
95
|
+
def __safe_memo_groups__: () -> Hash[Symbol, Array[Symbol]]
|
|
96
|
+
def __safe_memo_register_group__: (Symbol method_name, Symbol group) -> void
|
|
88
97
|
def memoized_method_visibility: (Symbol method_name) -> Symbol
|
|
89
98
|
end
|
|
90
99
|
|
|
@@ -111,6 +120,9 @@ module SafeMemoize
|
|
|
111
120
|
def memo_age: (Symbol | String method_name, *untyped args, **untyped kwargs) -> Float?
|
|
112
121
|
def memo_stale?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
113
122
|
def reset_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
123
|
+
def reset_memo_group: (Symbol | String group_name) -> void
|
|
124
|
+
def memo_group_methods: (Symbol | String group_name) -> Array[Symbol]
|
|
125
|
+
def memo_groups: () -> Array[Symbol]
|
|
114
126
|
def reset_all_memos: () -> void
|
|
115
127
|
def memo_inspect: (Symbol | String method_name, *untyped args, **untyped kwargs) -> { cached: bool, value: untyped, hits: Integer, misses: Integer, ttl_remaining: Float?, age: Float?, custom_key: untyped, lru_position: Integer? }?
|
|
116
128
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safe_memoize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -39,6 +39,7 @@ executables: []
|
|
|
39
39
|
extensions: []
|
|
40
40
|
extra_rdoc_files: []
|
|
41
41
|
files:
|
|
42
|
+
- ".github/FUNDING.yml"
|
|
42
43
|
- ".github/workflows/ci.yml"
|
|
43
44
|
- ".github/workflows/release.yml"
|
|
44
45
|
- ".yardopts"
|