response_bank 1.3.7 → 1.3.8

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: 0a5c7a799e1272fffad4878273bf154c09f809059944dbf47bf6c6c83d1b4acb
4
- data.tar.gz: ff7a669443427dcee35c7d2a590258fc0ac0591bd156c1316a42198cacc4bd0e
3
+ metadata.gz: f9c672021efe766d0fc33677c57dd7b1b1c4dc83c1a88824f40919722111b99a
4
+ data.tar.gz: f1215de9f225b7ea78d71cc7c815d7cff0811a737e3e9df4f686261e07339db7
5
5
  SHA512:
6
- metadata.gz: 5ddaeeca4364360ce6f2392d5b53d2f34421e44a62f1184d594add38dc45ebf23f72184bc9aa097667404c83d6b64c450b91b1dd25726a307ba41149b8a9509d
7
- data.tar.gz: 4bfc7ebc29beab0c67f72e24e635d9fa2a0ca59a06fd92027d00172acc31a1974288adee1a97baf005e2ae64751a49e205a7005c70881f27e44b03a6827bf66a
6
+ metadata.gz: 563abba82ed5b7e1308edcc406dfc842b4f7bfa1c7ae489d886f73a67737261690a8faeec48d0ff7e2458f57ec3142620da88777625362e4b2c874f8bd93873d
7
+ data.tar.gz: bb6450674d14ba9aa4b9e9b35c54ebc740c55903ae0f3b19ead162c6764948c37bb69029e903ad75886dc7de40f1628e81c0af5ba411a7a015ff600d91bdaf7a
data/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  This gem supports the following versions of Ruby and Rails:
13
13
 
14
- * Ruby 2.7.0+
14
+ * Ruby 3.1.0+
15
15
  * Rails 6.0.0+
16
16
 
17
17
  ## Usage
@@ -123,6 +123,124 @@ This gem supports the following versions of Ruby and Rails:
123
123
  end
