valkey-glide-rb 0.9.2 → 0.9.3.pre.rc1

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: 17396d39342cbcc8a293f5e8b252e1689841b436664c630e330d06b0a1cff9c9
4
- data.tar.gz: 7fa260b6afbb060a1c79a8dcb264aae5b211a24e69b042c720e242d39b7b3ab9
3
+ metadata.gz: f4e7169668b8720263d645f4a69385454d5c19ec621a9be173fff853becc4188
4
+ data.tar.gz: 9c314946080eb5ca6bb0c7e90a0cb29bef087c9fe6af5465310c3699ec4238db
5
5
  SHA512:
6
- metadata.gz: e96a09b0077bd54a30722201d03ccd8bb8baf7a47c0005d5c3d87441bfeb015479181548dce68bd1e882ee9e266be4658e68b2562e32dca6c48c101ca4e0e2cc
7
- data.tar.gz: dbee972964d0a2e54dbf7f4916c2bab6427525ace1d97a679845409b9d55390419631c8407df7502ca9b3561bd51ea0bba1ebecd8bbef680cdaa45b5ba28f405
6
+ metadata.gz: ca9bc3c4e73bd4b1c2fa09bb16aeca1f4a87d6af0522c2d1c1c34b7e9d5609cf3efe832850bc610cee9a2d8fa615b59ec81f1736f692fe6e849f74c03e6fa466
7
+ data.tar.gz: 9d2090d9f27e8876127eb5996f6be912c11532c02e2b1889c2007323fa4023b7a3a55058cdd3659d02993aef874bc97cb2f9f264632cced052f25ba43ba03d06
@@ -0,0 +1,18 @@
1
+ # Builds libglide_ffi.so against glibc 2.17 (CentOS 7 baseline), so the
2
+ # resulting binary loads on anything with glibc >= 2.17 — covers
3
+ # amazonlinux:2018.03 (2.17), amazonlinux:2 (2.26), amazonlinux:2023 (2.34).
4
+ FROM quay.io/pypa/manylinux2014_x86_64
5
+
6
+ ARG PROTOC_VERSION=29.1
7
+
8
+ RUN yum install -y unzip && yum clean all
9
+
10
+ RUN curl -sSL -o /tmp/protoc.zip \
11
+ "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" \
12
+ && unzip -q /tmp/protoc.zip -d /usr/local \
13
+ && rm /tmp/protoc.zip
14
+
15
+ ENV PATH="/root/.cargo/bin:${PATH}"
16
+ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
17
+
18
+ WORKDIR /workspace
@@ -581,7 +581,10 @@ class Valkey
581
581
  private
582
582
 
583
583
  # Flattens Arrays and Hashes (to alternating key/value) and stringifies
584
- # Integers/Floats, matching `redis-client`'s documented `#call`/`#call_v` behavior.
584
+ # Integers/Floats/Symbols, matching `redis-client`'s documented
585
+ # `#call`/`#call_v` behavior - including its type check: any leaf that
586
+ # isn't a String/Symbol/Integer/Float (e.g. `nil`) raises `TypeError`
587
+ # instead of being silently coerced to `""` via `#to_s`.
585
588
  #
586
589
  # @example
587
590
  # flatten_call_args(["CMD", [1, [2, 3]], { "a" => 1, "b" => [2, 3] }])
@@ -600,8 +603,10 @@ class Valkey
600
603
  stack.concat(arg.reverse)
601
604
  when Hash
602
605
  arg.to_a.reverse_each { |pair| stack.concat(pair.reverse) }
603
- else
606
+ when String, Symbol, Integer, Float
604
607
  acc << arg.to_s
608
+ else
609
+ raise TypeError, "Unsupported command argument type: #{arg.class}"
605
610
  end
606
611
  end
607
612
 
@@ -271,8 +271,13 @@ class Valkey
271
271
  end
272
272
 
273
273
  def invoke_script(script, args: [], keys: [])
