valkey-rb 0.3.5

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +43 -0
  3. data/.rubocop_todo.yml +22 -0
  4. data/README.md +34 -0
  5. data/Rakefile +23 -0
  6. data/lib/valkey/bindings.rb +173 -0
  7. data/lib/valkey/commands/bitmap_commands.rb +86 -0
  8. data/lib/valkey/commands/cluster_commands.rb +259 -0
  9. data/lib/valkey/commands/connection_commands.rb +318 -0
  10. data/lib/valkey/commands/function_commands.rb +255 -0
  11. data/lib/valkey/commands/generic_commands.rb +454 -0
  12. data/lib/valkey/commands/geo_commands.rb +87 -0
  13. data/lib/valkey/commands/hash_commands.rb +586 -0
  14. data/lib/valkey/commands/hyper_log_log_commands.rb +51 -0
  15. data/lib/valkey/commands/json_commands.rb +389 -0
  16. data/lib/valkey/commands/list_commands.rb +348 -0
  17. data/lib/valkey/commands/module_commands.rb +125 -0
  18. data/lib/valkey/commands/pubsub_commands.rb +237 -0
  19. data/lib/valkey/commands/scripting_commands.rb +217 -0
  20. data/lib/valkey/commands/server_commands.rb +961 -0
  21. data/lib/valkey/commands/set_commands.rb +220 -0
  22. data/lib/valkey/commands/sorted_set_commands.rb +756 -0
  23. data/lib/valkey/commands/stream_commands.rb +636 -0
  24. data/lib/valkey/commands/string_commands.rb +359 -0
  25. data/lib/valkey/commands/transaction_commands.rb +175 -0
  26. data/lib/valkey/commands/vector_search_commands.rb +271 -0
  27. data/lib/valkey/commands.rb +69 -0
  28. data/lib/valkey/errors.rb +41 -0
  29. data/lib/valkey/libglide_ffi.dylib +0 -0
  30. data/lib/valkey/libglide_ffi.so +0 -0
  31. data/lib/valkey/pipeline.rb +20 -0
  32. data/lib/valkey/protobuf/command_request_pb.rb +30 -0
  33. data/lib/valkey/protobuf/connection_request_pb.rb +28 -0
  34. data/lib/valkey/protobuf/response_pb.rb +18 -0
  35. data/lib/valkey/pubsub_callback.rb +10 -0
  36. data/lib/valkey/request_error_type.rb +10 -0
  37. data/lib/valkey/request_type.rb +436 -0
  38. data/lib/valkey/response_type.rb +20 -0
  39. data/lib/valkey/utils.rb +253 -0
  40. data/lib/valkey/version.rb +5 -0
  41. data/lib/valkey.rb +477 -0
  42. metadata +119 -0
