redis 4.8.1 → 6.0.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -1
  3. data/README.md +285 -169
  4. data/lib/redis/client.rb +116 -611
  5. data/lib/redis/commands/bitmaps.rb +14 -4
  6. data/lib/redis/commands/cluster.rb +1 -18
  7. data/lib/redis/commands/connection.rb +5 -10
  8. data/lib/redis/commands/geo.rb +109 -7
  9. data/lib/redis/commands/hashes.rb +179 -8
  10. data/lib/redis/commands/hyper_log_log.rb +1 -1
  11. data/lib/redis/commands/keys.rb +32 -24
  12. data/lib/redis/commands/lists.rb +167 -25
  13. data/lib/redis/commands/modules/json.rb +530 -0
  14. data/lib/redis/commands/modules/search/aggregation.rb +418 -0
  15. data/lib/redis/commands/modules/search/dialect.rb +14 -0
  16. data/lib/redis/commands/modules/search/field.rb +306 -0
  17. data/lib/redis/commands/modules/search/hybrid.rb +359 -0
  18. data/lib/redis/commands/modules/search/index.rb +351 -0
  19. data/lib/redis/commands/modules/search/index_definition.rb +114 -0
  20. data/lib/redis/commands/modules/search/miscellaneous.rb +607 -0
  21. data/lib/redis/commands/modules/search/query.rb +738 -0
  22. data/lib/redis/commands/modules/search/result.rb +488 -0
  23. data/lib/redis/commands/modules/search/schema.rb +211 -0
  24. data/lib/redis/commands/modules/search.rb +19 -0
  25. data/lib/redis/commands/pubsub.rb +34 -25
  26. data/lib/redis/commands/server.rb +15 -15
  27. data/lib/redis/commands/sets.rb +76 -40
  28. data/lib/redis/commands/sorted_sets.rb +128 -19
  29. data/lib/redis/commands/streams.rb +75 -28
  30. data/lib/redis/commands/strings.rb +18 -17
  31. data/lib/redis/commands/transactions.rb +7 -31
  32. data/lib/redis/commands.rb +39 -20
  33. data/lib/redis/distributed.rb +407 -73
  34. data/lib/redis/errors.rb +20 -50
  35. data/lib/redis/hash_ring.rb +26 -26
  36. data/lib/redis/lib_identity.rb +105 -0
  37. data/lib/redis/pipeline.rb +47 -222
  38. data/lib/redis/subscribe.rb +51 -15
  39. data/lib/redis/version.rb +1 -1
  40. data/lib/redis.rb +213 -188
  41. metadata +25 -59
  42. data/lib/redis/cluster/command.rb +0 -79
  43. data/lib/redis/cluster/command_loader.rb +0 -33
  44. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  45. data/lib/redis/cluster/node.rb +0 -120
  46. data/lib/redis/cluster/node_key.rb +0 -31
  47. data/lib/redis/cluster/node_loader.rb +0 -34
  48. data/lib/redis/cluster/option.rb +0 -100
  49. data/lib/redis/cluster/slot.rb +0 -86
  50. data/lib/redis/cluster/slot_loader.rb +0 -46
  51. data/lib/redis/cluster.rb +0 -315
  52. data/lib/redis/connection/command_helper.rb +0 -41
  53. data/lib/redis/connection/hiredis.rb +0 -68
  54. data/lib/redis/connection/registry.rb +0 -13
  55. data/lib/redis/connection/ruby.rb +0 -437
  56. data/lib/redis/connection/synchrony.rb +0 -148
  57. data/lib/redis/connection.rb +0 -11
@@ -21,15 +21,12 @@ class Redis
21
21
  # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
22
22
  def xinfo(subcommand, key, group = nil)
23
23
  args = [:xinfo, subcommand, key, group].compact
24
- synchronize do |client|
25
- client.call(args) do |reply|
26
- case subcommand.to_s.downcase
27
- when 'stream' then Hashify.call(reply)
28
- when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
29
- else reply
30
- end
31
- end
24
+ block = case subcommand.to_s.downcase
25
+ when 'stream' then Hashify
26
+ when 'groups', 'consumers' then proc { |r| r.map(&Hashify) }
32
27
  end
28
+
29
+ send_command(args, &block)
33
30
  end
34
31
 
35
32
  # Add new entry to the stream.