274
- arg_ptrs, arg_lens = build_command_args(args)
275
- keys_ptrs, keys_lens = build_command_args(keys)
274
+ # Must hold onto the returned buffers (_arg_bufs/_keys_bufs) for the
275
+ # lifetime of this method - they back arg_ptrs/keys_ptrs, and letting
276
+ # them go out of scope (e.g. by only capturing the first 2 return
277
+ # values) makes them eligible for GC before the native call below
278
+ # reads through those pointers, corrupting ARGV/KEYS with freed memory.
279
+ arg_ptrs, arg_lens, _arg_bufs, flattened_args = build_command_args(args)
280
+ keys_ptrs, keys_lens, _keys_bufs, flattened_keys = build_command_args(keys)
276
281
 
277
282
  route = ""
278
283
  route_buf = FFI::MemoryPointer.from_string(route)
@@ -285,10 +290,10 @@ class Valkey
285
290
  @connection,
286
291
  0,
287
292
  sha,
288
- keys.size,
293
+ flattened_keys.size,
289
294
  keys_ptrs,
290
295
  keys_lens,
291
- args.size,
296
+ flattened_args.size,
292
297
  arg_ptrs,
293
298
  arg_lens,
294
299
  route_buf,
@@ -29,7 +29,7 @@ class Valkey
29
29
  # @param [Integer] decrement
30
30
  # @return [Integer] value after decrementing it
31
31
  def decrby(key, decrement)
32
- send_command(RequestType::DECR_BY, [key, decrement])
32
+ send_command(RequestType::DECR_BY, [key, Integer(decrement)])
33
33
  end
34
34
 
35
35
  # Increment the integer value of a key by one.
@@ -54,7 +54,7 @@ class Valkey
54
54
  # @param [Integer] increment
55
55
  # @return [Integer] value after incrementing it
56
56
  def incrby(key, increment)
57
- send_command(RequestType::INCR_BY, [key, increment])
57
+ send_command(RequestType::INCR_BY, [key, Integer(increment)])
58
58
  end
59
59
 
60
60
  # Increment the numeric value of a key by the given float number.
@@ -85,11 +85,16 @@ class Valkey
85
85
  # - `:get => true`: Return the old string stored at key, or nil if key did not exist.
86
86
  # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
87
87
  def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
88
- args = [key, value]
89
- args << "EX" << ex if ex
90
- args << "PX" << px if px
91
- args << "EXAT" << exat if exat
92
- args << "PXAT" << pxat if pxat
88
+ # value.to_s (matching redis-rb): a non-String value (e.g. an Array,
89
+ # in test_set_and_get_with_non_string_value) must become ONE opaque
90
+ # value here, not get flattened as if it were a multi-value list -
91
+ # see build_command_args's flat_map fix for why this must happen
92
+ # before command_args is built, not be left to that generic layer.
93
+ args = [key, value.to_s]
94
+ args << "EX" << Integer(ex) if ex
95
+ args << "PX" << Integer(px) if px
96
+ args << "EXAT" << Integer(exat) if exat
97
+ args << "PXAT" << Integer(pxat) if pxat
93
98
  args << "NX" if nx
94
99
  args << "XX" if xx
95
100
  args << "KEEPTTL" if keepttl
@@ -110,7 +115,7 @@ class Valkey
110
115
  # @param [String] value
111
116
  # @return [String] `"OK"`
112
117
  def setex(key, ttl, value)
113
- send_command(RequestType::SET_EX, [key, ttl, value])
118
+ send_command(RequestType::SET_EX, [key, Integer(ttl), value.to_s])
114
119
  end
115
120
 
116
121
  # Set the time to live in milliseconds of a key.
@@ -120,7 +125,7 @@ class Valkey
120
125
  # @param [String] value
121
126
  # @return [String] `"OK"`
122
127
  def psetex(key, ttl, value)
123
- send_command(RequestType::PSET_EX, [key, Integer(ttl), value])
128
+ send_command(RequestType::PSET_EX, [key, Integer(ttl), value.to_s])
124
129
  end
125
130
 
126
131
  # Set the value of a key, only if the key does not exist.
@@ -136,7 +141,7 @@ class Valkey
136
141
  # other GLIDE bindings' existing contracts expect to keep returning a