124
124
  ```
125
125
 
126
+ ## Brotli Splice Slots
127
+
128
+ Applications that need per-request replacement inside cached Brotli HTML responses can pass an injector builder to `ResponseBank::Middleware`:
129
+
130
+ ```ruby
131
+ use ResponseBank::Middleware, ->(env) { HtmlMetadataInjector.new(env) }
132
+ ```
133
+
134
+ Rails applications can configure the same builder through `config.response_bank`:
135
+
136
+ ```ruby
137
+ config.response_bank.brotli_splice_injector =
138
+ ->(env) { HtmlMetadataInjector.new(env) }
139
+ ```
140
+
141
+ The injector is optional. If it is not configured, ResponseBank uses the normal Brotli compression path. Applications own the concrete injector implementation because they know how to read their request-specific metadata.
142
+
143
+ Injectors may include `ResponseBank::BrotliSpliceInjector` to document the required methods:
144
+
145
+ ```ruby
146
+ class HtmlMetadataInjector
147
+ include ResponseBank::BrotliSpliceInjector
148
+
149
+ # The per-request value spliced in on cache hits (a 36-byte UUID).
150
+ TOKEN_PLACEHOLDER = "00000000-0000-0000-0000-000000000000"
151
+ # BrotliSplice reserves the LAST 2 bytes of a slot as a fixed "\r\n" context
152
+ # suffix, so a slot must span 2 more bytes than its replaceable region and
153
+ # `replacement_length` always comes back as `slot length - 2`. We let those
154
+ # 2 bytes be a real "\r\n" placed after the tag, where a line break is
155
+ # harmless — the replaceable region is therefore `<uuid>">` (38 bytes).
156
+ CONTEXT_SUFFIX = "\r\n"
157
+ PLACEHOLDER_TAG = %(<meta name="shopify-y" content="#{TOKEN_PLACEHOLDER}">#{CONTEXT_SUFFIX})
158
+ SLOT = %(#{TOKEN_PLACEHOLDER}">#{CONTEXT_SUFFIX}) # 38 replaceable bytes + 2 reserved
159
+
160
+ def initialize(env)
161
+ @env = env
162
+ end
163
+
164
+ def prepare_response_bank_brotli_splice(body, _headers)
165
+ body_with_placeholder = body.sub("</head>", "#{PLACEHOLDER_TAG}</head>")
166
+ # Use a byte offset: BrotliSplice.encode addresses the slot by bytes.
167
+ offset = body_with_placeholder.b.index(TOKEN_PLACEHOLDER)
168
+ return unless offset
169
+
170
+ {
171
+ body: body_with_placeholder,
172
+ slots: [
173
+ {
174
+ name: "shopify_y",
175
+ offset: offset,
176
+ # Include the 2 bytes reserved for the context suffix, or the
177
+ # replacement below will be 2 bytes too long and silently dropped.
178
+ length: SLOT.bytesize,
179
+ },
180
+ ],
181
+ }
182
+ end
183
+
184
+ def response_bank_brotli_splice_replacement(slot)
185
+ # Must return EXACTLY slot["replacement_length"] bytes (== slot length - 2).
186
+ # If it does not, ResponseBank skips the splice and serves the neutral
187
+ # placeholder, so guard the length rather than assuming it.
188
+ replacement = %(#{shopify_y}">)
189
+ return unless replacement.bytesize == slot.fetch("replacement_length")
190
+
191
+ replacement
192
+ end
193
+
194
+ def replace_response_bank_brotli_splice_placeholders(body, slots)
195
+ slots.reduce(body) do |current, slot|
196
+ replacement = response_bank_brotli_splice_replacement(slot)
197
+ next current unless replacement
198
+
199
+ offset = slot.fetch("html_placeholder_offset")
200
+ length = slot.fetch("html_placeholder_length")
201
+ suffix = slot.fetch("context_suffix", CONTEXT_SUFFIX)
202
+
203
+ # replacement + suffix must equal the original slot length (byteslice
204
+ # replaces `length` bytes), keeping the body byte-for-byte consistent.
205
+ current.byteslice(0, offset) + replacement + suffix +
206
+ current.byteslice(offset + length, current.bytesize)
207
+ end
208
+ end
209
+
210
+ private
211
+
212
+ def shopify_y
213
+ @env.fetch("HTTP_SHOPIFY_Y") # 36-byte UUID
214
+ end
215
+ end
216
+ ```
217
+
218
+ `prepare_response_bank_brotli_splice` is used on cache writes. It returns HTML
219
+ containing a neutral placeholder and one slot describing that placeholder.
220
+ ResponseBank stores the slot metadata with the cached Brotli body. The slot's
221
+ `offset` and `length` are **byte** offsets/counts — `BrotliSplice.encode`
222
+ addresses the stream by bytes — so locate the placeholder on a binary view
223
+ (`body.b.index(...)`) and size it with `bytesize`. A plain `String#index`/`size`
224
+ silently breaks once any multi-byte UTF-8 precedes the placeholder: a character
225
+ offset is always ≤ the byte length, so the bounds check still passes.
226
+
227
+ `response_bank_brotli_splice_replacement` is used on Brotli cache hits. It must
228
+ return exactly `slot["replacement_length"]` bytes. Note that `replacement_length`
229
+ is **2 fewer** than the `length` you registered in the slot: `BrotliSplice.encode`
230
+ reserves the last 2 bytes of every slot as a fixed `\r\n` context suffix. So size
231
+ your placeholder to include those 2 bytes (as the example does with the trailing
232
+ `\r\n`), and have the replacement match `replacement_length`. If the byte length
233
+ does not match, ResponseBank **silently skips the splice and serves the neutral
234
+ placeholder** — no exception is raised — so guard the length instead of assuming it.
235
+
236
+ `replace_response_bank_brotli_splice_placeholders` is used when a cached Brotli response is decompressed for a client that does not accept Brotli.
237
+
238
+ Advanced integrations can still install the per-request injector directly in the Rack env before ResponseBank reads or writes the cached body:
239
+
240
+ ```ruby
241
+ env[ResponseBank::BrotliSpliceSlot::INJECTOR_ENV_KEY] = injector
242
+ ```
243
+
126
244
  ## License
127
245
 
128
246
  ResponseBank is released under the [MIT License](LICENSE.txt).
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResponseBank
4
+ module BrotliSpliceInjector
5
+ def prepare_response_bank_brotli_splice(body, headers)
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def response_bank_brotli_splice_replacement(slot)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def replace_response_bank_brotli_splice_placeholders(body, slots)
14
+ raise NotImplementedError
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResponseBank
4
+ module BrotliSpliceSlot
5
+ INJECTOR_ENV_KEY = 'response_bank.html_metadata_injector'
6
+ METADATA_KEY = 'brotli_splice'
7
+ METADATA_VERSION = 1
8
+ # BrotliSplice reserves the last 2 bytes of every slot as a fixed "\r\n" context
9
+ # suffix, so a slot must be longer than that to hold any replaceable content --
10
+ # BrotliSplice.encode raises unless the slot length exceeds it.
11
+ CONTEXT_SUFFIX_LENGTH = 2
12
+
13
+ EncodedBody = Struct.new(:body, :compressed_body, :metadata, keyword_init: true)
14
+
15
+ class << self
16
+ def encode_body(env, body, headers, compression_level:)
17
+ injector = env[INJECTOR_ENV_KEY]
18
+ return unless injector
19
+ return unless body && body != ''
20
+
21
+ prepared = injector.prepare_response_bank_brotli_splice(body, headers)
22
+ return unless prepared
23
+
24
+ prepared_body = hash_fetch(prepared, :body)
25
+ slots = hash_fetch(prepared, :slots)
26
+ return unless prepared_body && slots && slots.length == 1
27
+
28
+ slot = slots.first
29
+ slot_name = hash_fetch(slot, :name).to_s
30
+ html_offset = integer_value(hash_fetch(slot, :offset))
31
+ html_length = integer_value(hash_fetch(slot, :length))
32
+ return unless valid_html_slot?(prepared_body, html_offset, html_length)
33
+
34
+ # Load the native gem only once we have real splice work to do. Keeping this
35
+ # outside the begin/rescue below is deliberate: the rescue clause references
36
+ # BrotliSplice::Error, so if the gem is missing, evaluating that clause would
37
+ # raise NameError and mask the LoadError we want the caller to see.
38
+ ensure_brotli_splice_loaded!
39
+
40
+ begin
41
+ result = BrotliSplice.encode(prepared_body, html_offset, html_length, quality: compression_level)
42
+
43
+ metadata_slot = {
44
+ 'name' => slot_name,
45
+ 'compressed_offset' => result[:secret_offset],
46
+ 'replacement_length' => result[:secret_length],
47
+ 'html_placeholder_offset' => html_offset,
48
+ 'html_placeholder_length' => html_length,
49
+ }
50
+ metadata_slot['context_suffix'] = result[:context_suffix] if result[:context_suffix]
51
+
52
+ EncodedBody.new(
53
+ body: prepared_body,
54
+ compressed_body: result[:data],
55
+ metadata: {
56
+ METADATA_KEY => {
57
+ 'version' => METADATA_VERSION,
58
+ 'slots' => [metadata_slot],
59
+ },
60
+ },
61
+ )
62
+ rescue BrotliSplice::Error, ArgumentError => error
63
+ ResponseBank.log("BrotliSplice encode skipped: #{error.class}")
64
+ nil
65
+ end
66
+ end
67
+
68
+ def replace_compressed_secret(env, body, metadata)
69
+ injector = env[INJECTOR_ENV_KEY]
70
+ slots = metadata_slots(metadata)
71
+ return body unless injector && slots && body && body != ''
72
+
73
+ # See encode_body: load outside the begin/rescue so a missing gem surfaces as
74
+ # LoadError rather than a masked NameError from the BrotliSplice::Error clause.
75
+ ensure_brotli_splice_loaded!
76
+
77
+ begin
78
+ slots.reduce(body) do |current_body, slot|
79
+ replacement = replacement_for_slot(injector, slot)
80
+
81
+ unless valid_replacement?(replacement, slot)
82
+ got = replacement ? replacement.bytesize : 'nil'
83
+ ResponseBank.log(
84
+ 'BrotliSplice replace skipped: replacement length mismatch ' \
85
+ "(got #{got}, want #{slot['replacement_length']})",
86
+ )
87
+ next current_body
88
+ end
89
+
90
+ offset = integer_value(slot['compressed_offset'])
91
+ length = integer_value(slot['replacement_length'])
92
+
93
+ unless valid_compressed_slot?(current_body, offset, length)
94
+ ResponseBank.log('BrotliSplice replace skipped: compressed slot out of bounds')
95
+ next current_body
96
+ end
97
+
98
+ BrotliSplice.replace(current_body, replacement, offset, length)
99
+ end
100
+ rescue BrotliSplice::Error, ArgumentError => error
101
+ ResponseBank.log("BrotliSplice replace skipped: #{error.class}")
102
+ body
103
+ end
104
+ end
105
+
106
+ def replace_plain_body(env, body, metadata)
107
+ injector = env[INJECTOR_ENV_KEY]
108
+ slots = metadata_slots(metadata)
109
+ return body unless injector && slots && body && body != ''
110
+
111
+ injector.replace_response_bank_brotli_splice_placeholders(body, slots)
112
+ rescue ArgumentError => error
113
+ ResponseBank.log("BrotliSplice plain replacement skipped: #{error.class}")
114
+ body
115
+ end
116
+
117
+ def metadata_slots(metadata)
118
+ return unless metadata
119
+
120
+ brotli_splice = metadata[METADATA_KEY]
121
+ return unless brotli_splice && brotli_splice['version'] == METADATA_VERSION
122
+
123
+ brotli_splice['slots']
124
+ end
125
+
126
+ private
127
+
128
+ # Load the native brotli_splice gem on first use. It is an optional dependency:
129
+ # apps opt into Brotli splice slots by installing an injector, and only those
130
+ # apps need the gem. If it is missing when we actually need it, fail loudly with
131
+ # a pointer to the fix rather than limping along.
132
+ def ensure_brotli_splice_loaded!
133
+ return if @brotli_splice_loaded
134
+
135
+ begin
136
+ gem('brotli_splice')
137
+ require('brotli_splice')
138
+ rescue LoadError => error
139
+ warn(
140
+ 'The Brotli splice slot feature requires the "brotli_splice" gem. ' \
141
+ 'Add it to your application Gemfile.',
142
+ )
143
+ raise error
144
+ end
145
+
146
+ @brotli_splice_loaded = true
147
+ end
148
+
149
+ def replacement_for_slot(injector, slot)
150
+ injector.response_bank_brotli_splice_replacement(slot)
151
+ end
152
+
153
+ def valid_replacement?(replacement, slot)
154
+ return false unless replacement
155
+
156
+ replacement.bytesize == slot['replacement_length']
157
+ end
158
+
159
+ def valid_compressed_slot?(body, offset, length)
160
+ return false unless offset && length
161
+ return false unless offset >= 0 && length > 0
162
+
163
+ offset + length <= body.bytesize
164
+ end
165
+
166
+ def valid_html_slot?(body, offset, length)
167
+ return false unless offset && length
168
+ return false unless offset >= 0 && length > CONTEXT_SUFFIX_LENGTH
169
+
170
+ offset + length <= body.bytesize
171
+ end
172
+
173
+ def integer_value(value)
174
+ Integer(value)
175
+ rescue ArgumentError, TypeError
176
+ nil
177
+ end
178
+
179
+ def hash_fetch(hash, key)
180
+ hash[key] || hash[key.to_s]
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'response_bank/brotli_splice_slot'
2
3
 
3
4
  module ResponseBank
4
5
  class Middleware
@@ -10,12 +11,15 @@ module ResponseBank
10
11
  ACCEPT = "HTTP_ACCEPT"
11
12
  USER_AGENT = "HTTP_USER_AGENT"
12
13
 
13
- def initialize(app)
14
+ def initialize(app, brotli_splice_injector = nil)
14
15
  @app = app
16
+ @brotli_splice_injector = brotli_splice_injector
15
17
  end
16
18
 
17
19
  def call(env)
18
20
  env['cacheable.cache'] = false
21
+ install_brotli_splice_injector(env)
22
+
19
23
  content_encoding = env['response_bank.server_cache_encoding'] = ResponseBank.check_encoding(env)
20
24
 
21
25
  status, headers, body = @app.call(env)
@@ -35,15 +39,31 @@ module ResponseBank
35
39
  end
36
40
 
37
41
  body_compressed = nil
42
+ metadata = nil
38
43
  if body_string && body_string != ""
39
44
  headers['Content-Encoding'] = content_encoding
40
45
  env["cacheable.compression_level"] = ResponseBank.compression_level_for_request(env, headers)
41
46
  time = ResponseBank.measure do
42
- body_compressed = ResponseBank.compress(
43
- body_string,
44
- content_encoding,
45
- compression_level: env["cacheable.compression_level"],
46
- )
47
+ encoded_body = if content_encoding == 'br'
48
+ ResponseBank::BrotliSpliceSlot.encode_body(
49
+ env,
50
+ body_string,
51
+ headers,
52
+ compression_level: env["cacheable.compression_level"],
53
+ )
54
+ end
55
+
56
+ if encoded_body
57
+ body_string = encoded_body.body
58
+ body_compressed = encoded_body.compressed_body
59
+ metadata = encoded_body.metadata
60
+ else
61
+ body_compressed = ResponseBank.compress(
62
+ body_string,
63
+ content_encoding,
64
+ compression_level: env["cacheable.compression_level"],
65
+ )
66
+ end
47
67
  end
48
68
  ResponseBank.log("Compression time: #{time}ms")
49
69
  env["cacheable.compression_time"] = time
@@ -52,6 +72,7 @@ module ResponseBank
52
72
  cached_headers = headers.slice(*CACHEABLE_HEADERS)
53
73
  # Store result
54
74
  cache_data = [status, cached_headers, body_compressed, timestamp, env["cacheable.compression_level"]]
75
+ cache_data << metadata if metadata
55
76
 
56
77
  ResponseBank.write_to_cache(env['cacheable.key']) do
57
78
  payload = MessagePack.dump(cache_data)
@@ -67,10 +88,15 @@ module ResponseBank
67
88
  # as well serve it if the client wants it
68
89
  if body_compressed
69
90
  if env['HTTP_ACCEPT_ENCODING'].to_s.include?(content_encoding)
70
- body = [body_compressed]
91
+ if content_encoding == 'br'
92
+ body = [ResponseBank::BrotliSpliceSlot.replace_compressed_secret(env, body_compressed, metadata)]
93
+ else
94
+ body = [body_compressed]
95
+ end
71
96
  else
72
97
  # Remove content-encoding header for response with compressed content
73
98
  headers.delete('Content-Encoding')
99
+ body = [ResponseBank::BrotliSpliceSlot.replace_plain_body(env, body_string, metadata)] if metadata
74
100
  end
75
101
  end
76
102
  end
@@ -87,6 +113,19 @@ module ResponseBank
87
113
 
88
114
  private
89
115
 
116
+ def install_brotli_splice_injector(env)
117
+ return unless @brotli_splice_injector
118
+ return if env.key?(ResponseBank::BrotliSpliceSlot::INJECTOR_ENV_KEY)
119
+
120
+ injector = if @brotli_splice_injector.respond_to?(:call)
121
+ @brotli_splice_injector.call(env)
122
+ else
123
+ @brotli_splice_injector
124
+ end
125
+
126
+ env[ResponseBank::BrotliSpliceSlot::INJECTOR_ENV_KEY] = injector if injector
127
+ end
128
+
90
129
  def timestamp
91
130
  Time.now.to_i
92
131
  end
@@ -1,12 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
  require 'rails'
3
+ require 'active_support/ordered_options'
3
4
  require 'response_bank/controller'
4
5
  require 'response_bank/model_extensions'
5
6
 
6
7
  module ResponseBank
7
8
  class Railtie < ::Rails::Railtie
8
- initializer "cachable.configure_active_record" do |config|
9
- config.middleware.insert_after(Rack::Head, ResponseBank::Middleware)
9
+ config.response_bank = ActiveSupport::OrderedOptions.new
10
+ config.response_bank.brotli_splice_injector = nil
11
+
12
+ initializer "cachable.configure_active_record" do |app|
13
+ app.config.middleware.insert_after(
14
+ Rack::Head,
15
+ ResponseBank::Middleware,
16
+ app.config.response_bank.brotli_splice_injector,
17
+ )
10
18
 
11
19
  ActiveSupport.on_load(:action_controller) do
12
20
  include ResponseBank::Controller
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'digest/md5'
3
+ require 'response_bank/brotli_splice_slot'
3
4
 
4
5
  module ResponseBank
5
6
  class ResponseCacheHandler
@@ -119,7 +120,7 @@ module ResponseBank
119
120
  @env['cacheable.miss'] = false
120
121
  @env['cacheable.store'] = 'server'
121
122
 
122
- status, headers, body, timestamp, compression_level = hit
123
+ status, headers, body, timestamp, compression_level, metadata = hit
123
124
 
124
125
  @env['cacheable.compression_level'] = compression_level
125
126
 
@@ -152,9 +153,14 @@ module ResponseBank
152
153
  @headers.merge!(headers)
153
154
 
154
155
  if @headers['Content-Encoding']
155
- if !@env['HTTP_ACCEPT_ENCODING'].to_s.include?(@headers['Content-Encoding'])
156
+ if @env['HTTP_ACCEPT_ENCODING'].to_s.include?(@headers['Content-Encoding'])
157
+ if @headers['Content-Encoding'] == 'br'
158
+ body = ResponseBank::BrotliSpliceSlot.replace_compressed_secret(@env, body, metadata)
159
+ end
160
+ else
156
161
  ResponseBank.log("uncompressing payload for client as client doesn't require encoding")
157
162
  body = ResponseBank.decompress(body, @headers['Content-Encoding'])
163
+ body = ResponseBank::BrotliSpliceSlot.replace_plain_body(@env, body, metadata)
158
164
  @headers.delete('Content-Encoding')
159
165
  end
160
166
  else
@@ -180,9 +186,9 @@ module ResponseBank
180
186
 
181
187
  # strictly speaking an unquoted etag is not valid, yet common
182
188
  # to avoid unintended greedy matches in we check for naked entity then includes with quoted entity values
183
- entity_tag = %{"#{entity_tag}"} unless entity_tag.starts_with?('"')
189
+ entity_tag = %{"#{entity_tag}"} unless entity_tag.start_with?('"')
184
190
 
185
- if_none_match = %{"#{if_none_match}"} unless if_none_match.starts_with?('"') || if_none_match.starts_with?('W/"')
191
+ if_none_match = %{"#{if_none_match}"} unless if_none_match.start_with?('"') || if_none_match.start_with?('W/"')
186
192
 
187
193
  if_none_match == entity_tag || if_none_match.include?(entity_tag)
188
194
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ResponseBank
3
- VERSION = "1.3.7"
3
+ VERSION = "1.3.8"
4
4
  end
data/lib/response_bank.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'response_bank/brotli_splice_injector'
3
+ require 'response_bank/brotli_splice_slot'
2
4
  require 'response_bank/middleware'
3
5
  require 'response_bank/railtie' if defined?(Rails)
4
6
  require 'response_bank/response_cache_handler'
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.3.7
4
+ version: 1.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: brotli_splice
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.1
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: minitest
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -118,6 +132,8 @@ files:
118
132
  - LICENSE.txt
119
133
  - README.md
120
134
  - lib/response_bank.rb
135
+ - lib/response_bank/brotli_splice_injector.rb
136
+ - lib/response_bank/brotli_splice_slot.rb
121
137
  - lib/response_bank/controller.rb
122
138
  - lib/response_bank/middleware.rb
123
139
  - lib/response_bank/model_extensions.rb
@@ -136,14 +152,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
136
152
  requirements:
137
153
  - - ">="
138
154
  - !ruby/object:Gem::Version
139
- version: 2.7.0
155
+ version: 3.1.0
140
156
  required_rubygems_version: !ruby/object:Gem::Requirement
141
157
  requirements:
142
158
  - - ">="
143
159
  - !ruby/object:Gem::Version
144
160
  version: '0'
145
161
  requirements: []
146
- rubygems_version: 3.7.2
162
+ rubygems_version: 4.0.14
147
163
  specification_version: 4
148
164
  summary: Simple response caching for Ruby applications
149
165
  test_files: []