@@ -37,26 +34,37 @@ class Redis
37
34
  # @example Without options
38
35
  # redis.xadd('mystream', f1: 'v1', f2: 'v2')
39
36
  # @example With options
40
- # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
37
+ # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true, nomkstream: true)
41
38
  #
42
39
  # @param key [String] the stream key
43
40
  # @param entry [Hash] one or multiple field-value pairs
44
41
  # @param opts [Hash] several options for `XADD` command
45
42
  #
46
43
  # @option opts [String] :id the entry id, default value is `*`, it means auto generation
47
- # @option opts [Integer] :maxlen max length of entries
48
- # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
44
+ # @option opts [Integer] :maxlen max length of entries to keep
45
+ # @option opts [Integer] :minid min id of entries to keep
46
+ # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen/minid or not
47
+ # @option opts [Integer] :limit maximum count of entries to be evicted, requires approximate trimming
48
+ # @option opts [Boolean] :nomkstream whether to add NOMKSTREAM, default is not to add
49
49
  #
50
50
  # @return [String] the entry id
51
- def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
51
+ def xadd(key, entry, approximate: nil, maxlen: nil, minid: nil, limit: nil, nomkstream: nil, id: '*')
52
52
  args = [:xadd, key]
53
+ args << 'NOMKSTREAM' if nomkstream
53
54
  if maxlen
55
+ raise ArgumentError, "can't supply both maxlen and minid" if minid
56
+
54
57
  args << "MAXLEN"
55
58
  args << "~" if approximate
56
59
  args << maxlen
60
+ elsif minid
61
+ args << "MINID"
62
+ args << "~" if approximate
63
+ args << minid
57
64
  end
65
+ args.concat(['LIMIT', limit]) if limit
58
66
  args << id
59
- args.concat(entry.to_a.flatten)
67
+ args.concat(entry.flatten)
60
68
  send_command(args)
61
69
  end
62
70
 
@@ -66,14 +74,30 @@ class Redis
66
74
  # redis.xtrim('mystream', 1000)
67
75
  # @example With options
68
76
  # redis.xtrim('mystream', 1000, approximate: true)
69
- #
70
- # @param key [String] the stream key
71
- # @param mexlen [Integer] max length of entries
72
- # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
77
+ # @example With strategy
78
+ # redis.xtrim('mystream', '1-0', strategy: 'MINID')
79
+ #
80
+ # @overload xtrim(key, maxlen, strategy: 'MAXLEN', approximate: true)
81
+ # @param key [String] the stream key
82
+ # @param maxlen [Integer] max length of entries
83
+ # @param strategy [String] the limit strategy, must be MAXLEN
84
+ # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
85
+ # @param limit [Integer] maximum count of entries to be evicted
86
+ # @overload xtrim(key, minid, strategy: 'MINID', approximate: true)
87
+ # @param key [String] the stream key
88
+ # @param minid [String] minimum id of entries
89
+ # @param strategy [String] the limit strategy, must be MINID
90
+ # @param approximate [Boolean] whether to add `~` modifier of minid or not
91
+ # @param limit [Integer] maximum count of entries to be evicted
73
92
  #
74
93
  # @return [Integer] the number of entries actually deleted
75
- def xtrim(key, maxlen, approximate: false)
76
- args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
94
+ def xtrim(key, len_or_id, strategy: 'MAXLEN', approximate: false, limit: nil)
95
+ strategy = strategy.to_s.upcase
96
+
97
+ args = [:xtrim, key, strategy]
98
+ args << '~' if approximate
99
+ args << len_or_id
100
+ args.concat(['LIMIT', limit]) if limit
77
101
  send_command(args)
78
102
  end
79
103
 
@@ -113,7 +137,7 @@ class Redis
113
137
  def xrange(key, start = '-', range_end = '+', count: nil)
114
138
  args = [:xrange, key, start, range_end]
115
139
  args.concat(['COUNT', count]) if count
116
- synchronize { |client| client.call(args, &HashifyStreamEntries) }
140
+ send_command(args, &HashifyStreamEntries)
117
141
  end
118
142
 
119
143
  # Fetches entries of the stream in descending order.
@@ -159,18 +183,28 @@ class Redis
159
183
  # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
160
184
  # @example With count option
161
185
  # redis.xread('mystream', '0-0', count: 2)