137
142
  # plain 0/1 integer. Doing the conversion here keeps it scoped to this
138
143
  # one Ruby-level method - see hexists/hsetnx for the same pattern.
139
- send_command(RequestType::SET_NX, [key, value], &Utils::Boolify)
144
+ send_command(RequestType::SET_NX, [key, value.to_s], &Utils::Boolify)
140
145
  end
141
146
 
142
147
  # Set one or more values.
@@ -259,7 +264,7 @@ class Valkey
259
264
  # @param [String] value
260
265
  # @return [Integer] length of the string after it was modified
261
266
  def setrange(key, offset, value)
262
- send_command(RequestType::SET_RANGE, [key, offset, value])
267
+ send_command(RequestType::SET_RANGE, [key, Integer(offset), value.to_s])
263
268
  end
264
269
 
265
270
  # Get a substring of the string stored at key.
@@ -301,10 +306,10 @@ class Valkey
301
306
  # @return [String, nil] the value of key, or nil when key does not exist
302
307
  def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
303
308
  args = [key]
304
- args << "EX" << ex if ex
305
- args << "PX" << px if px
306
- args << "EXAT" << exat if exat
307
- args << "PXAT" << pxat if pxat
309
+ args << "EX" << Integer(ex) if ex
310
+ args << "PX" << Integer(px) if px
311
+ args << "EXAT" << Integer(exat) if exat
312
+ args << "PXAT" << Integer(pxat) if pxat
308
313
  args << "PERSIST" if persist
309
314
 
310
315
  send_command(RequestType::GET_EX, args)
@@ -20,7 +20,17 @@ class Valkey
20
20
  # single atomic batch once the block returns - GLIDE wraps them in a real
21
21
  # MULTI/EXEC transaction internally. If the block raises, nothing has been
22
22
  # sent to the server yet, so the exception simply propagates - there is no
23
- # transaction to discard.
23
+ # transaction to discard. Each in-block command call returns a
24
+ # {Valkey::Future} immediately; capture it and call `#value` on it once
25
+ # the block has returned to read that command's own reply:
26
+ #
27
+ # @example Capturing a per-command reply
28
+ # future = nil
29
+ # valkey.multi do |multi|
30
+ # future = multi.incr("counter")
31
+ # multi.expire("counter", 60)
32
+ # end
33
+ # future.value # => 6
24
34
  # @yieldparam [Valkey::Pipeline] multi collects the block's commands
25
35
  #
26
36
  # @return [Array<...>]
@@ -31,11 +41,19 @@ class Valkey
31
41
  def multi
32
42
  if block_given?
33
43
  pipeline = Pipeline.new
34
- yield pipeline
35
44
 
36
- return [] if pipeline.commands.empty?
45
+ begin
46
+ yield pipeline
47
+
48
+ return [] if pipeline.commands.empty?
37
49
 
38
- send_batch_commands(pipeline.commands, exception: true, is_atomic: true)
50
+ results = send_batch_commands(pipeline.commands, exception: true, is_atomic: true)
51
+ pipeline.resolve_futures!(results)
52
+ results
53
+ rescue StandardError
54
+ pipeline.abort_futures!
55
+ raise
56
+ end
39
57
  else
40
58
  start_multi
