response_bank 1.1.0 → 1.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/README.md +91 -89
- data/lib/response_bank/middleware.rb +21 -27
- data/lib/response_bank/response_cache_handler.rb +76 -59
- data/lib/response_bank/version.rb +1 -1
- data/lib/response_bank.rb +37 -13
- metadata +15 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d25739f9bd846e1cd3364a422cc90590b32fcb5f7e56cd307af0e83f0d5ddac
|
4
|
+
data.tar.gz: 14c1a7f96abcc45025cee9f998342fb6185779774dc72986ebb1a5bbcb96c413
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6120bc7a707fa5ddd1cd3a143e40681e375864b075fe0e74b1128cc4ee460690400a70c47599772addba8d2bee56b02ce14a820c5e4af376f0eaa154f1fe88c
|
7
|
+
data.tar.gz: 5ae1545f2c40d7abbf06b8ba43c8d353cb171fc97ce667fa280c5f991c37f7e514c7673898a99242e446f9975ef22f4cb91eaa053165664a9c309fa4d6b39197
|
data/README.md
CHANGED
@@ -1,126 +1,128 @@
|
|
1
|
-
# ResponseBank [](http://travis-ci.org/Shopify/response_bank)
|
1
|
+
# ResponseBank [](http://travis-ci.org/Shopify/response_bank) [](https://github.com/Shopify/response_bank/actions/workflows/ci.yml)
|
2
2
|
|
3
|
-
|
3
|
+
## Features
|
4
4
|
|
5
5
|
* Serve gzip'd content
|
6
6
|
* Add ETag and 304 Not Modified headers
|
7
7
|
* Generational caching
|
8
8
|
* No explicit expiry
|
9
9
|
|
10
|
-
|
10
|
+
## Support
|
11
11
|
|
12
12
|
This gem supports the following versions of Ruby and Rails:
|
13
13
|
|
14
|
-
* Ruby 2.
|
15
|
-
* Rails
|
14
|
+
* Ruby 2.7.0+
|
15
|
+
* Rails 6.0.0+
|
16
16
|
|
17
|
-
|
17
|
+
## Usage
|
18
18
|
|
19
19
|
1. include the gem in your Gemfile
|
20
20
|
|
21
|
-
```ruby
|
22
|
-
gem 'response_bank'
|
23
|
-
```
|
21
|
+
```ruby
|
22
|
+
gem 'response_bank'
|
23
|
+
```
|
24
24
|
|
25
25
|
2. add an initializer file. We need to configure the `acquire_lock` method, set the cache store and the logger
|
26
26
|
|
27
|
-
```ruby
|
28
|
-
require 'response_bank'
|
27
|
+
```ruby
|
28
|
+
require 'response_bank'
|
29
29
|
|
30
|
-
module ResponseBank
|
31
|
-
|
30
|
+
module ResponseBank
|
31
|
+
LOCK_TTL = 90
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
class << self
|
34
|
+
def acquire_lock(cache_key)
|
35
|
+
cache_store.write("#{cache_key}:lock", '1', unless_exist: true, expires_in: LOCK_TTL, raw: true)
|
36
|
+
end
|
37
|
+
end
|
36
38
|
end
|
37
|
-
end
|
38
|
-
end
|
39
39
|
|
40
|
-
ResponseBank.cache_store = ActiveSupport::Cache.lookup_store(Rails.configuration.cache_store)
|
41
|
-
ResponseBank.logger = Rails.logger
|
40
|
+
ResponseBank.cache_store = ActiveSupport::Cache.lookup_store(Rails.configuration.cache_store)
|
41
|
+
ResponseBank.logger = Rails.logger
|
42
42
|
|
43
|
-
```
|
43
|
+
```
|
44
44
|
|
45
45
|
3. enables caching on your application
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
config.action_controller.perform_caching = true
|
49
|
+
```
|
49
50
|
|
50
51
|
4. use `#response_cache` method to any desired controller's action
|
51
52
|
|
52
|
-
```ruby
|
53
|
-
class PostsController < ApplicationController
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
```ruby
|
54
|
+
class PostsController < ApplicationController
|
55
|
+
def show
|
56
|
+
response_cache do
|
57
|
+
@post = @shop.posts.find(params[:id])
|
58
|
+
respond_with(@post)
|
59
|
+
end
|
60
|
+
end
|
58
61
|
end
|
59
|
-
|
60
|
-
end
|
61
|
-
```
|
62
|
+
```
|
62
63
|
|
63
64
|
5. **(optional)** set a custom TTL for the cache by overriding the `write_to_backing_cache_store` method in your initializer file
|
64
|
-
```ruby
|
65
|
-
module ResponseBank
|
66
|
-
CACHE_TTL = 30.minutes
|
67
|
-
def write_to_backing_cache_store(_env, key, payload, expires_in: CACHE_TTL)
|
68
|
-
cache_store.write(key, payload, raw: true, expires_in: expires_in)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
```
|
72
65
|
|
73
|
-
|
66
|
+
```ruby
|
67
|
+
module ResponseBank
|
68
|
+
CACHE_TTL = 30.minutes
|
69
|
+
def write_to_backing_cache_store(_env, key, payload, expires_in: nil)
|
70
|
+
cache_store.write(key, payload, raw: true, expires_in: expires_in || CACHE_TTL)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
74
|
|
75
|
-
|
76
|
-
class PostsController < ApplicationController
|
77
|
-
before_action :set_shop
|
75
|
+
6. **(optional)** override custom cache key data. For default, cache key is defined by URL and query string
|
78
76
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
77
|
+
```ruby
|
78
|
+
class PostsController < ApplicationController
|
79
|
+
before_action :set_shop
|
80
|
+
|
81
|
+
def index
|
82
|
+
response_cache do
|
83
|
+
@post = @shop.posts
|
84
|
+
respond_with(@post)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def show
|
89
|
+
response_cache do
|
90
|
+
@post = @shop.posts.find(params[:id])
|
91
|
+
respond_with(@post)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def another_action
|
96
|
+
# custom cache key data
|
97
|
+
cache_key = {
|
98
|
+
action: action_name,
|
99
|
+
format: request.format,
|
100
|
+
shop_updated_at: @shop.updated_at
|
101
|
+
# you may add more keys here
|
102
|
+
}
|
103
|
+
response_cache cache_key do
|
104
|
+
@post = @shop.posts.find(params[:id])
|
105
|
+
respond_with(@post)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# override default cache key data globally per class
|
110
|
+
def cache_key_data
|
111
|
+
{
|
112
|
+
action: action_name,
|
113
|
+
format: request.format,
|
114
|
+
params: params.slice(:id),
|
115
|
+
shop_version: @shop.version
|
116
|
+
# you may add more keys here
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_shop
|
121
|
+
# @shop = ...
|
122
|
+
end
|
83
123
|
end
|
84
|
-
|
124
|
+
```
|
85
125
|
|
86
|
-
|
87
|
-
response_cache do
|
88
|
-
@post = @shop.posts.find(params[:id])
|
89
|
-
respond_with(@post)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def another_action
|
94
|
-
# custom cache key data
|
95
|
-
cache_key = {
|
96
|
-
action: action_name,
|
97
|
-
format: request.format,
|
98
|
-
shop_updated_at: @shop.updated_at
|
99
|
-
# you may add more keys here
|
100
|
-
}
|
101
|
-
response_cache cache_key do
|
102
|
-
@post = @shop.posts.find(params[:id])
|
103
|
-
respond_with(@post)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# override default cache key data globally per class
|
108
|
-
def cache_key_data
|
109
|
-
{
|
110
|
-
action: action_name,
|
111
|
-
format: request.format,
|
112
|
-
params: params.slice(:id),
|
113
|
-
shop_version: @shop.version
|
114
|
-
# you may add more keys here
|
115
|
-
}
|
116
|
-
end
|
117
|
-
|
118
|
-
def set_shop
|
119
|
-
# @shop = ...
|
120
|
-
end
|
121
|
-
end
|
122
|
-
```
|
123
|
-
|
124
|
-
### License
|
126
|
+
## License
|
125
127
|
|
126
128
|
ResponseBank is released under the [MIT License](LICENSE.txt).
|
@@ -1,8 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require 'useragent'
|
3
2
|
|
4
3
|
module ResponseBank
|
5
4
|
class Middleware
|
5
|
+
# Limit the cached headers
|
6
|
+
# TODO: Make this lowercase/case-insentitive as per rfc2616 §4.2
|
7
|
+
CACHEABLE_HEADERS = ["Location", "Content-Type", "ETag", "Content-Encoding", "Last-Modified", "Cache-Control", "Expires", "Link", "Surrogate-Keys", "Cache-Tags"].freeze
|
8
|
+
|
6
9
|
REQUESTED_WITH = "HTTP_X_REQUESTED_WITH"
|
7
10
|
ACCEPT = "HTTP_ACCEPT"
|
8
11
|
USER_AGENT = "HTTP_USER_AGENT"
|
@@ -13,18 +16,14 @@ module ResponseBank
|
|
13
16
|
|
14
17
|
def call(env)
|
15
18
|
env['cacheable.cache'] = false
|
16
|
-
|
19
|
+
content_encoding = env['response_bank.server_cache_encoding'] = ResponseBank.check_encoding(env)
|
17
20
|
|
18
21
|
status, headers, body = @app.call(env)
|
19
22
|
|
20
23
|
if env['cacheable.cache']
|
21
24
|
if [200, 404, 301, 304].include?(status)
|
22
25
|
headers['ETag'] = env['cacheable.key']
|
23
|
-
headers['X-Alternate-Cache-Key'] = env['cacheable.unversioned-key']
|
24
26
|
|
25
|
-
if ie_ajax_request?(env)
|
26
|
-
headers["Expires"] = "-1"
|
27
|
-
end
|
28
27
|
end
|
29
28
|
|
30
29
|
if [200, 404, 301].include?(status) && env['cacheable.miss']
|
@@ -36,31 +35,35 @@ module ResponseBank
|
|
36
35
|
body.each { |part| body_string << part }
|
37
36
|
end
|
38
37
|
|
39
|
-
|
38
|
+
body_compressed = nil
|
39
|
+
if body_string && body_string != ""
|
40
|
+
headers['Content-Encoding'] = content_encoding
|
41
|
+
body_compressed = ResponseBank.compress(body_string, content_encoding)
|
42
|
+
end
|
40
43
|
|
44
|
+
cached_headers = headers.slice(*CACHEABLE_HEADERS)
|
41
45
|
# Store result
|
42
|
-
cache_data = [status,
|
43
|
-
cache_data << headers['Location'] if status == 301
|
46
|
+
cache_data = [status, cached_headers, body_compressed, timestamp]
|
44
47
|
|
45
48
|
ResponseBank.write_to_cache(env['cacheable.key']) do
|
46
49
|
payload = MessagePack.dump(cache_data)
|
47
50
|
ResponseBank.write_to_backing_cache_store(
|
48
51
|
env,
|
49
|
-
env['cacheable.key'],
|
52
|
+
env['cacheable.unversioned-key'],
|
50
53
|
payload,
|
51
54
|
expires_in: env['cacheable.versioned-cache-expiry'],
|
52
55
|
)
|
53
|
-
|
54
|
-
if env['cacheable.unversioned-key']
|
55
|
-
ResponseBank.write_to_backing_cache_store(env, env['cacheable.unversioned-key'], payload)
|
56
|
-
end
|
57
56
|
end
|
58
57
|
|
59
|
-
# since we had to generate the
|
58
|
+
# since we had to generate the compressed version already we may
|
60
59
|
# as well serve it if the client wants it
|
61
|
-
if
|
62
|
-
|
63
|
-
|
60
|
+
if body_compressed
|
61
|
+
if env['HTTP_ACCEPT_ENCODING'].to_s.include?(content_encoding)
|
62
|
+
body = [body_compressed]
|
63
|
+
else
|
64
|
+
# Remove content-encoding header for response with compressed content
|
65
|
+
headers.delete('Content-Encoding')
|
66
|
+
end
|
64
67
|
end
|
65
68
|
end
|
66
69
|
|
@@ -80,14 +83,5 @@ module ResponseBank
|
|
80
83
|
Time.now.to_i
|
81
84
|
end
|
82
85
|
|
83
|
-
def ie_ajax_request?(env)
|
84
|
-
return false unless !env[USER_AGENT].nil? && !env[USER_AGENT].empty?
|
85
|
-
|
86
|
-
if env[REQUESTED_WITH] == "XmlHttpRequest" || env[ACCEPT] == "application/json"
|
87
|
-
UserAgent.parse(env["HTTP_USER_AGENT"]).is_a?(UserAgent::Browsers::InternetExplorer)
|
88
|
-
else
|
89
|
-
false
|
90
|
-
end
|
91
|
-
end
|
92
86
|
end
|
93
87
|
end
|
@@ -3,6 +3,8 @@ require 'digest/md5'
|
|
3
3
|
|
4
4
|
module ResponseBank
|
5
5
|
class ResponseCacheHandler
|
6
|
+
CACHE_KEY_SCHEMA_VERSION = 1
|
7
|
+
|
6
8
|
def initialize(
|
7
9
|
key_data:,
|
8
10
|
version_data:,
|
@@ -25,12 +27,13 @@ module ResponseBank
|
|
25
27
|
@force_refill_cache = force_refill_cache
|
26
28
|
@cache_store = cache_store
|
27
29
|
@headers = headers || {}
|
30
|
+
@key_schema_version = @env.key?('cacheable.key_version') ? @env.key['cacheable.key_version'] : CACHE_KEY_SCHEMA_VERSION
|
28
31
|
end
|
29
32
|
|
30
33
|
def run!
|
31
34
|
@env['cacheable.cache'] = true
|
32
|
-
@env['cacheable.key'] =
|
33
|
-
@env['cacheable.unversioned-key'] =
|
35
|
+
@env['cacheable.key'] = entity_tag_hash
|
36
|
+
@env['cacheable.unversioned-key'] = cache_key_hash
|
34
37
|
|
35
38
|
ResponseBank.log(cacheable_info_dump)
|
36
39
|
|
@@ -41,32 +44,32 @@ module ResponseBank
|
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
44
|
-
def
|
45
|
-
@
|
47
|
+
def entity_tag_hash
|
48
|
+
@entity_tag_hash ||= hash(entity_tag)
|
46
49
|
end
|
47
50
|
|
48
|
-
def
|
49
|
-
@
|
51
|
+
def cache_key_hash
|
52
|
+
@cache_key_hash ||= hash(cache_key)
|
50
53
|
end
|
51
54
|
|
52
55
|
private
|
53
56
|
|
54
|
-
def
|
55
|
-
"cacheable
|
57
|
+
def hash(key)
|
58
|
+
"cacheable:" + Digest::MD5.hexdigest(key)
|
56
59
|
end
|
57
60
|
|
58
|
-
def
|
59
|
-
@
|
61
|
+
def entity_tag
|
62
|
+
@entity_tag ||= ResponseBank.cache_key_for(key: @key_data, version: @version_data, key_schema_version: @key_schema_version)
|
60
63
|
end
|
61
64
|
|
62
|
-
def
|
63
|
-
@
|
65
|
+
def cache_key
|
66
|
+
@cache_key ||= ResponseBank.cache_key_for(key: @key_data, key_schema_version: @key_schema_version, encoding: @env['response_bank.server_cache_encoding'])
|
64
67
|
end
|
65
68
|
|
66
69
|
def cacheable_info_dump
|
67
70
|
log_info = [
|
68
|
-
"Raw cacheable.key: #{
|
69
|
-
"cacheable.key: #{
|
71
|
+
"Raw cacheable.key: #{entity_tag}",
|
72
|
+
"cacheable.key: #{entity_tag_hash}",
|
70
73
|
]
|
71
74
|
|
72
75
|
if @env['HTTP_IF_NONE_MATCH']
|
@@ -78,42 +81,19 @@ module ResponseBank
|
|
78
81
|
|
79
82
|
def try_to_serve_from_cache
|
80
83
|
# Etag
|
81
|
-
response = serve_from_browser_cache(
|
82
|
-
|
84
|
+
response = serve_from_browser_cache(entity_tag_hash, @env['HTTP_IF_NONE_MATCH'])
|
83
85
|
return response if response
|
84
86
|
|
85
|
-
|
86
|
-
response = if @serve_unversioned
|
87
|
-
serve_from_cache(unversioned_key_hash, "Cache hit: server (unversioned)")
|
88
|
-
else
|
89
|
-
serve_from_cache(versioned_key_hash, "Cache hit: server")
|
90
|
-
end
|
91
|
-
|
87
|
+
response = serve_from_cache(cache_key_hash, @serve_unversioned ? "*" : entity_tag_hash, @cache_age_tolerance)
|
92
88
|
return response if response
|
93
89
|
|
94
|
-
@env['cacheable.locked'] ||= false
|
95
|
-
|
96
|
-
if @env['cacheable.locked'] || ResponseBank.acquire_lock(versioned_key_hash)
|
97
|
-
# execute if we can get the lock
|
98
|
-
@env['cacheable.locked'] = true
|
99
|
-
elsif serving_from_noncurrent_but_recent_version_acceptable?
|
100
|
-
# serve a stale version
|
101
|
-
response = serve_from_cache(unversioned_key_hash, "Cache hit: server (recent)", @cache_age_tolerance)
|
102
|
-
|
103
|
-
return response if response
|
104
|
-
end
|
105
|
-
|
106
90
|
# No cache hit; this request cannot be handled from cache.
|
107
91
|
# Yield to the controller and mark for writing into cache.
|
108
92
|
refill_cache
|
109
93
|
end
|
110
94
|
|
111
|
-
def
|
112
|
-
|
113
|
-
end
|
114
|
-
|
115
|
-
def serve_from_browser_cache(cache_key_hash)
|
116
|
-
if @env["HTTP_IF_NONE_MATCH"] == cache_key_hash
|
95
|
+
def serve_from_browser_cache(entity_tag, if_none_match)
|
96
|
+
if etag_matches?(entity_tag, if_none_match)
|
117
97
|
@env['cacheable.miss'] = false
|
118
98
|
@env['cacheable.store'] = 'client'
|
119
99
|
|
@@ -126,7 +106,7 @@ module ResponseBank
|
|
126
106
|
end
|
127
107
|
end
|
128
108
|
|
129
|
-
def serve_from_cache(cache_key_hash,
|
109
|
+
def serve_from_cache(cache_key_hash, match_entity_tag = "*", cache_age_tolerance = nil)
|
130
110
|
raw = ResponseBank.read_from_backing_cache_store(@env, cache_key_hash, backing_cache_store: @cache_store)
|
131
111
|
|
132
112
|
if raw
|
@@ -135,37 +115,74 @@ module ResponseBank
|
|
135
115
|
@env['cacheable.miss'] = false
|
136
116
|
@env['cacheable.store'] = 'server'
|
137
117
|
|
138
|
-
status,
|
118
|
+
status, headers, body, timestamp = hit
|
139
119
|
|
140
|
-
|
141
|
-
ResponseBank.log("Found an unversioned cache entry, but it was too old (#{timestamp})")
|
120
|
+
@env['cacheable.locked'] ||= false
|
142
121
|
|
143
|
-
|
122
|
+
# to preserve the unversioned/versioned logging messages from past releases we split the match_entity_tag test
|
123
|
+
if match_entity_tag == "*"
|
124
|
+
ResponseBank.log("Cache hit: server (unversioned)")
|
125
|
+
# page tolerance only applies for versioned + etag mismatch
|
126
|
+
elsif etag_matches?(headers['ETag'], match_entity_tag)
|
127
|
+
ResponseBank.log("Cache hit: server")
|
144
128
|
else
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
129
|
+
# cache miss; check to see if any parallel requests already are regenerating the cache
|
130
|
+
if ResponseBank.acquire_lock(match_entity_tag)
|
131
|
+
# execute if we can get the lock
|
132
|
+
@env['cacheable.locked'] = true
|
133
|
+
return
|
134
|
+
elsif stale_while_revalidate?(timestamp, cache_age_tolerance)
|
135
|
+
# cache is being regenerated, can we avoid piling on and use a stale version in the interim?
|
136
|
+
ResponseBank.log("Cache hit: server (recent)")
|
151
137
|
else
|
152
|
-
|
153
|
-
|
154
|
-
body = ResponseBank.decompress(body)
|
138
|
+
ResponseBank.log("Found an unversioned cache entry, but it was too old (#{timestamp})")
|
139
|
+
return
|
155
140
|
end
|
141
|
+
end
|
156
142
|
|
157
|
-
|
143
|
+
# version check
|
144
|
+
# unversioned but tolerance threshold
|
145
|
+
# regen
|
146
|
+
@headers.merge!(headers)
|
158
147
|
|
159
|
-
|
148
|
+
# if a cache key hit and client doesn't match encoding, return the raw body
|
149
|
+
if !@env['HTTP_ACCEPT_ENCODING'].to_s.include?(@headers['Content-Encoding'])
|
150
|
+
ResponseBank.log("uncompressing payload for client as client doesn't require encoding")
|
151
|
+
body = ResponseBank.decompress(body, @headers['Content-Encoding'])
|
152
|
+
@headers.delete('Content-Encoding')
|
160
153
|
end
|
154
|
+
|
155
|
+
[status, @headers, [body]]
|
156
|
+
|
161
157
|
end
|
162
158
|
end
|
163
159
|
|
164
|
-
def
|
165
|
-
|
160
|
+
def etag_matches?(entity_tag, if_none_match)
|
161
|
+
# Support for Etag variations including:
|
162
|
+
# If-None-Match: abc
|
163
|
+
# If-None-Match: "abc"
|
164
|
+
# If-None-Match: W/"abc"
|
165
|
+
# If-None-Match: "abc", "def"
|
166
|
+
# If-None-Match: "*"
|
167
|
+
return false unless entity_tag
|
168
|
+
return false unless if_none_match
|
169
|
+
|
170
|
+
# strictly speaking an unquoted etag is not valid, yet common
|
171
|
+
# to avoid unintended greedy matches in we check for naked entity then includes with quoted entity values
|
172
|
+
if_none_match == "*" || if_none_match == entity_tag || if_none_match.include?(%{"#{entity_tag}"})
|
173
|
+
end
|
174
|
+
|
175
|
+
def stale_while_revalidate?(timestamp, cache_age_tolerance)
|
176
|
+
return false if !cache_age_tolerance
|
177
|
+
return false if !timestamp
|
178
|
+
|
179
|
+
timestamp >= (Time.now.to_i - cache_age_tolerance)
|
166
180
|
end
|
167
181
|
|
168
182
|
def refill_cache
|
183
|
+
# non cache hits do not yet have the lock
|
184
|
+
ResponseBank.acquire_lock(entity_tag_hash) unless @env['cacheable.locked']
|
185
|
+
@env['cacheable.locked'] = true
|
169
186
|
@env['cacheable.miss'] = true
|
170
187
|
|
171
188
|
ResponseBank.log("Refilling cache")
|
data/lib/response_bank.rb
CHANGED
@@ -3,6 +3,7 @@ require 'response_bank/middleware'
|
|
3
3
|
require 'response_bank/railtie' if defined?(Rails)
|
4
4
|
require 'response_bank/response_cache_handler'
|
5
5
|
require 'msgpack'
|
6
|
+
require 'brotli'
|
6
7
|
|
7
8
|
module ResponseBank
|
8
9
|
class << self
|
@@ -29,17 +30,26 @@ module ResponseBank
|
|
29
30
|
backing_cache_store.read(cache_key, raw: true)
|
30
31
|
end
|
31
32
|
|
32
|
-
def compress(content)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
def compress(content, encoding = "gzip")
|
34
|
+
case encoding
|
35
|
+
when 'gzip'
|
36
|
+
Zlib.gzip(content, level: Zlib::BEST_COMPRESSION)
|
37
|
+
when 'br'
|
38
|
+
Brotli.deflate(content, mode: :text, quality: 7)
|
39
|
+
else
|
40
|
+
raise ArgumentError, "Unsupported encoding: #{encoding}"
|
41
|
+
end
|
39
42
|
end
|
40
43
|
|
41
|
-
def decompress(content)
|
42
|
-
|
44
|
+
def decompress(content, encoding = "gzip")
|
45
|
+
case encoding
|
46
|
+
when 'gzip'
|
47
|
+
Zlib.gunzip(content)
|
48
|
+
when 'br'
|
49
|
+
Brotli.inflate(content)
|
50
|
+
else
|
51
|
+
raise ArgumentError, "Unsupported encoding: #{encoding}"
|
52
|
+
end
|
43
53
|
end
|
44
54
|
|
45
55
|
def cache_key_for(data)
|
@@ -49,17 +59,20 @@ module ResponseBank
|
|
49
59
|
|
50
60
|
key = hash_value_str(data[:key])
|
51
61
|
|
52
|
-
|
62
|
+
key = %{#{data[:key_schema_version]}:#{key}} if data[:key_schema_version]
|
63
|
+
|
64
|
+
key = %{#{key}:#{hash_value_str(data[:version])}} if data[:version]
|
53
65
|
|
54
|
-
|
66
|
+
# add the encoding to only the cache key but don't expose this detail in the entity_tag
|
67
|
+
key = %{#{key}:#{hash_value_str(data[:encoding])}} if data[:encoding] && data[:encoding] != "gzip"
|
55
68
|
|
56
|
-
|
69
|
+
key
|
57
70
|
when Array
|
58
71
|
data.inspect
|
59
72
|
when Time, DateTime
|
60
73
|
data.to_i
|
61
74
|
when Date
|
62
|
-
data.
|
75
|
+
data.to_s # Date#to_i does not support timezones, using iso8601 instead
|
63
76
|
when true, false, Integer, Symbol, String
|
64
77
|
data.inspect
|
65
78
|
else
|
@@ -67,6 +80,17 @@ module ResponseBank
|
|
67
80
|
end
|
68
81
|
end
|
69
82
|
|
83
|
+
def check_encoding(env, default_encoding = 'br')
|
84
|
+
if env['HTTP_ACCEPT_ENCODING'].to_s.include?('br')
|
85
|
+
'br'
|
86
|
+
elsif env['HTTP_ACCEPT_ENCODING'].to_s.include?('gzip')
|
87
|
+
'gzip'
|
88
|
+
else
|
89
|
+
# No encoding requested from client, but we still need to cache the page in server cache
|
90
|
+
default_encoding
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
70
94
|
private
|
71
95
|
|
72
96
|
def hash_value_str(data)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: response_bank
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tobias Lütke
|
@@ -9,10 +9,10 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2023-04-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: msgpack
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '0'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
|
-
name:
|
29
|
+
name: brotli
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
@@ -45,28 +45,28 @@ dependencies:
|
|
45
45
|
requirements:
|
46
46
|
- - ">="
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: 5.
|
48
|
+
version: 5.18.0
|
49
49
|
type: :development
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
53
|
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: 5.
|
55
|
+
version: 5.18.0
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: mocha
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
60
|
- - ">="
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version:
|
62
|
+
version: 2.0.0
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version:
|
69
|
+
version: 2.0.0
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rake
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,28 +87,28 @@ dependencies:
|
|
87
87
|
requirements:
|
88
88
|
- - ">="
|
89
89
|
- !ruby/object:Gem::Version
|
90
|
-
version: '
|
90
|
+
version: '6.1'
|
91
91
|
type: :development
|
92
92
|
prerelease: false
|
93
93
|
version_requirements: !ruby/object:Gem::Requirement
|
94
94
|
requirements:
|
95
95
|
- - ">="
|
96
96
|
- !ruby/object:Gem::Version
|
97
|
-
version: '
|
97
|
+
version: '6.1'
|
98
98
|
- !ruby/object:Gem::Dependency
|
99
|
-
name:
|
99
|
+
name: pry
|
100
100
|
requirement: !ruby/object:Gem::Requirement
|
101
101
|
requirements:
|
102
102
|
- - ">="
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version:
|
104
|
+
version: '0'
|
105
105
|
type: :development
|
106
106
|
prerelease: false
|
107
107
|
version_requirements: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
109
|
- - ">="
|
110
110
|
- !ruby/object:Gem::Version
|
111
|
-
version:
|
111
|
+
version: '0'
|
112
112
|
description:
|
113
113
|
email:
|
114
114
|
- tobi@shopify.com
|
@@ -139,14 +139,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
139
139
|
requirements:
|
140
140
|
- - ">="
|
141
141
|
- !ruby/object:Gem::Version
|
142
|
-
version: 2.
|
142
|
+
version: 2.7.0
|
143
143
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
144
|
requirements:
|
145
145
|
- - ">="
|
146
146
|
- !ruby/object:Gem::Version
|
147
147
|
version: '0'
|
148
148
|
requirements: []
|
149
|
-
rubygems_version: 3.
|
149
|
+
rubygems_version: 3.4.10
|
150
150
|
signing_key:
|
151
151
|
specification_version: 4
|
152
152
|
summary: Simple response caching for Ruby applications
|