factorix 0.5.1 → 0.7.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 +45 -0
- data/README.md +1 -1
- data/exe/factorix +17 -0
- data/lib/factorix/api/mod_download_api.rb +11 -6
- data/lib/factorix/api/mod_info.rb +2 -2
- data/lib/factorix/api/mod_management_api.rb +1 -1
- data/lib/factorix/api/mod_portal_api.rb +6 -49
- data/lib/factorix/api_credential.rb +1 -1
- data/lib/factorix/cache/base.rb +116 -0
- data/lib/factorix/cache/entry.rb +25 -0
- data/lib/factorix/cache/file_system.rb +137 -57
- data/lib/factorix/cache/redis.rb +287 -0
- data/lib/factorix/cache/s3.rb +388 -0
- data/lib/factorix/cli/commands/backup_support.rb +1 -1
- data/lib/factorix/cli/commands/base.rb +3 -3
- data/lib/factorix/cli/commands/cache/evict.rb +19 -24
- data/lib/factorix/cli/commands/cache/stat.rb +66 -67
- data/lib/factorix/cli/commands/command_wrapper.rb +5 -5
- data/lib/factorix/cli/commands/completion.rb +1 -2
- data/lib/factorix/cli/commands/confirmable.rb +1 -1
- data/lib/factorix/cli/commands/download_support.rb +2 -7
- data/lib/factorix/cli/commands/mod/check.rb +1 -1
- data/lib/factorix/cli/commands/mod/disable.rb +1 -1
- data/lib/factorix/cli/commands/mod/download.rb +7 -7
- data/lib/factorix/cli/commands/mod/edit.rb +10 -13
- data/lib/factorix/cli/commands/mod/enable.rb +1 -1
- data/lib/factorix/cli/commands/mod/image/add.rb +3 -6
- data/lib/factorix/cli/commands/mod/image/edit.rb +2 -5
- data/lib/factorix/cli/commands/mod/image/list.rb +5 -8
- data/lib/factorix/cli/commands/mod/install.rb +7 -7
- data/lib/factorix/cli/commands/mod/list.rb +7 -7
- data/lib/factorix/cli/commands/mod/search.rb +13 -12
- data/lib/factorix/cli/commands/mod/settings/dump.rb +3 -3
- data/lib/factorix/cli/commands/mod/settings/restore.rb +2 -2
- data/lib/factorix/cli/commands/mod/show.rb +22 -23
- data/lib/factorix/cli/commands/mod/sync.rb +8 -8
- data/lib/factorix/cli/commands/mod/uninstall.rb +1 -1
- data/lib/factorix/cli/commands/mod/update.rb +11 -43
- data/lib/factorix/cli/commands/mod/upload.rb +7 -10
- data/lib/factorix/cli/commands/path.rb +2 -2
- data/lib/factorix/cli/commands/portal_support.rb +27 -0
- data/lib/factorix/cli/commands/version.rb +1 -1
- data/lib/factorix/container.rb +155 -0
- data/lib/factorix/dependency/parser.rb +1 -1
- data/lib/factorix/errors.rb +3 -0
- data/lib/factorix/http/cache_decorator.rb +5 -5
- data/lib/factorix/http/client.rb +3 -3
- data/lib/factorix/info_json.rb +7 -7
- data/lib/factorix/mod_list.rb +2 -2
- data/lib/factorix/mod_settings.rb +2 -2
- data/lib/factorix/portal.rb +3 -2
- data/lib/factorix/runtime/user_configurable.rb +9 -9
- data/lib/factorix/service_credential.rb +3 -3
- data/lib/factorix/transfer/downloader.rb +19 -11
- data/lib/factorix/version.rb +1 -1
- data/lib/factorix.rb +110 -1
- data/sig/factorix/api/mod_download_api.rbs +1 -2
- data/sig/factorix/cache/base.rbs +28 -0
- data/sig/factorix/cache/entry.rbs +14 -0
- data/sig/factorix/cache/file_system.rbs +7 -6
- data/sig/factorix/cache/redis.rbs +36 -0
- data/sig/factorix/cache/s3.rbs +38 -0
- data/sig/factorix/container.rbs +15 -0
- data/sig/factorix/errors.rbs +3 -0
- data/sig/factorix/portal.rbs +1 -1
- data/sig/factorix.rbs +99 -0
- metadata +27 -4
- data/lib/factorix/application.rb +0 -218
- data/sig/factorix/application.rbs +0 -86
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1d6b99d769a238f64676cfd19aef4d0f8852d2dc38df210eb753a8bc202f67f
|
|
4
|
+
data.tar.gz: c51b472d2eb47e7d0d39c4789112da30faf5fbf7e225da330d19e989bce86f43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e9bf68578d26f4b08e77028b1da59ff8d972f807822be5fa19ca1eb951e89412a3314a3db5fde1fcd821f399ee6c9e4e8c28e37d2d0c5e3d000046c5e663d2f
|
|
7
|
+
data.tar.gz: 81222393a0ab0a6b6e2cbbccac52dec243d4806ef688a1240a01367142094e368175caba564d4842ebd627ebdef98e322f8217b14a3424e8e20c7bbeddc248a4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-01-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add pluggable cache backend architecture with Redis and S3 support (#18, #19)
|
|
8
|
+
- Configure backend per cache type: `config.cache.<type>.backend = :file_system | :redis | :s3`
|
|
9
|
+
- **Redis backend** (`Cache::Redis`): requires `redis` gem (~> 5)
|
|
10
|
+
- Distributed locking via Lua script for atomic lock release
|
|
11
|
+
- Auto-namespaced keys: `factorix-cache:{cache_type}:{key}`
|
|
12
|
+
- **S3 backend** (`Cache::S3`): requires `aws-sdk-s3` gem
|
|
13
|
+
- Distributed locking via conditional PUT (`if_none_match: "*"`)
|
|
14
|
+
- TTL managed via S3 custom metadata, age from native `Last-Modified`
|
|
15
|
+
- `cache stat` command displays backend-specific information (directory, URL, bucket, etc.)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Refactor `Cache::FileSystem` to use `cache_type:` parameter instead of `root:` (#25)
|
|
20
|
+
- Aligns interface with other backends for consistent initialization
|
|
21
|
+
- Cache directory is now auto-computed from `Container[:runtime].factorix_cache_dir / cache_type`
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- Remove deprecated `Factorix::Application` compatibility class
|
|
26
|
+
- Use `Factorix::Container` for DI (`[]`, `resolve`, `register`)
|
|
27
|
+
- Use `Factorix.config` and `Factorix.configure` for configuration
|
|
28
|
+
|
|
29
|
+
## [0.6.0] - 2026-01-18
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Reorganize configuration and DI container interfaces (#7, #9)
|
|
34
|
+
- `Factorix::Application` renamed to `Factorix::Container` (DI container only)
|
|
35
|
+
- Configuration interface (`config`, `configure`, `load_config`) moved to `Factorix` module
|
|
36
|
+
- Use `Factorix.configure { |c| ... }` instead of `Factorix::Container.configure { |c| ... }`
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Fix integer CLI options being parsed as strings after dry-cli 1.4.0 update (#12)
|
|
41
|
+
|
|
42
|
+
### Deprecated
|
|
43
|
+
|
|
44
|
+
- `Factorix::Application` still works but emits deprecation warnings; will be removed in v1.0
|
|
45
|
+
- DI methods (`[]`, `resolve`, `register`) delegate to `Factorix::Container`
|
|
46
|
+
- Configuration methods (`config`, `configure`) delegate to `Factorix`
|
|
47
|
+
|
|
3
48
|
## [0.5.1] - 2026-01-13
|
|
4
49
|
|
|
5
50
|
### Fixed
|
data/README.md
CHANGED
|
@@ -65,7 +65,7 @@ $EDITOR ~/.config/factorix/config.rb
|
|
|
65
65
|
|
|
66
66
|
**Example configuration:**
|
|
67
67
|
```ruby
|
|
68
|
-
Factorix
|
|
68
|
+
Factorix.configure do |config|
|
|
69
69
|
config.runtime.executable_path = "/Applications/Factorio.app/Contents/MacOS/factorio"
|
|
70
70
|
config.runtime.user_dir = "#{Dir.home}/Library/Application Support/factorio"
|
|
71
71
|
config.runtime.data_dir = "/Applications/Factorio.app/Contents/data"
|
data/exe/factorix
CHANGED
|
@@ -8,6 +8,23 @@ require "zip"
|
|
|
8
8
|
# Suppress warnings about invalid dates in ZIP files
|
|
9
9
|
Zip.warn_invalid_date = false
|
|
10
10
|
|
|
11
|
+
# Load config early, before dry-cli instantiates commands.
|
|
12
|
+
# This is necessary because command classes use Import which resolves
|
|
13
|
+
# dependencies at instantiation time (before CommandWrapper#call).
|
|
14
|
+
# Without this, cache backends would use default config instead of user config.
|
|
15
|
+
#
|
|
16
|
+
# Note: --config-path option is handled separately in CommandWrapper and
|
|
17
|
+
# will override settings if specified.
|
|
18
|
+
config_path_index = ARGV.index("--config-path") || ARGV.index("-c")
|
|
19
|
+
if config_path_index && ARGV[config_path_index + 1]
|
|
20
|
+
Factorix.load_config(Pathname(ARGV[config_path_index + 1]))
|
|
21
|
+
elsif ENV["FACTORIX_CONFIG"]
|
|
22
|
+
Factorix.load_config(Pathname(ENV.fetch("FACTORIX_CONFIG")))
|
|
23
|
+
else
|
|
24
|
+
default_config = Factorix::Container[:runtime].factorix_config_path
|
|
25
|
+
Factorix.load_config(default_config) if default_config.exist?
|
|
26
|
+
end
|
|
27
|
+
|
|
11
28
|
begin
|
|
12
29
|
Dry::CLI.new(Factorix::CLI).call
|
|
13
30
|
exit 0
|
|
@@ -12,11 +12,9 @@ module Factorix
|
|
|
12
12
|
# when FACTORIO_USERNAME/FACTORIO_TOKEN environment variables are not set.
|
|
13
13
|
# It's resolved lazily via reader method instead.
|
|
14
14
|
# @!parse
|
|
15
|
-
# # @return [Transfer::Downloader]
|
|
16
|
-
# attr_reader :downloader
|
|
17
15
|
# # @return [Dry::Logger::Dispatcher]
|
|
18
16
|
# attr_reader :logger
|
|
19
|
-
include Import[:
|
|
17
|
+
include Import[:logger]
|
|
20
18
|
|
|
21
19
|
BASE_URL = "https://mods.factorio.com"
|
|
22
20
|
private_constant :BASE_URL
|
|
@@ -34,24 +32,31 @@ module Factorix
|
|
|
34
32
|
# @param download_url [String] relative download URL from API response (e.g., "/download/mod-name/...")
|
|
35
33
|
# @param output [Pathname] output file path
|
|
36
34
|
# @param expected_sha1 [String, nil] expected SHA1 digest for verification (optional)
|
|
35
|
+
# @param handler [Object, nil] event handler for download progress (optional)
|
|
37
36
|
# @return [void]
|
|
38
37
|
# @raise [ArgumentError] if download_url is not a relative path starting with "/"
|
|
39
38
|
# @raise [DigestMismatchError] if SHA1 verification fails
|
|
40
|
-
def download(download_url, output, expected_sha1: nil)
|
|
39
|
+
def download(download_url, output, expected_sha1: nil, handler: nil)
|
|
41
40
|
unless download_url.start_with?("/")
|
|
42
41
|
logger.error("Invalid download_url", url: download_url)
|
|
43
42
|
raise ArgumentError, "download_url must be a relative path starting with '/'"
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
uri = build_download_uri(download_url)
|
|
47
|
-
downloader
|
|
46
|
+
downloader = Container[:downloader]
|
|
47
|
+
downloader.subscribe(handler) if handler
|
|
48
|
+
begin
|
|
49
|
+
downloader.download(uri, output, expected_sha1:)
|
|
50
|
+
ensure
|
|
51
|
+
downloader.unsubscribe(handler) if handler
|
|
52
|
+
end
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
private def service_credential
|
|
51
56
|
return @service_credential if defined?(@service_credential)
|
|
52
57
|
|
|
53
58
|
@service_credential_mutex.synchronize do
|
|
54
|
-
@service_credential ||=
|
|
59
|
+
@service_credential ||= Container[:service_credential]
|
|
55
60
|
end
|
|
56
61
|
end
|
|
57
62
|
|
|
@@ -114,7 +114,7 @@ module Factorix
|
|
|
114
114
|
|
|
115
115
|
URI(value)
|
|
116
116
|
rescue URI::InvalidURIError => e
|
|
117
|
-
|
|
117
|
+
Container[:logger].warn("Skipping invalid URI '#{value}': #{e.message}")
|
|
118
118
|
nil
|
|
119
119
|
end
|
|
120
120
|
end
|
|
@@ -145,7 +145,7 @@ module Factorix
|
|
|
145
145
|
Release[**r]
|
|
146
146
|
rescue RangeError => e
|
|
147
147
|
# Skip releases with invalid version numbers
|
|
148
|
-
|
|
148
|
+
Container[:logger].warn("Skipping release #{name}@#{r[:version]}: #{e.message}")
|
|
149
149
|
nil
|
|
150
150
|
end
|
|
151
151
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
4
|
require "json"
|
|
5
|
-
require "tempfile"
|
|
6
5
|
require "uri"
|
|
7
6
|
|
|
8
7
|
module Factorix
|
|
@@ -86,13 +85,13 @@ module Factorix
|
|
|
86
85
|
|
|
87
86
|
# Invalidate get_mod cache
|
|
88
87
|
mod_uri = build_uri("/api/mods/#{encoded_name}")
|
|
89
|
-
|
|
90
|
-
cache.with_lock(
|
|
88
|
+
mod_cache_key = mod_uri.to_s
|
|
89
|
+
cache.with_lock(mod_cache_key) { cache.delete(mod_cache_key) }
|
|
91
90
|
|
|
92
91
|
# Invalidate get_mod_full cache
|
|
93
92
|
full_uri = build_uri("/api/mods/#{encoded_name}/full")
|
|
94
|
-
|
|
95
|
-
cache.with_lock(
|
|
93
|
+
full_cache_key = full_uri.to_s
|
|
94
|
+
cache.with_lock(full_cache_key) { cache.delete(full_cache_key) }
|
|
96
95
|
|
|
97
96
|
logger.debug("Invalidated cache for MOD", mod: mod_name)
|
|
98
97
|
end
|
|
@@ -101,55 +100,13 @@ module Factorix
|
|
|
101
100
|
URI.join(BASE_URL, path).tap {|uri| uri.query = URI.encode_www_form(params.sort.to_h) unless params.empty? }
|
|
102
101
|
end
|
|
103
102
|
|
|
104
|
-
# Fetch data
|
|
103
|
+
# Fetch data from API (caching is handled by CacheDecorator in api_http_client)
|
|
105
104
|
#
|
|
106
105
|
# @param uri [URI::HTTPS] URI to fetch
|
|
107
106
|
# @return [Hash{Symbol => untyped}] parsed JSON response with symbolized keys
|
|
108
107
|
private def fetch_with_cache(uri)
|
|
109
|
-
key = cache.key_for(uri.to_s)
|
|
110
|
-
|
|
111
|
-
cached = cache.read(key, encoding: "UTF-8")
|
|
112
|
-
if cached
|
|
113
|
-
logger.debug("API cache hit", uri: uri.to_s)
|
|
114
|
-
return JSON.parse(cached, symbolize_names: true)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
logger.debug("API cache miss", uri: uri.to_s)
|
|
118
|
-
response_body = fetch_from_api(uri)
|
|
119
|
-
|
|
120
|
-
store_in_cache(key, response_body)
|
|
121
|
-
|
|
122
|
-
JSON.parse(response_body, symbolize_names: true)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Fetch data from API via HTTP
|
|
126
|
-
#
|
|
127
|
-
# @param uri [URI::HTTPS] URI to fetch
|
|
128
|
-
# @return [String] response body
|
|
129
|
-
# @raise [HTTPClientError] for 4xx errors
|
|
130
|
-
# @raise [HTTPServerError] for 5xx errors
|
|
131
|
-
private def fetch_from_api(uri)
|
|
132
|
-
logger.info("Fetching from API", uri: uri.to_s)
|
|
133
108
|
response = client.get(uri)
|
|
134
|
-
|
|
135
|
-
response.body
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Store response body in cache via temporary file
|
|
139
|
-
#
|
|
140
|
-
# @param key [String] cache key
|
|
141
|
-
# @param data [String] response body
|
|
142
|
-
# @return [void]
|
|
143
|
-
private def store_in_cache(key, data)
|
|
144
|
-
temp_file = Tempfile.new("cache")
|
|
145
|
-
begin
|
|
146
|
-
temp_file.write(data)
|
|
147
|
-
temp_file.close
|
|
148
|
-
cache.store(key, Pathname(temp_file.path))
|
|
149
|
-
logger.debug("Stored API response in cache", key:)
|
|
150
|
-
ensure
|
|
151
|
-
temp_file.unlink
|
|
152
|
-
end
|
|
109
|
+
JSON.parse((+response.body).force_encoding(Encoding::UTF_8), symbolize_names: true)
|
|
153
110
|
end
|
|
154
111
|
|
|
155
112
|
# Validate page_size parameter
|
|
@@ -22,7 +22,7 @@ module Factorix
|
|
|
22
22
|
# @return [APICredential] new instance with API key from environment
|
|
23
23
|
# @raise [CredentialError] if API key is not set in environment
|
|
24
24
|
def self.load
|
|
25
|
-
logger =
|
|
25
|
+
logger = Container["logger"]
|
|
26
26
|
logger.debug "Loading API credentials from environment"
|
|
27
27
|
|
|
28
28
|
api_key = ENV.fetch(ENV_API_KEY, nil)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
module Cache
|
|
5
|
+
# Abstract base class for cache backends.
|
|
6
|
+
#
|
|
7
|
+
# All cache backends (FileSystem, S3, Redis) inherit from this class
|
|
8
|
+
# and implement the abstract methods defined here.
|
|
9
|
+
#
|
|
10
|
+
# @abstract Subclasses must implement all abstract methods.
|
|
11
|
+
class Base
|
|
12
|
+
# @return [Integer, nil] time-to-live in seconds (nil for unlimited)
|
|
13
|
+
attr_reader :ttl
|
|
14
|
+
|
|
15
|
+
# Initialize a new cache backend.
|
|
16
|
+
#
|
|
17
|
+
# @param ttl [Integer, nil] time-to-live in seconds (nil for unlimited)
|
|
18
|
+
def initialize(ttl: nil)
|
|
19
|
+
@ttl = ttl
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if a cache entry exists and is not expired.
|
|
23
|
+
#
|
|
24
|
+
# @param key [String] logical cache key
|
|
25
|
+
# @return [Boolean] true if the cache entry exists and is valid
|
|
26
|
+
# @abstract
|
|
27
|
+
def exist?(key) = raise NotImplementedError, "#{self.class}#exist? must be implemented"
|
|
28
|
+
|
|
29
|
+
# Read a cached entry as a string.
|
|
30
|
+
#
|
|
31
|
+
# @param key [String] logical cache key
|
|
32
|
+
# @return [String, nil] cached content or nil if not found/expired
|
|
33
|
+
# @abstract
|
|
34
|
+
def read(key) = raise NotImplementedError, "#{self.class}#read must be implemented"
|
|
35
|
+
|
|
36
|
+
# Write cached content to a file.
|
|
37
|
+
#
|
|
38
|
+
# Unlike {#read} which returns content as a String, this method writes
|
|
39
|
+
# directly to a file path, which is more memory-efficient for large files.
|
|
40
|
+
#
|
|
41
|
+
# @param key [String] logical cache key
|
|
42
|
+
# @param output [Pathname] path to write the cached content
|
|
43
|
+
# @return [Boolean] true if written successfully, false if not found/expired
|
|
44
|
+
# @abstract
|
|
45
|
+
def write_to(key, output) = raise NotImplementedError, "#{self.class}#write_to must be implemented"
|
|
46
|
+
|
|
47
|
+
# Store data in the cache.
|
|
48
|
+
#
|
|
49
|
+
# @param key [String] logical cache key
|
|
50
|
+
# @param src [Pathname] path to the source file
|
|
51
|
+
# @return [Boolean] true if stored successfully
|
|
52
|
+
# @abstract
|
|
53
|
+
def store(key, src) = raise NotImplementedError, "#{self.class}#store must be implemented"
|
|
54
|
+
|
|
55
|
+
# Delete a cache entry.
|
|
56
|
+
#
|
|
57
|
+
# @param key [String] logical cache key
|
|
58
|
+
# @return [Boolean] true if deleted, false if not found
|
|
59
|
+
# @abstract
|
|
60
|
+
def delete(key) = raise NotImplementedError, "#{self.class}#delete must be implemented"
|
|
61
|
+
|
|
62
|
+
# Clear all cache entries.
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
# @abstract
|
|
66
|
+
def clear = raise NotImplementedError, "#{self.class}#clear must be implemented"
|
|
67
|
+
|
|
68
|
+
# Execute a block with an exclusive lock on the cache entry.
|
|
69
|
+
#
|
|
70
|
+
# @param key [String] logical cache key
|
|
71
|
+
# @yield block to execute with lock held
|
|
72
|
+
# @abstract
|
|
73
|
+
def with_lock(key) = raise NotImplementedError, "#{self.class}#with_lock must be implemented"
|
|
74
|
+
|
|
75
|
+
# Get the age of a cache entry in seconds.
|
|
76
|
+
#
|
|
77
|
+
# @param key [String] logical cache key
|
|
78
|
+
# @return [Float, nil] age in seconds, or nil if entry doesn't exist
|
|
79
|
+
# @abstract
|
|
80
|
+
def age(key) = raise NotImplementedError, "#{self.class}#age must be implemented"
|
|
81
|
+
|
|
82
|
+
# Check if a cache entry has expired based on TTL.
|
|
83
|
+
#
|
|
84
|
+
# @param key [String] logical cache key
|
|
85
|
+
# @return [Boolean] true if expired, false otherwise
|
|
86
|
+
# @abstract
|
|
87
|
+
def expired?(key) = raise NotImplementedError, "#{self.class}#expired? must be implemented"
|
|
88
|
+
|
|
89
|
+
# Get the size of a cached entry in bytes.
|
|
90
|
+
#
|
|
91
|
+
# @param key [String] logical cache key
|
|
92
|
+
# @return [Integer, nil] size in bytes, or nil if entry doesn't exist/expired
|
|
93
|
+
# @abstract
|
|
94
|
+
def size(key) = raise NotImplementedError, "#{self.class}#size must be implemented"
|
|
95
|
+
|
|
96
|
+
# Enumerate cache entries.
|
|
97
|
+
#
|
|
98
|
+
# Yields [key, entry] pairs similar to Hash#each.
|
|
99
|
+
#
|
|
100
|
+
# @yield [key, entry] logical key and Entry object
|
|
101
|
+
# @yieldparam key [String] logical cache key
|
|
102
|
+
# @yieldparam entry [Entry] cache entry metadata
|
|
103
|
+
# @return [Enumerator] if no block given
|
|
104
|
+
# @abstract
|
|
105
|
+
def each = raise NotImplementedError, "#{self.class}#each must be implemented"
|
|
106
|
+
|
|
107
|
+
# Return backend-specific information.
|
|
108
|
+
#
|
|
109
|
+
# Subclasses should override this method to provide configuration details
|
|
110
|
+
# specific to their backend implementation.
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash] backend-specific information (empty by default)
|
|
113
|
+
def backend_info = {}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
module Cache
|
|
5
|
+
Entry = Data.define(:size, :age, :expired)
|
|
6
|
+
|
|
7
|
+
# Represents a cache entry for enumeration operations.
|
|
8
|
+
#
|
|
9
|
+
# Used by {Base#each} to yield entry metadata alongside keys.
|
|
10
|
+
# Note: The key is NOT included in Entry; it is yielded separately.
|
|
11
|
+
#
|
|
12
|
+
# @!attribute [r] size
|
|
13
|
+
# @return [Integer] entry size in bytes
|
|
14
|
+
# @!attribute [r] age
|
|
15
|
+
# @return [Float] age in seconds since creation/modification
|
|
16
|
+
class Entry
|
|
17
|
+
private :expired
|
|
18
|
+
|
|
19
|
+
# Check if the cache entry has expired.
|
|
20
|
+
#
|
|
21
|
+
# @return [Boolean] true if entry has exceeded TTL
|
|
22
|
+
def expired? = expired
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|