41
59
  self
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ # Raised by Future#value when the pipeline/multi block has not finished
5
+ # executing yet (e.g. called from inside the block itself).
6
+ class FutureNotReady < BaseError
7
+ def initialize(msg = "Value will be available once the pipeline executes.")
8
+ super
9
+ end
10
+ end
11
+
12
+ # Raised by Future#value when the owning pipeline/multi block raised (or a
13
+ # sequential MULTI/EXEC/DISCARD fallback batch failed partway through)
14
+ # before this command's slot could ever be resolved. Distinct from
15
+ # FutureNotReady (still a subclass, so `rescue FutureNotReady` still catches
16
+ # it) to give a much clearer diagnostic than "not ready yet" for something
17
+ # that in fact will never become ready.
18
+ class FutureAborted < FutureNotReady
19
+ def initialize(msg = "This pipeline/multi block raised (or a sequential " \
20
+ "fallback batch failed partway through) before this " \
21
+ "command's result could be resolved.")
22
+ super
23
+ end
24
+ end
25
+
26
+ # A placeholder for the reply to a command queued inside Valkey#pipelined
27
+ # or the block form of Valkey::Commands::TransactionCommands#multi.
28
+ #
29
+ # Deliberately a plain Object, not BasicObject (unlike redis-rb's
30
+ # Redis::Future): #value is always called explicitly by callers, never as a
31
+ # transparent proxy, so BasicObject's stripped-down method surface buys
32
+ # nothing here.
33
+ #
34
+ # Coercion (Utils::Boolify, etc.) is intentionally not duplicated here: by
35
+ # the time Pipeline#resolve_futures! calls #_set, send_batch_commands has
36
+ # already applied it to the corresponding results[i].
37
+ class Future
38
+ NOT_READY = Object.new.freeze
39
+ private_constant :NOT_READY
40
+
41
+ def initialize(command_type, command_args)
42
+ @command_type = command_type
43
+ @command_args = command_args
44
+ @object = NOT_READY
45
+ @aborted = false
46
+ end
47
+
48
+ # @api private
49
+ def _set(object)
50
+ @object = object
51
+ end
52
+
53
+ # @api private
54
+ def _abort!
55
+ @aborted = true if @object.equal?(NOT_READY)
56
+ end
57
+
58
+ # @return [Boolean] whether #value can be called right now without
59
+ # raising FutureNotReady/FutureAborted.
60
+ def ready?
61
+ !@aborted && !@object.equal?(NOT_READY)
62
+ end
63
+
64
+ # @return [Object] the resolved reply, already coerced the same way a
65
+ # live (non-pipelined) call to the same command would be.
66
+ # @raise [FutureAborted] if the owning pipeline never got to resolve this
67
+ # command (block raised, or the batch failed partway through)
68
+ # @raise [FutureNotReady] if called before the pipeline/multi block has
69
+ # returned and its batch has been sent
70
+ # @raise [CommandError] if this slot's reply was itself an inline error
71
+ # (e.g. WRONGTYPE inside a batch) - matches Valkey#convert_response's
72
+ # "no rollback on runtime error" semantics.
73
+ def value
74
+ raise FutureAborted if @aborted
75
+ raise FutureNotReady if @object.equal?(NOT_READY)
76
+ raise @object if @object.is_a?(StandardError)
77
+
78
+ @object
79
+ end
80
+
81
+ def inspect
82
+ "#<Valkey::Future #{@command_type.inspect} #{@command_args.inspect}>"
83
+ end
84
+ end
85
+ end
@@ -4,10 +4,11 @@ class Valkey
4
4
  class Pipeline
5
5
  include Commands
6
6
 
7
- attr_reader :commands
7
+ attr_reader :commands, :futures
8
8
 
9
9
  def initialize
10
10
  @commands = []
11
+ @futures = []
11
12
  # Keep transactional state consistent with the main client so that
12
13
  # helpers like `multi`/`exec` can safely consult `@in_multi`.
13
14
  @in_multi = false
@@ -15,6 +16,33 @@ class Valkey
15
16
 
16
17
  def send_command(command_type, command_args = [], &block)
17
18
  @commands << [command_type, command_args, block]
19
+ future = Future.new(command_type, command_args)
20
+ @futures << future
21
+ future
22
+ end
23
+
24
+ # @api private - called by Valkey#pipelined / the block form of #multi
25
+ # once send_batch_commands' final results are available. Purely
26
+ # positional, mirroring send_batch_commands' own per-command block
27
+ # re-application - safe for both the real-batch and the sequential
28
+ # MULTI/EXEC/DISCARD fallback branch, since both produce the same
29
+ # shape/order of results.
30
+ #
31
+ # `results` is `nil` when a watched key was modified, aborting the whole
32
+ # transaction server-side before any queued command actually ran - none
33
+ # of these futures were ever really resolved, so treat it the same as
34
+ # abort_futures! instead of raising NoMethodError on a nil index.
35
+ def resolve_futures!(results)
36
+ return abort_futures! if results.nil?
37
+
38
+ @futures.each_with_index { |future, i| future._set(results[i]) }
39
+ end
40
+
41
+ # @api private - called when an exception escapes pipelined/multi before
42
+ # resolve_futures! ran, so every still-unresolved future raises a clear
43
+ # FutureAborted instead of hanging on FutureNotReady forever.
44
+ def abort_futures!
45
+ @futures.each(&:_abort!)
18
46
  end