186
+ # @example With max_count option
187
+ # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0], max_count: 10)
162
188
  # @example With block option
163
189
  # redis.xread('mystream', '$', block: 1000)
164
190
  #
165
- # @param keys [Array<String>] one or multiple stream keys
166
- # @param ids [Array<String>] one or multiple entry ids
167
- # @param count [Integer] the number of entries as limit per stream
168
- # @param block [Integer] the number of milliseconds as blocking timeout
191
+ # @param keys [Array<String>] one or multiple stream keys
192
+ # @param ids [Array<String>] one or multiple entry ids
193
+ # @param count [Integer] the number of entries as limit per stream
194
+ # @param max_count [Integer] the total number of entries as limit across all streams
195
+ # combined, must be greater than or equal to `count` when both are given (Redis 8.10+).
196
+ # Defaults to unlimited.
197
+ # @param max_size [Integer] soft cap on the total reply size in bytes across all
198
+ # streams combined, measured on the serialized server reply including protocol overhead;
199
+ # a single oversized first entry can still be returned (Redis 8.10+). Defaults to unlimited.
200
+ # @param block [Integer] the number of milliseconds as blocking timeout
169
201
  #
170
202
  # @return [Hash{String => Hash{String => Hash}}] the entries
171
- def xread(keys, ids, count: nil, block: nil)
203
+ def xread(keys, ids, count: nil, max_count: nil, max_size: nil, block: nil)
172
204
  args = [:xread]
173
205
  args << 'COUNT' << count if count
206
+ args << 'MAXCOUNT' << Integer(max_count) if max_count
207
+ args << 'MAXSIZE' << Integer(max_size) if max_size
174
208
  args << 'BLOCK' << block.to_i if block
175
209
  _xread(args, keys, ids, block)
176
210
  end
@@ -221,14 +255,22 @@ class Redis
221
255
  # @param ids [Array<String>] one or multiple entry ids
222
256
  # @param opts [Hash] several options for `XREADGROUP` command
223
257
  #
224
- # @option opts [Integer] :count the number of entries as limit
258
+ # @option opts [Integer] :count the number of entries as limit per stream
259
+ # @option opts [Integer] :max_count the total number of entries as limit across all streams
260
+ # combined, must be greater than or equal to `count` when both are given (Redis 8.10+).
261
+ # Defaults to unlimited.
262
+ # @option opts [Integer] :max_size soft cap on the total reply size in bytes across all
263
+ # streams combined, measured on the serialized server reply including protocol overhead;
264
+ # a single oversized first entry can still be returned (Redis 8.10+). Defaults to unlimited.
225
265
  # @option opts [Integer] :block the number of milliseconds as blocking timeout
226
266
  # @option opts [Boolean] :noack whether message loss is acceptable or not
227
267
  #
228
268
  # @return [Hash{String => Hash{String => Hash}}] the entries
229
- def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
269
+ def xreadgroup(group, consumer, keys, ids, count: nil, max_count: nil, max_size: nil, block: nil, noack: nil)
230
270
  args = [:xreadgroup, 'GROUP', group, consumer]
231
271
  args << 'COUNT' << count if count
272
+ args << 'MAXCOUNT' << Integer(max_count) if max_count
273
+ args << 'MAXSIZE' << Integer(max_size) if max_size
232
274
  args << 'BLOCK' << block.to_i if block
233
275
  args << 'NOACK' if noack
234
276
  _xread(args, keys, ids, block)
@@ -334,6 +376,8 @@ class Redis
334
376
  # redis.xpending('mystream', 'mygroup')
335
377
  # @example With range options
336
378
  # redis.xpending('mystream', 'mygroup', '-', '+', 10)
379
+ # @example With range and idle time options
380
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10, idle: 9000)
337
381
  # @example With range and consumer options
338
382
  # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
339
383
  #
@@ -344,10 +388,13 @@ class Redis
344
388
  # @param count [Integer] count the number of entries as limit
345
389
  # @param consumer [String] the consumer name
346
390
  #
391
+ # @option opts [Integer] :idle pending message minimum idle time in milliseconds
392
+ #
347
393
  # @return [Hash] the summary of pending entries
348
394
  # @return [Array<Hash>] the pending entries details if options were specified