data/lib/valkey.rb ADDED
@@ -0,0 +1,477 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require "google/protobuf"
5
+
6
+ require "valkey/version"
7
+ require "valkey/request_type"
8
+ require "valkey/response_type"
9
+ require "valkey/request_error_type"
10
+ require "valkey/protobuf/command_request_pb"
11
+ require "valkey/protobuf/connection_request_pb"
12
+ require "valkey/protobuf/response_pb"
13
+ require "valkey/bindings"
14
+ require "valkey/utils"
15
+ require "valkey/commands"
16
+ require "valkey/errors"
17
+ require "valkey/pubsub_callback"
18
+ require "valkey/pipeline"
19
+
20
+ class Valkey
21
+ include Utils
22
+ include Commands
23
+ include PubSubCallback
24
+
25
+ def pipelined(exception: true)
26
+ # Redis-rb v5 and earlier behavior: commands called on the original client
27
+ # inside a pipelined block are automatically pipelined
28
+ original_pipeline = @current_pipeline
29
+ @current_pipeline = Pipeline.new
30
+
31
+ yield @current_pipeline
32
+
33
+ commands = @current_pipeline.commands
34
+ @current_pipeline = original_pipeline
35
+
36
+ return [] if commands.empty?
37
+
38
+ send_batch_commands(commands, exception: exception)
39
+ end
40
+
41
+ def send_batch_commands(commands, exception: true)
42
+ # WORKAROUND: The underlying Glide FFI backend has stability issues when
43
+ # batching transactional commands like MULTI / EXEC / DISCARD. To avoid
44
+ # native crashes we fall back to issuing those commands sequentially
45
+ # instead of via `Bindings.batch`.
46
+ tx_types = [RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD]
47
+
48
+ if commands.any? { |(command_type, _args, _block)| tx_types.include?(command_type) }
49
+ results = []
50
+
51
+ commands.each do |command_type, command_args, block|
52
+ res = send_command(command_type, command_args)
53
+ res = block.call(res) if block
54
+ results << res
55
+ end
56
+
57
+ return results
58
+ end
59
+
60
+ cmds = []
61
+ blocks = []
62
+ buffers = [] # Keep references to prevent GC
63
+
64
+ commands.each do |command_type, command_args, block|
65
+ arg_ptrs, arg_lens = build_command_args(command_args)
66
+
67
+ cmd = Bindings::CmdInfo.new
68
+ cmd[:request_type] = command_type
69
+ cmd[:args] = arg_ptrs
70
+ cmd[:arg_count] = command_args.size
71
+ cmd[:args_len] = arg_lens
72
+
73
+ cmds << cmd
74
+ blocks << block
75
+ buffers << [arg_ptrs, arg_lens] # Prevent GC
76
+ end
77
+
78
+ # Create array of pointers to CmdInfo structs
79
+ cmd_ptrs = FFI::MemoryPointer.new(:pointer, cmds.size)
80
+ cmds.each_with_index do |cmd, i|
81
+ cmd_ptrs[i].put_pointer(0, cmd.to_ptr)
82
+ end
83
+
84
+ batch_info = Bindings::BatchInfo.new
85
+ batch_info[:cmd_count] = cmds.size
86
+ batch_info[:cmds] = cmd_ptrs
87
+ batch_info[:is_atomic] = false
88
+
89
+ batch_options = Bindings::BatchOptionsInfo.new
90
+ batch_options[:retry_server_error] = true
91
+ batch_options[:retry_connection_error] = true
92
+ batch_options[:has_timeout] = false
93
+ batch_options[:timeout] = 0 # No timeout
94
+ batch_options[:route_info] = FFI::Pointer::NULL
95
+
96
+ res = Bindings.batch(
97
+ @connection,
98
+ 0,
99
+ batch_info,
100
+ exception,
101
+ batch_options.to_ptr,
102
+ 0
103
+ )
104
+
105
+ results = convert_response(res)
106
+
107
+ blocks.each_with_index do |block, i|
108
+ results[i] = block.call(results[i]) if block
109
+ end
110
+
111
+ results
112
+ end
113
+
114
+ def build_command_args(command_args)
115
+ arg_ptrs = FFI::MemoryPointer.new(:pointer, command_args.size)
116
+ arg_lens = FFI::MemoryPointer.new(:ulong, command_args.size)
117
+ buffers = []
118
+
119
+ command_args.each_with_index do |arg, i|
120
+ arg = arg.to_s # Ensure we convert to string
121
+
122
+ buf = FFI::MemoryPointer.from_string(arg.to_s)
123
+ buffers << buf # prevent garbage collection
124
+ arg_ptrs.put_pointer(i * FFI::Pointer.size, buf)
125
+ arg_lens.put_ulong(i * 8, arg.bytesize)
126
+ end
127
+
128
+ [arg_ptrs, arg_lens]
129
+ end
130
+
131
+ def convert_response(res, &block)
132
+ result = Bindings::CommandResult.new(res)
133
+
134
+ if result[:response].null?
135
+ error = result[:command_error]
136
+
137
+ case error[:command_error_type]
138
+ when RequestErrorType::EXECABORT, RequestErrorType::UNSPECIFIED
139
+ raise CommandError, error[:command_error_message]
140
+ when RequestErrorType::TIMEOUT
141
+ raise TimeoutError, error[:command_error_message]
142
+ when RequestErrorType::DISCONNECT
143
+ raise ConnectionError, error[:command_error_message]
144
+ else
145
+ raise "Unknown error type: #{error[:command_error_type]}"
146
+ end
147
+ end
148
+
149
+ result = result[:response]
150
+
151
+ convert_response = lambda { |response_item|
152
+ # TODO: handle all types of responses
153
+ case response_item[:response_type]
154
+ when ResponseType::STRING
155
+ response_item[:string_value].read_string(response_item[:string_value_len])
156
+ when ResponseType::INT
157
+ response_item[:int_value]
158
+ when ResponseType::FLOAT
159
+ response_item[:float_value]
160
+ when ResponseType::BOOL
161
+ response_item[:bool_value]
162
+ when ResponseType::ARRAY
163
+ ptr = response_item[:array_value]
164
+ count = response_item[:array_value_len].to_i
165
+
166
+ Array.new(count) do |i|
167
+ item = Bindings::CommandResponse.new(ptr + i * Bindings::CommandResponse.size)
168
+ convert_response.call(item)
169
+ end
170
+ when ResponseType::MAP
171
+ return nil if response_item[:array_value].null?
172
+
173
+ ptr = response_item[:array_value]
174
+ count = response_item[:array_value_len].to_i
175
+ map = {}
176
+
177
+ Array.new(count) do |i|
178
+ item = Bindings::CommandResponse.new(ptr + i * Bindings::CommandResponse.size)
179
+
180
+ map_key = convert_response.call(Bindings::CommandResponse.new(item[:map_key]))
181
+ map_value = convert_response.call(Bindings::CommandResponse.new(item[:map_value]))
182
+
183
+ map[map_key] = map_value
184
+ end
185
+
186
+ # technically it has to return a Hash, but as of now we return just one pair
187
+ map.to_a.flatten(1) # Flatten to get pairs
188
+ when ResponseType::SETS
189
+ ptr = response_item[:sets_value]
190
+ count = response_item[:sets_value_len].to_i
191
+
192
+ Array.new(count) do |i|
193
+ item = Bindings::CommandResponse.new(ptr + i * Bindings::CommandResponse.size)
194
+ convert_response.call(item)
195
+ end
196
+ when ResponseType::NULL
197
+ nil
198
+ when ResponseType::OK
199
+ "OK"
200
+ when ResponseType::ERROR
201
+ # For errors in arrays (like EXEC responses), return an error object
202
+ # instead of raising. The error message is typically in string_value.
203
+ error_msg = if response_item[:string_value].null?
204
+ "Unknown error"
205
+ else
206
+ response_item[:string_value].read_string(response_item[:string_value_len])
207
+ end
208
+ CommandError.new(error_msg)
209
+ else
210
+ raise "Unsupported response type: #{response_item[:response_type]}"
211
+ end
212
+ }
213
+
214
+ response = convert_response.call(result)
215
+
216
+ if block_given?
217
+ block.call(response)
218
+ else
219
+ response
220
+ end
221
+ end
222
+
223
+ def send_command(command_type, command_args = [], &block)
224
+ # Redis-rb v5 and earlier behavior: if we're inside a pipelined block,
225
+ # commands on the client are automatically added to the pipeline
226
+ if @current_pipeline
227
+ @current_pipeline.send_command(command_type, command_args, &block)
228
+ return
229
+ end
230
+
231
+ # Validate connection
232
+ if @connection.nil?
233
+ raise "Connection is nil"
234
+ elsif @connection.null?
235
+ raise "Connection pointer is null"
236
+ elsif @connection.address.zero?
237
+ raise "Connection address is 0"
238
+ end
239
+
240
+ channel = 0
241
+ route = ""
242
+
243
+ route_buf = FFI::MemoryPointer.from_string(route)
244
+
245
+ # Handle empty command_args case
246
+ if command_args.empty?
247
+ arg_ptrs = FFI::MemoryPointer.new(:pointer, 1)
248
+ arg_lens = FFI::MemoryPointer.new(:ulong, 1)
249
+ arg_ptrs.put_pointer(0, FFI::MemoryPointer.new(1))
250
+ arg_lens.put_ulong(0, 0)
251
+ else
252
+ arg_ptrs, arg_lens = build_command_args(command_args)
253
+ end
254
+
255
+ res = Bindings.command(
256
+ @connection,
257
+ channel,
258
+ command_type,
259
+ command_args.size,
260
+ arg_ptrs,
261
+ arg_lens,
262
+ route_buf,
263
+ route.bytesize,
264
+ 0
265
+ )
266
+
267
+ result = convert_response(res, &block)
268
+
269
+ # Track queued commands during MULTI (except for MULTI, EXEC, DISCARD, WATCH, UNWATCH)
270
+ if @in_multi && !@queued_commands.nil?
271
+ tx_commands = [
272
+ RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD,
273
+ RequestType::WATCH, RequestType::UNWATCH
274
+ ]
275
+ @queued_commands << [command_type, command_args.dup] if !tx_commands.include?(command_type) && result == "QUEUED"
276
+ end
277
+
278
+ result
279
+ end
280
+
281
+ def initialize(options = {})
282
+ # Parse URL if provided
283
+ if options[:url]
284
+ url_options = Utils.parse_redis_url(options[:url])
285
+ # Merge URL options, but explicit options take precedence
286
+ options = url_options.merge(options.reject { |k, _v| k == :url })
287
+ end
288
+
289
+ # Extract connection parameters
290
+ host = options[:host] || "127.0.0.1"
291
+ port = options[:port] || 6379
292
+
293
+ nodes = options[:nodes] || [{ host: host, port: port }]
294
+
295
+ cluster_mode_enabled = options[:cluster_mode] || false
296
+
297
+ # Protocol defaults to RESP2
298
+ protocol = case options[:protocol]
299
+ when :resp3, "resp3", 3
300
+ ConnectionRequest::ProtocolVersion::RESP3
301
+ else
302
+ ConnectionRequest::ProtocolVersion::RESP2
303
+ end
304
+
305
+ # TLS/SSL support
306
+ tls_mode = if [true, "true"].include?(options[:ssl])
307
+ ConnectionRequest::TlsMode::SecureTls
308
+ else
309
+ ConnectionRequest::TlsMode::NoTls
310
+ end
311
+
312
+ # SSL parameters - map ssl_params to protobuf root_certs
313
+ root_certs = []
314
+ if options[:ssl_params].is_a?(Hash)
315
+ # ca_file - read CA certificate file (PEM or DER format)
316
+ root_certs << File.binread(options[:ssl_params][:ca_file]) if options[:ssl_params][:ca_file]
317
+
318
+ # cert - client certificate (file path or OpenSSL::X509::Certificate)
319
+ if options[:ssl_params][:cert]
320
+ cert_data = if options[:ssl_params][:cert].is_a?(String)
321
+ File.binread(options[:ssl_params][:cert])
322
+ elsif options[:ssl_params][:cert].respond_to?(:to_pem)
323
+ options[:ssl_params][:cert].to_pem
324
+ elsif options[:ssl_params][:cert].respond_to?(:to_der)
325
+ options[:ssl_params][:cert].to_der
326
+ else
327
+ options[:ssl_params][:cert].to_s
328
+ end
329
+ root_certs << cert_data
330
+ end
331
+
332
+ # key - client key (file path or OpenSSL::PKey)
333
+ if options[:ssl_params][:key]
334
+ key_data = if options[:ssl_params][:key].is_a?(String)
335
+ File.binread(options[:ssl_params][:key])
336
+ elsif options[:ssl_params][:key].respond_to?(:to_pem)
337
+ options[:ssl_params][:key].to_pem
338
+ elsif options[:ssl_params][:key].respond_to?(:to_der)
339
+ options[:ssl_params][:key].to_der
340
+ else
341
+ options[:ssl_params][:key].to_s
342
+ end
343
+ root_certs << key_data
344
+ end
345
+
346
+ # Additional root certificates from ca_path
347
+ if options[:ssl_params][:ca_path]
348
+ Dir.glob(File.join(options[:ssl_params][:ca_path], "*.crt")).each do |cert_file|
349
+ root_certs << File.binread(cert_file)
350
+ end
351
+ Dir.glob(File.join(options[:ssl_params][:ca_path], "*.pem")).each do |cert_file|
352
+ root_certs << File.binread(cert_file)
353
+ end
354
+ end
355
+
356
+ # Direct root_certs array support
357
+ root_certs.concat(options[:ssl_params][:root_certs]) if options[:ssl_params][:root_certs].is_a?(Array)
358
+ end
359
+
360
+ # Authentication support
361
+ authentication_info = nil
362
+ if options[:password] || options[:username]
363
+ authentication_info = ConnectionRequest::AuthenticationInfo.new(
364
+ password: options[:password] || "",
365
+ username: options[:username] || ""
366
+ )
367
+ end
368
+
369
+ # Database selection
370
+ database_id = options[:db] || 0
371
+
372
+ # Client name
373
+ client_name = options[:client_name] || ""
374
+
375
+ # Timeout handling
376
+ # :timeout sets the request timeout (for command execution)
377
+ # :connect_timeout sets the connection establishment timeout
378
+ # Default request timeout is 5.0 seconds
379
+ request_timeout = options[:timeout] || 5.0
380
+
381
+ # Connection timeout (milliseconds) - defaults to 0 (uses system default)
382
+ connection_timeout_ms = if options[:connect_timeout]
383
+ (options[:connect_timeout] * 1000).to_i
384
+ else
385
+ 0
386
+ end
387
+
388
+ # Connection retry strategy
389
+ connection_retry_strategy = nil
390
+ if options[:reconnect_attempts] || options[:reconnect_delay] || options[:reconnect_delay_max]
391
+ number_of_retries = options[:reconnect_attempts] || 1
392
+ base_delay = options[:reconnect_delay] || 0.5
393
+ max_delay = options[:reconnect_delay_max]
394
+ exponent_base = 2
395
+ jitter_percent = 0
396
+
397
+ if max_delay && base_delay.positive? && number_of_retries.positive?
398
+ calculated_base = (max_delay / base_delay)**(1.0 / number_of_retries.to_f)
399
+ exponent_base = [calculated_base.round, 2].max
400
+ end
401
+
402
+ factor_ms = (base_delay * 1000).to_i
403
+
404
+ connection_retry_strategy = ConnectionRequest::ConnectionRetryStrategy.new(
405
+ number_of_retries: number_of_retries,
406
+ factor: factor_ms,
407
+ exponent_base: exponent_base,
408
+ jitter_percent: jitter_percent
409
+ )
410
+ end
411
+
412
+ # Build connection request
413
+ request_params = {
414
+ cluster_mode_enabled: cluster_mode_enabled,
415
+ request_timeout: request_timeout,
416
+ protocol: protocol,
417
+ tls_mode: tls_mode,
418
+ addresses: nodes.map { |node| ConnectionRequest::NodeAddress.new(host: node[:host], port: node[:port]) }
419
+ }
420
+
421
+ # Add optional fields only if they have values
422
+ request_params[:connection_timeout] = connection_timeout_ms if connection_timeout_ms.positive?
423
+ request_params[:database_id] = database_id if database_id.positive?
424
+ request_params[:client_name] = client_name unless client_name.empty?
425
+ request_params[:authentication_info] = authentication_info if authentication_info
426
+ request_params[:root_certs] = root_certs unless root_certs.empty?
427
+ request_params[:connection_retry_strategy] = connection_retry_strategy if connection_retry_strategy
428
+
429
+ request = ConnectionRequest::ConnectionRequest.new(request_params)
430
+
431
+ client_type = Bindings::ClientType.new
432
+ client_type[:tag] = 1 # SyncClient
433
+
434
+ request_str = ConnectionRequest::ConnectionRequest.encode(request)
435
+ request_buf = FFI::MemoryPointer.new(:char, request_str.bytesize)
436
+ request_buf.put_bytes(0, request_str)
437
+
438
+ request_len = request_str.bytesize
439
+
440
+ response_ptr = Bindings.create_client(
441
+ request_buf,
442
+ request_len,
443
+ client_type,
444
+ method(:pubsub_callback)
445
+ )
446
+
447
+ res = Bindings::ConnectionResponse.new(response_ptr)
448
+
449
+ # Check if connection was successful
450
+ if res[:conn_ptr].null?
451
+ error_message = res[:connection_error_message]
452
+ raise CannotConnectError, "Failed to connect to cluster: #{error_message}"
453
+ end
454
+
455
+ @connection = res[:conn_ptr]
456
+
457
+ # Track transactional state for `MULTI` / `EXEC` / `DISCARD` helpers.
458
+ # This avoids Ruby warnings about uninitialised instance variables and
459
+ # gives us a single source of truth for whether we're inside a TX.
460
+ @in_multi = false
461
+ # Track queued commands during MULTI for transaction isolation support
462
+ @queued_commands = []
463
+ # Track if we're inside a multi block (multi { ... }) vs direct multi calls
464
+ @in_multi_block = false
465
+ # Track current pipeline for redis-rb v5 compatibility (commands on client auto-pipeline)
466
+ @current_pipeline = nil
467
+ end
468
+
469
+ def close
470
+ return if @connection.nil? || @connection.null?
471
+
472
+ Bindings.close_client(@connection)
473
+ @connection = nil
474
+ end
475
+
476
+ alias disconnect! close
477
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: valkey-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.5
5
+ platform: ruby
6
+ authors:
7
+ - Valkey GLIDE Maintainers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.17.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.17.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: google-protobuf
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.23'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.23.4
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '3.23'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.23.4
47
+ description: A Ruby client library for Valkey
48
+ email:
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - ".rubocop.yml"
54
+ - ".rubocop_todo.yml"
55
+ - README.md
56
+ - Rakefile
57
+ - lib/valkey.rb
58
+ - lib/valkey/bindings.rb
59
+ - lib/valkey/commands.rb
60
+ - lib/valkey/commands/bitmap_commands.rb
61
+ - lib/valkey/commands/cluster_commands.rb
62
+ - lib/valkey/commands/connection_commands.rb
63
+ - lib/valkey/commands/function_commands.rb
64
+ - lib/valkey/commands/generic_commands.rb
65
+ - lib/valkey/commands/geo_commands.rb
66
+ - lib/valkey/commands/hash_commands.rb
67
+ - lib/valkey/commands/hyper_log_log_commands.rb
68
+ - lib/valkey/commands/json_commands.rb
69
+ - lib/valkey/commands/list_commands.rb
70
+ - lib/valkey/commands/module_commands.rb
71
+ - lib/valkey/commands/pubsub_commands.rb
72
+ - lib/valkey/commands/scripting_commands.rb
73
+ - lib/valkey/commands/server_commands.rb
74
+ - lib/valkey/commands/set_commands.rb
75
+ - lib/valkey/commands/sorted_set_commands.rb
76
+ - lib/valkey/commands/stream_commands.rb
77
+ - lib/valkey/commands/string_commands.rb
78
+ - lib/valkey/commands/transaction_commands.rb
79
+ - lib/valkey/commands/vector_search_commands.rb
80
+ - lib/valkey/errors.rb
81
+ - lib/valkey/libglide_ffi.dylib
82
+ - lib/valkey/libglide_ffi.so
83
+ - lib/valkey/pipeline.rb
84
+ - lib/valkey/protobuf/command_request_pb.rb
85
+ - lib/valkey/protobuf/connection_request_pb.rb
86
+ - lib/valkey/protobuf/response_pb.rb
87
+ - lib/valkey/pubsub_callback.rb
88
+ - lib/valkey/request_error_type.rb
89
+ - lib/valkey/request_type.rb
90
+ - lib/valkey/response_type.rb
91
+ - lib/valkey/utils.rb
92
+ - lib/valkey/version.rb
93
+ homepage: https://github.com/valkey-io/valkey-glide-ruby
94
+ licenses: []
95
+ metadata:
96
+ homepage_uri: https://github.com/valkey-io/valkey-glide-ruby
97
+ source_code_uri: https://github.com/valkey-io/valkey-glide-ruby
98
+ changelog_uri: https://github.com/valkey-io/valkey-glide-ruby
99
+ rubygems_mfa_required: 'true'
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 2.6.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.4.19
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: A Ruby client library for Valkey
119
+ test_files: []