19
47
  end
20
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Valkey
4
- VERSION = "0.9.2"
4
+ VERSION = "0.9.3-rc1"
5
5
  end
data/lib/valkey.rb CHANGED
@@ -13,6 +13,7 @@ require "valkey/bindings"
13
13
  require "valkey/utils"
14
14
  require "valkey/commands"
15
15
  require "valkey/errors"
16
+ require "valkey/future"
16
17
  require "valkey/pubsub_callback"
17
18
  require "valkey/pipeline"
18
19
  require "valkey/opentelemetry"
@@ -26,11 +27,18 @@ class Valkey
26
27
  def pipelined(exception: true)
27
28
  pipeline = Pipeline.new
28
29
 
29
- yield pipeline
30
+ begin
31
+ yield pipeline
30
32
 
31
- return [] if pipeline.commands.empty?
33
+ return [] if pipeline.commands.empty?
32
34
 
33
- send_batch_commands(pipeline.commands, exception: exception)
35
+ results = send_batch_commands(pipeline.commands, exception: exception)
36
+ pipeline.resolve_futures!(results)
37
+ results
38
+ rescue StandardError
39
+ pipeline.abort_futures!
40
+ raise
41
+ end
34
42
  end
35
43
 
36
44
  def initialize(options = {})
@@ -377,8 +385,9 @@ class Valkey
377
385
  arg_ptrs.put_pointer(0, FFI::MemoryPointer.new(1))
378
386
  arg_lens.put_ulong(0, 0)
379
387
  _buffers = [] # nothing to keep alive
388
+ flattened_args = command_args
380
389
  else
381
- arg_ptrs, arg_lens, _buffers = build_command_args(command_args)
390
+ arg_ptrs, arg_lens, _buffers, flattened_args = build_command_args(command_args)
382
391
  end
383
392
 
384
393
  # Create OpenTelemetry span if sampling is enabled, as a child of the app's current
@@ -410,7 +419,7 @@ class Valkey
410
419
  @connection,
411
420
  channel,
412
421
  command_type,
413
- command_args.size,
422
+ flattened_args.size,
414
423
  arg_ptrs,
415
424
  arg_lens,
416
425
  route_info.to_ptr,
@@ -426,7 +435,7 @@ class Valkey
426
435
  @connection,
427
436
  channel,
428
437
  command_type,
429
- command_args.size,
438
+ flattened_args.size,
430
439
  arg_ptrs,
431
440
  arg_lens,
432
441
  route_buf,
@@ -497,12 +506,12 @@ class Valkey
497
506
  buffers = [] # Keep references to prevent GC
498
507
 
499
508
  commands.each do |command_type, command_args, block|
500
- arg_ptrs, arg_lens, arg_bufs = build_command_args(command_args)
509
+ arg_ptrs, arg_lens, arg_bufs, flattened_args = build_command_args(command_args)
501
510
 
502
511
  cmd = Bindings::CmdInfo.new
503
512
  cmd[:request_type] = command_type
504
513
  cmd[:args] = arg_ptrs
505
- cmd[:arg_count] = command_args.size
514
+ cmd[:arg_count] = flattened_args.size
506
515
  cmd[:args_len] = arg_lens
507
516
 
508
517
  cmds << cmd
@@ -572,8 +581,12 @@ class Valkey
572
581
  end
573
582
  end
574
583
 
584
+ # An inline error slot (see the ResponseType::ERROR case in
585
+ # convert_response above) must be left alone here - e.g. Utils::Boolify
586
+ # would otherwise silently coerce a CommandError object to `true`
587
+ # (`value != 0` is true for any non-numeric object), hiding the error.
575
588
  blocks.each_with_index do |block, i|