349
- def xpending(key, group, *args)
395
+ def xpending(key, group, *args, idle: nil)
350
396
  command_args = [:xpending, key, group]
397
+ command_args << 'IDLE' << Integer(idle) if idle
351
398
  case args.size
352
399
  when 0, 3, 4
353
400
  command_args.concat(args)
@@ -25,7 +25,7 @@ class Redis
25
25
  # @param [Integer] decrement
26
26
  # @return [Integer] value after decrementing it
27
27
  def decrby(key, decrement)
28
- send_command([:decrby, key, decrement])
28
+ send_command([:decrby, key, Integer(decrement)])
29
29
  end
30
30
 
31
31
  # Increment the integer value of a key by one.
@@ -50,7 +50,7 @@ class Redis
50
50
  # @param [Integer] increment
51
51
  # @return [Integer] value after incrementing it
52
52
  def incrby(key, increment)
53
- send_command([:incrby, key, increment])
53
+ send_command([:incrby, key, Integer(increment)])
54
54
  end
55
55
 
56
56
  # Increment the numeric value of a key by the given float number.
@@ -63,7 +63,7 @@ class Redis
63
63
  # @param [Float] increment
64
64
  # @return [Float] value after incrementing it
65
65
  def incrbyfloat(key, increment)
66
- send_command([:incrbyfloat, key, increment], &Floatify)
66
+ send_command([:incrbyfloat, key, Float(increment)], &Floatify)
67
67
  end
68
68
 
69
69
  # Set the string value of a key.
@@ -82,10 +82,10 @@ class Redis
82
82
  # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
83
83
  def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
84
84
  args = [:set, key, value.to_s]
85
- args << "EX" << ex if ex
86
- args << "PX" << px if px
87
- args << "EXAT" << exat if exat
88
- args << "PXAT" << pxat if pxat
85
+ args << "EX" << Integer(ex) if ex
86
+ args << "PX" << Integer(px) if px
87
+ args << "EXAT" << Integer(exat) if exat
88
+ args << "PXAT" << Integer(pxat) if pxat
89
89
  args << "NX" if nx
90
90
  args << "XX" if xx
91
91
  args << "KEEPTTL" if keepttl
@@ -105,7 +105,7 @@ class Redis
105
105
  # @param [String] value
106
106
  # @return [String] `"OK"`
107
107
  def setex(key, ttl, value)
108
- send_command([:setex, key, ttl, value.to_s])
108
+ send_command([:setex, key, Integer(ttl), value.to_s])
109
109
  end
110
110
 
111
111
  # Set the time to live in milliseconds of a key.
@@ -115,7 +115,7 @@ class Redis
115
115
  # @param [String] value
116
116
  # @return [String] `"OK"`
117
117
  def psetex(key, ttl, value)
118
- send_command([:psetex, key, ttl, value.to_s])
118
+ send_command([:psetex, key, Integer(ttl), value.to_s])
119
119
  end
120
120
 
121
121
  # Set the value of a key, only if the key does not exist.
@@ -152,7 +152,7 @@ class Redis
152
152
  #
153
153
  # @see #mset
154
154
  def mapped_mset(hash)
155
- mset(hash.to_a.flatten)
155
+ mset(hash.flatten)
156
156
  end
157
157
 
158
158
  # Set one or more values, only if none of the keys exist.
@@ -180,7 +180,7 @@ class Redis
180
180
  #
181
181
  # @see #msetnx
182
182
  def mapped_msetnx(hash)
183
- msetnx(hash.to_a.flatten)
183
+ msetnx(hash.flatten)
184
184
  end
185
185
 
186
186
  # Get the value of a key.
@@ -202,6 +202,7 @@ class Redis
202
202
  #
203
203
  # @see #mapped_mget
204
204
  def mget(*keys, &blk)
205
+ keys.flatten!(1)
205
206
  send_command([:mget, *keys], &blk)
206
207
  end
207
208
 
@@ -232,7 +233,7 @@ class Redis
232
233
  # @param [String] value
233
234
  # @return [Integer] length of the string after it was modified
234
235
  def setrange(key, offset, value)
235
- send_command([:setrange, key, offset, value.to_s])
236
+ send_command([:setrange, key, Integer(offset), value.to_s])
236
237
  end
237
238
 
238
239
  # Get a substring of the string stored at a key.
