rails_query 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: c5fc3a99fb7dcbafa2b6f9385808a7474b2c9b5a3405da0b91a1fb8ba56cf27a
4
- data.tar.gz: 1cdeff8c81675c2ff6db8bc229ec5c6abc293c7b527561b35e5e0c1b6e4040b5
3
+ metadata.gz: e8ca91e8274e6e24acc0f4101ad2ef02b18a4aeef9e2adafdc65f28188c80fc5
4
+ data.tar.gz: 54d293281ead95744f0970457f91fe0367c84bb8b623f54a2a2fe607900023d5
5
5
  SHA512:
6
- metadata.gz: 2edc4e880007981026be9d9dfe6b91243d81400473561b414ca22687b6d60cb0b6de2b423772482d829bf64330236b6c2d6f6614eb6403a2caad1ef7f494524d
7
- data.tar.gz: f99c922f1e671480f609f90f4581761a8701243f221e2bceeefe0c1940897d3448199e98fbb88203756269c2de1e969fa808a4c915bcea7c40fc18721fe697be
6
+ metadata.gz: 2c16e22500a124ca6f03e7e8d37761457c6aadf69e5bccb4fd6eda48fea1859ed911899fb93ac6b17b8b7f9dc22016bc61391bda6a4809a39171adf56982645a
7
+ data.tar.gz: b6de1573128e8c403af465e0927af4b9402e319897cbe6759069cd6b18b6f4cd348fb4294464b9e94f91af98135c2ce13a9cfa66c9ed12a0fa6a2ac360bc24ea
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-04-02
4
+ * Add support stale time by @ilsonlasmar in https://github.com/ilsonlasmar/rails_query/pull/7
5
+ * Support more ruby version by @ilsonlasmar in https://github.com/ilsonlasmar/rails_query/pull/8
6
+
7
+ ## [0.2.0] - 2026-04-02
8
+ * Fix badge version in README by @ilsonlasmar in https://github.com/ilsonlasmar/rails_query/pull/4
9
+ * Add dynamic values by context by @ilsonlasmar in https://github.com/ilsonlasmar/rails_query/pull/5
10
+ * change version 0.2.0 by @ilsonlasmar in https://github.com/ilsonlasmar/rails_query/pull/6
11
+
3
12
  ## [0.1.0] - 2026-04-29
4
13
 
5
14
  🚀 [FEATURE] Initial release of Rails Query