576
- results[i] = block.call(results[i]) if block
589
+ results[i] = block.call(results[i]) if block && !results[i].is_a?(CommandError)
577
590
  end
578
591
 
579
592
  results
@@ -601,25 +614,43 @@ class Valkey
601
614
  { "manual_interval" => { "duration_in_sec" => duration_in_sec } }
602
615
  end
603
616
 
617
+ # Builds the FFI arg_ptrs/arg_lens/buffers for command_args, flattening nested
618
+ # Array/Hash elements first (mirroring redis-client's CommandBuilder#generate)
619
+ # so callers like hset(key, [field, value]) serialize correctly instead of
620
+ # collapsing into one garbled Array#to_s/Hash#to_s string. Returns the
621
+ # flattened command_args too - callers must size arg_count off this returned
622
+ # array, not their original pre-flatten one, or arg_count goes out of sync
623
+ # with arg_ptrs/arg_lens. Each element's type is checked against the same
624
+ # allow-list redis-client's CommandBuilder#generate uses (String, Symbol,
625
+ # Integer, Float) - anything else, including nil, raises TypeError instead
626
+ # of being silently coerced via #to_s (e.g. nil.to_s => "").
604
627
  def build_command_args(command_args)
628
+ # Flatten nested Arrays/Hashes to match redis-client's behavior.
629
+ command_args = command_args.flat_map { |el| el.is_a?(Hash) ? el.flatten : el }
630
+
605
631
  # For empty arrays, pass NULL pointers as per Rust FFI contract
606
632
  # This matches Go's approach which successfully uses nil pointers
607
- return [FFI::Pointer::NULL, FFI::Pointer::NULL, []] if command_args.empty?
633
+ return [FFI::Pointer::NULL, FFI::Pointer::NULL, [], []] if command_args.empty?
608
634
 
609
635
  arg_ptrs = FFI::MemoryPointer.new(:pointer, command_args.size)
610
636
  arg_lens = FFI::MemoryPointer.new(:ulong, command_args.size)
611
637
  buffers = []
612
638
 
613
639
  command_args.each_with_index do |arg, i|
614
- arg = arg.to_s # Ensure we convert to string
615
-
616
- buf = FFI::MemoryPointer.from_string(arg.to_s)
640
+ arg = case arg
641
+ when String, Symbol, Integer, Float
642
+ arg.to_s
643
+ else
644
+ raise TypeError, "Unsupported command argument type: #{arg.class}"
645
+ end
646
+
647
+ buf = FFI::MemoryPointer.from_string(arg)
617
648
  buffers << buf # prevent garbage collection
618
649
  arg_ptrs.put_pointer(i * FFI::Pointer.size, buf)
619
650
  arg_lens.put_ulong(i * 8, arg.bytesize)
620
651
  end
621
652
 
622
- [arg_ptrs, arg_lens, buffers]
653
+ [arg_ptrs, arg_lens, buffers, command_args]
623
654
  end
624
655
 
625
656
  def convert_response(res, return_map_as_hash: false, &block)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: valkey-glide-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3.pre.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valkey GLIDE Maintainers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-22 00:00:00.000000000 Z
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -40,6 +40,7 @@ files:
40
40
  - README.md
41
41
  - Rakefile
42
42
  - THIRD_PARTY_LICENSES_RUBY
43
+ - docker/manylinux2014/Dockerfile
43
44
  - examples/README.md
44
45
  - examples/cluster.rb
45
46
  - examples/opentelemetry.rb
@@ -70,6 +71,7 @@ files:
70
71
  - lib/valkey/commands/transaction_commands.rb
71
72
  - lib/valkey/commands/vector_search_commands.rb
72
73
  - lib/valkey/errors.rb
74
+ - lib/valkey/future.rb
73
75
  - lib/valkey/native/aarch64-apple-darwin/libglide_ffi.dylib
74
76
  - lib/valkey/native/aarch64-unknown-linux-gnu/libglide_ffi.so
75
77
  - lib/valkey/native/aarch64-unknown-linux-musl/libglide_ffi.so