@@ -243,7 +244,7 @@ class Redis
243
244
  # the end of the string
244
245
  # @return [Integer] `0` or `1`
245
246
  def getrange(key, start, stop)
246
- send_command([:getrange, key, start, stop])
247
+ send_command([:getrange, key, Integer(start), Integer(stop)])
247
248
  end
248
249
 
249
250
  # Append a value to a key.
@@ -291,10 +292,10 @@ class Redis
291
292
  # @return [String] The value of key, or nil when key does not exist.
292
293
  def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
293
294
  args = [:getex, key]
294
- args << "EX" << ex if ex
295
- args << "PX" << px if px
296
- args << "EXAT" << exat if exat
297
- args << "PXAT" << pxat if pxat
295
+ args << "EX" << Integer(ex) if ex
296
+ args << "PX" << Integer(px) if px
297
+ args << "EXAT" << Integer(exat) if exat
298
+ args << "PXAT" << Integer(pxat) if pxat
298
299
  args << "PERSIST" if persist
299
300
 
300
301
  send_command(args)
@@ -5,48 +5,26 @@ class Redis
5
5
  module Transactions
6
6
  # Mark the start of a transaction block.
7
7
  #
8
- # Passing a block is optional.
9
- #
10
8
  # @example With a block
11
9
  # redis.multi do |multi|
12
10
  # multi.set("key", "value")
13
11
  # multi.incr("counter")
14
12
  # end # => ["OK", 6]
15
13
  #
16
- # @example Without a block
17
- # redis.multi
18
- # # => "OK"
19
- # redis.set("key", "value")
20
- # # => "QUEUED"
21
- # redis.incr("counter")
22
- # # => "QUEUED"
23
- # redis.exec
24
- # # => ["OK", 6]
25
- #
26
14
  # @yield [multi] the commands that are called inside this block are cached
27
15
  # and written to the server upon returning from it
28
16
  # @yieldparam [Redis] multi `self`
29
17
  #
30
- # @return [String, Array<...>]
31
- # - when a block is not given, `OK`
32
- # - when a block is given, an array with replies
18
+ # @return [Array<...>]
19
+ # - an array with replies
33
20
  #
34
21
  # @see #watch
35
22
  # @see #unwatch
36
- def multi(&block) # :nodoc:
37
- if block_given?
38
- if block&.arity == 0
39
- Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5))
40
- end
41
-
42
- synchronize do |prior_client|
43
- pipeline = Pipeline::Multi.new(prior_client)
44
- pipelined_connection = PipelinedConnection.new(pipeline)
45
- yield pipelined_connection
46
- prior_client.call_pipeline(pipeline)
23
+ def multi
24
+ synchronize do |client|
25
+ client.multi do |raw_transaction|
26
+ yield MultiConnection.new(raw_transaction)
47
27
  end
48
- else
49
- send_command([:multi])
50
28
  end
51
29
  end
52
30
 
@@ -82,7 +60,7 @@ class Redis
82
60
  # @see #multi
83
61
  def watch(*keys)
84
62
  synchronize do |client|
85
- res = client.call([:watch, *keys])
63
+ res = client.call_v([:watch] + keys)
86
64
 
87
65
  if block_given?
88
66
  begin
@@ -125,8 +103,6 @@ class Redis
125
103
 
126
104
  # Discard all commands issued after MULTI.
127
105
  #
128
- # Only call this method when `#multi` was called **without** a block.
129
- #
130
106
  # @return [String] `"OK"`
131
107
  #
132
108
  # @see #multi
@@ -8,8 +8,10 @@ require "redis/commands/hashes"
8
8
  require "redis/commands/hyper_log_log"
9
9
  require "redis/commands/keys"
10
10
  require "redis/commands/lists"
11
+ require "redis/commands/modules/json"
11
12
  require "redis/commands/pubsub"
12
13
  require "redis/commands/scripting"
14
+ require "redis/commands/modules/search"
13
15
  require "redis/commands/server"
14
16
  require "redis/commands/sets"
15
17
  require "redis/commands/sorted_sets"
@@ -27,8 +29,10 @@ class Redis
27
29
  include HyperLogLog
28
30
  include Keys
29
31
  include Lists
32
+ include Json
30
33
  include Pubsub
31
34
  include Scripting
35
+ include Search
32
36
  include Server
33
37
  include Sets