data/README.md CHANGED
@@ -50,6 +50,7 @@ app/providers/
50
50
  - [Generators](#generators)
51
51
  - [Caching](#caching)
52
52
  - [Invalidation](#invalidation)
53
+ - [Stale Time](#stale-time)
53
54
 
54
55
 
55
56
  ## Installation
@@ -141,7 +142,7 @@ UserProvider
141
142
 
142
143
  ```ruby
143
144
  class UserProvider
144
- include RailsQuery::Provider
145
+ include RailsQuery::Adapter
145
146
 
146
147
  base_url ENV["USER_API_URL"]
147
148
 
@@ -196,6 +197,41 @@ Mutations invalidate cache after execution.
196
197
  invalidates "UserProvider"
197
198
  ```
198
199
 
200
+ # Stale Time
201
+ `stale` defines how long cached data is considered **fresh**.
202
+
203
+ #### How it works
204
+ - While data is **fresh**:
205
+ - RailsQuery will **not trigger new requests**
206
+ - Cached data is returned immediately
207
+ - No background refetch occurs
208
+
209
+ - When data becomes **stale**:
210
+ - Data remains in cache
211
+ - RailsQuery may **refetch in the background** when a trigger occurs
212
+
213
+ ### Default behavior
214
+ Stale behavior is disabled by default
215
+
216
+ ```ruby
217
+ stale nil
218
+ ```
219
+
220
+ ### Obs.:
221
+ Never configure `stale` to be greater than your cache expiration (TTL or equivalent).
222
+
223
+ If data is considered fresh for longer than it actually exists in cache, RailsQuery will be forced to fetch everything again from the source, losing the benefit of freshness.
224
+
225
+
226
+ ### Stale vs Cache Expiration (TTL)
227
+
228
+ | Feature | Stale | Cache expiration (TTL) |
229
+ |--------|--------|------------------------|
230
+ | **What it controls** | When data is considered outdated and eligible for refetch | How long data remains stored in cache |
231
+ | **Does the data still exist?** | Yes, but marked as stale | Yes, until it expires and is removed |
232
+ | **Triggers new request?** | No while fresh; yes when stale (on next call/trigger) | Not directly; only after cache is gone |
233
+ | **Default** | nil (disabled) | 0 (not cache store) |
234
+ | **Example** | 0 (revalidate always) | 5 minutes (after disuse) |
199
235
 
200
236
  ## Development
201
237
 
@@ -4,16 +4,34 @@ module RailsQuery
4
4
  # Internal client class responsible for cache interactions
5
5
  class Client
6
6
  def initialize(config)
7
- @cache = config.cache_store
8
- @default_ttl = config.default_ttl
9
- @namespace = config.namespace
10
- @logger = config.logger
7
+ @cache = config.cache_store
8
+ @default_ttl = config.default_ttl
9
+ @default_stale = config.default_stale
10
+ @executor = config.executor
11
+ @namespace = config.namespace
12
+ @logger = config.logger
11
13
  end
12
14
 
13
- def fetch(key, ttl: @default_ttl, provider: nil, &block)
14
- store_index(provider, key) if provider
15
+ def fetch(key, ttl: @default_ttl, stale: @default_stale, provider: nil, &block)
15
16
  namespaced = namespaced_key(key)
16
- @cache.fetch(namespaced, expires_in: ttl, &block)
17
+ entry = @cache.read(namespaced)
18
+
19
+ if entry
20
+ age = Time.now - entry[:fetched_at]
21
+ async_refetch(namespaced, ttl, provider, &block) if stale && stale < age
22
+ return entry[:data]
23
+ end
24
+
25
+ data = block.call
26
+ write(namespaced, data, ttl, provider: provider)
27
+
28
+ data
29
+ end
30
+
31
+ def write(key, data, ttl, provider: nil)
32
+ store_index(provider, key) if provider
33
+
34
+ @cache.write(key, { data: data, fetched_at: Time.now }, expires_in: ttl)
17
35
  end
18
36
 
19
37
  def store_index(provider, key)
@@ -45,6 +63,36 @@ module RailsQuery
45
63
 
46
64
  private
47
65
 
66
+ def in_lock?(key)
67
+ lock_key = "lock:#{key}"
68
+ @cache.exist?(lock_key)
69
+ end
70
+
71
+ def write_lock(key)
72
+ lock_key = "lock:#{key}"
73
+ @cache.write(lock_key, true, expires_in: 10.seconds)
74
+ end
75
+
76
+ def delete_lock(key)
77
+ lock_key = "lock:#{key}"
78
+ @cache.delete(lock_key)
79
+ end
80
+
81
+ def async_refetch(key, ttl, provider, &block)
82
+ return if in_lock?(key)
83
+
84
+ write_lock(key)
85
+
86
+ @executor.post do
87
+ data = block.call
88
+ write(key, data, ttl, provider: provider)
89
+ rescue StandardError => e
90
+ @logger.error("[RailsQuery] SWR refetch failed: #{e.message}")
91
+ ensure
92
+ delete_lock(key)
93
+ end
94
+ end
95
+
48
96
  def namespaced_key(key)
49
97
  "#{@namespace}:#{Array(key).join(":")}"
50
98
  end
@@ -3,11 +3,13 @@
3
3
  module RailsQuery
4
4
  # Configuration class for RailsQuery
5
5
  class Configuration
6
- attr_accessor :cache_store, :default_ttl, :namespace, :logger
6
+ attr_accessor :cache_store, :default_ttl, :default_stale, :namespace, :executor, :logger
7
7
 
8
8
  def initialize
9
9
  @cache_store = default_cache_store
10
- @default_ttl = 30.seconds
10
+ @executor = default_executor
11
+ @default_ttl = 0
12
+ @default_stale = nil
11
13
  @namespace = "rails_query"
12
14
  @logger = default_logger
13
15
  end
@@ -22,6 +24,15 @@ module RailsQuery
22
24
  end
23
25
  end
24
26
 
27
+ def default_executor
28
+ Concurrent::ThreadPoolExecutor.new(
29
+ min_threads: 2,
30
+ max_threads: Concurrent.processor_count * 2,
31
+ max_queue: 100,
32
+ fallback_policy: :discard
33
+ )
34
+ end
35
+
25
36
  def default_logger
26
37
  if defined?(Rails) && Rails.respond_to?(:logger)
27
38
  Rails.logger
@@ -13,6 +13,11 @@ module RailsQuery
13
13
  @ttl
14
14
  end
15
15
 
16
+ def stale(value = nil)
17
+ @stale = value if value
18
+ @stale
19
+ end
20
+
16
21
  def key(&block)
17
22
  @key_block = block if block
18
23
  @key_block
@@ -22,7 +27,7 @@ module RailsQuery
22
27
  def call(*args, **opts)
23
28
  key = build_key(*args, **opts)
24
29
  provider_class = opts[:provider] || self.class.name
25
- RailsQuery.client.fetch(key, ttl: ttl, provider: provider_class) do
30
+ RailsQuery.client.fetch(key, ttl: ttl, stale: stale, provider: provider_class) do
26
31
  kwargs? ? resolve(*args, **opts) : resolve(*args)
27
32
  end
28
33
  end
@@ -31,6 +36,10 @@ module RailsQuery
31
36
  self.class.ttl || RailsQuery.configuration.default_ttl
32
37
  end
33
38
 
39
+ def stale
40
+ self.class.stale || RailsQuery.configuration.default_stale
41
+ end
42
+
34
43
  def build_key(*args, **opts)
35
44
  opts = opts.select { |k, _| self.class.key.parameters.map(&:last).include?(k) }
36
45
  return instance_exec(*args, **opts, &self.class.key) if self.class.key
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsQuery
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_query
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
  - Ilson Lasmar
@@ -9,6 +9,34 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: concurrent-ruby
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: minitest
14
42
  requirement: !ruby/object:Gem::Requirement
@@ -52,19 +80,33 @@ dependencies:
52
80
  - !ruby/object:Gem::Version
53
81
  version: '0.14'
54
82
  - !ruby/object:Gem::Dependency
55
- name: rails
83
+ name: rake
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '13.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '13.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop
56
98
  requirement: !ruby/object:Gem::Requirement
57
99
  requirements:
58
100
  - - "~>"
59
101
  - !ruby/object:Gem::Version
60
- version: '7.1'
102
+ version: '1.21'
61
103
  type: :development
62
104
  prerelease: false
63
105
  version_requirements: !ruby/object:Gem::Requirement
64
106
  requirements:
65
107
  - - "~>"
66
108
  - !ruby/object:Gem::Version
67
- version: '7.1'
109
+ version: '1.21'
68
110
  - !ruby/object:Gem::Dependency
69
111
  name: simplecov
70
112
  requirement: !ruby/object:Gem::Requirement