34
38
  include SortedSets
@@ -40,12 +44,7 @@ class Redis
40
44
  # where the method call will return nil. Propagate the nil instead of falsely
41
45
  # returning false.
42
46
  Boolify = lambda { |value|
43
- case value
44
- when Integer
45
- value > 0
46
- else
47
- value
48
- end
47
+ value != 0 unless value.nil?
49
48
  }
50
49
 
51
50
  BoolifySet = lambda { |value|
@@ -60,7 +59,9 @@ class Redis
60
59
  }
61
60
 
62
61
  Hashify = lambda { |value|
63
- if value.respond_to?(:each_slice)
62
+ if value.is_a?(Hash) # RESP3 already returns a map
63
+ value
64
+ elsif value.respond_to?(:each_slice) # RESP2 flat [k, v, k, v, ...]
64
65
  value.each_slice(2).to_h
65
66
  else
66
67
  value
@@ -68,10 +69,12 @@ class Redis
68
69
  }
69
70
 
70
71
  Pairify = lambda { |value|
71
- if value.respond_to?(:each_slice)
72
- value.each_slice(2).to_a
73
- else
72
+ return value unless value.respond_to?(:each_slice)
73
+
74
+ if value.first.is_a?(Array) # RESP3 already returns [[k, v], ...]
74
75
  value
76
+ else # RESP2 flat [k, v, k, v, ...]
77
+ value.each_slice(2).to_a
75
78
  end
76
79
  }
77
80
 
@@ -88,11 +91,20 @@ class Redis
88
91
  end
89
92
  }
90
93
 
94
+ FloatifyPair = lambda { |(first, score)|
95
+ [first, Floatify.call(score)]
96
+ }
97
+
91
98
  FloatifyPairs = lambda { |value|
92
99
  return value unless value.respond_to?(:each_slice)
93
100
 
94
- value.each_slice(2).map do |member, score|
95
- [member, Floatify.call(score)]
101
+ if value.first.is_a?(Array) # RESP3 already returns [[member, score], ...]
102
+ # Scores arrive as native doubles, so the pairs are already in the final shape and
103
+ # re-mapping would only re-allocate identical arrays. Floatify only transforms Strings, so
104
+ # unless a score came back as one (it shouldn't under RESP3) return the parser's array as-is.
105
+ value.first.last.is_a?(String) ? value.map(&FloatifyPair) : value
106
+ else # RESP2 flat [member, score, member, score, ...]
107
+ value.each_slice(2).map(&FloatifyPair)
96
108
  end
97
109
  }
98
110
 
@@ -124,7 +136,9 @@ class Redis
124
136
  HashifyStreamAutoclaim = lambda { |reply|
125
137
  {
126
138
  'next' => reply[0],
127
- 'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
139
+ 'entries' => reply[1].compact.map do |entry, values|
140
+ [entry, values.each_slice(2)&.to_h]
141
+ end
128
142
  }
129
143
  }
130
144
 
@@ -202,8 +216,8 @@ class Redis
202
216
  # hash, are up to consumers.
203
217
  #
204
218
  # Redis error replies are raised as Ruby exceptions.
205
- def call(*command)
206
- send_command(command)
219
+ def call(*command, &block)
220
+ send_command(command, &block)
207
221
  end
208
222
 
209
223
  # Interact with the sentinel command (masters, master, slaves, failover)
@@ -218,14 +232,19 @@ class Redis
218
232
  when "get-master-addr-by-name"
219
233
  reply
220
234
  else
221
- if reply.is_a?(Array)
222
- if reply[0].is_a?(Array)
223
- reply.map(&Hashify)
235
+ case reply
236
+ when Array
237
+ if reply.empty?
238
+ reply # empty list (e.g. sentinels/slaves with no entries) stays []; don't Hashify to {}
224
239
  else
225
- Hashify.call(reply)
240
+ case reply[0]
241
+ when Array then reply.map(&Hashify) # RESP2: list of flat [k, v, ...] arrays
242
+ when Hash then reply # RESP3: list of maps (already hashes)
243
+ else Hashify.call(reply) # RESP2: a single flat [k, v, ...] array
244
+ end
226
245
  end
227
246
  else
228
- reply
247
+ reply # RESP3 single map, or a scalar reply
229
248
  end
230
249
  end
231
250
  end