async-redis 0.11.2 → 0.13.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.
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "async/limited_queue"
7
+ require "async/barrier"
8
+
9
+ module Async
10
+ module Redis
11
+ # Context for managing sharded subscriptions across multiple Redis cluster nodes.
12
+ # This class handles the complexity of subscribing to channels that may be distributed
13
+ # across different shards in a Redis cluster.
14
+ class ClusterSubscription
15
+ # Represents a failure in the subscription process, e.g. network issues, shard failures.
16
+ class SubscriptionError < StandardError
17
+ end
18
+
19
+ # Initialize a new shard subscription context.
20
+ # @parameter cluster_client [ClusterClient] The cluster client to use.
21
+ def initialize(cluster_client, queue: Async::LimitedQueue.new)
22
+ @cluster_client = cluster_client
23
+ @subscriptions = {}
24
+ @channels = []
25
+
26
+ @barrier = Async::Barrier.new
27
+ @queue = queue
28
+ end
29
+
30
+ # Close all shard subscriptions.
31
+ def close
32
+ if barrier = @barrier
33
+ @barrier = nil
34
+ barrier.stop
35
+ end
36
+
37
+ @subscriptions.each_value(&:close)
38
+ @subscriptions.clear
39
+ end
40
+
41
+ # Listen for the next message from any subscribed shard.
42
+ # @returns [Array] The next message response.
43
+ # @raises [SubscriptionError] If the subscription has failed for any reason.
44
+ def listen
45
+ @queue.pop
46
+ rescue => error
47
+ raise SubscriptionError, "Failed to read message!"
48
+ end
49
+
50
+ # Iterate over all messages from all subscribed shards.
51
+ # @yields {|response| ...} Block called for each message.
52
+ # @parameter response [Array] The message response.
53
+ def each
54
+ return to_enum unless block_given?
55
+
56
+ while response = self.listen
57
+ yield response
58
+ end
59
+ end
60
+
61
+ # Subscribe to additional sharded channels.
62
+ # @parameter channels [Array(String)] The channels to subscribe to.
63
+ def subscribe(channels)
64
+ slots = @cluster_client.slots_for(channels)
65
+
66
+ slots.each do |slot, channels_for_slot|
67
+ if subscription = @subscriptions[slot]
68
+ # Add to existing subscription for this shard
69
+ subscription.ssubscribe(channels_for_slot)
70
+ else
71
+ # Create new subscription for this shard
72
+ client = @cluster_client.client_for(slot)
73
+ subscription = @subscriptions[slot] = client.ssubscribe(*channels_for_slot)
74
+
75
+ @barrier.async do
76
+ # This is optimistic, in other words, subscription.listen will also fail on close.
77
+ until subscription.closed?
78
+ @queue << subscription.listen
79
+ end
80
+ ensure
81
+ # If we are exiting here for any reason OTHER than the subscription was closed, we need to re-create the subscription state:
82
+ unless subscription.closed?
83
+ @queue.close
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ @channels.concat(channels)
90
+ end
91
+
92
+ # Unsubscribe from sharded channels.
93
+ # @parameter channels [Array(String)] The channels to unsubscribe from.
94
+ def unsubscribe(channels)
95
+ slots = @cluster_client.slots_for(channels)
96
+
97
+ slots.each do |slot, channels_for_slot|
98
+ if subscription = @subscriptions[slot]
99
+ subscription.sunsubscribe(channels_for_slot)
100
+
101
+ # Remove channels from our tracking
102
+ @channels -= channels_for_slot
103
+
104
+ # Check if this shard still has channels
105
+ remaining_channels_for_slot = @channels.select {|ch| @cluster_client.slot_for(ch) == slot}
106
+
107
+ # If no channels left for this shard, close and remove it
108
+ if remaining_channels_for_slot.empty?
109
+ @subscriptions.delete(slot)
110
+ subscription.close
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # Get the list of currently subscribed channels.
117
+ # @returns [Array(String)] The list of subscribed channels.
118
+ def channels
119
+ @channels.dup
120
+ end
121
+
122
+ # Get the number of active shard subscriptions.
123
+ # @returns [Integer] The number of shard connections.
124
+ def shard_count
125
+ @subscriptions.size
126
+ end
127
+ end
128
+ end
129
+ end
@@ -22,12 +22,17 @@ module Async
22
22
 
23
23
  # Close the context and release the connection back to the pool.
24
24
  def close
25
- if @connection
26
- @pool.release(@connection)
25
+ if connection = @connection
27
26
  @connection = nil
27
+ @pool.release(connection)
28
28
  end
29
29
  end
30
30
 
31
+ # @returns [Boolean] Whether the context is closed.
32
+ def closed?
33
+ @connection.nil?
34
+ end
35
+
31
36
  # Write a Redis command request to the connection.
32
37
  # @parameter command [String] The Redis command.
33
38
  # @parameter arguments [Array] The command arguments.
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019, by David Ortiz.
5
- # Copyright, 2019-2024, by Samuel Williams.
5
+ # Copyright, 2019-2025, by Samuel Williams.
6
6
  # Copyright, 2022, by Tim Willard.
7
7
 
8
8
  require_relative "generic"
@@ -44,7 +44,7 @@ module Async
44
44
  end
45
45
 
46
46
  # Flush responses.
47
- # @param count [Integer] leave this many responses.
47
+ # @parameter count [Integer] leave this many responses.
48
48
  def flush(count = 0)
49
49
  while @count > count
50
50
  read_response
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018, by Huba Nagy.
5
- # Copyright, 2018-2024, by Samuel Williams.
5
+ # Copyright, 2018-2025, by Samuel Williams.
6
6
 
7
7
  require_relative "generic"
8
8
 
@@ -10,8 +10,10 @@ module Async
10
10
  module Redis
11
11
  module Context
12
12
  # Context for Redis pub/sub subscription operations.
13
- class Subscribe < Generic
13
+ class Subscription < Generic
14
14
  MESSAGE = "message"
15
+ PMESSAGE = "pmessage"
16
+ SMESSAGE = "smessage"
15
17
 
16
18
  # Initialize a new subscription context.
17
19
  # @parameter pool [Pool] The connection pool to use.
@@ -19,12 +21,12 @@ module Async
19
21
  def initialize(pool, channels)
20
22
  super(pool)
21
23
 
22
- subscribe(channels)
24
+ subscribe(channels) if channels.any?
23
25
  end
24
26
 
25
27
  # Close the subscription context.
26
28
  def close
27
- # There is no way to reset subscription state. On Redis v6+ you can use RESET, but this is not supported in <= v6.
29
+ # This causes anyone calling `#listen` to exit, as `read_response` will fail. If we decided to use `RESET` instead, we'd need to take that into account.
28
30
  @connection&.close
29
31
 
30
32
  super
@@ -34,7 +36,11 @@ module Async
34
36
  # @returns [Array] The next message response, or nil if connection closed.
35
37
  def listen
36
38
  while response = @connection.read_response
37
- return response if response.first == MESSAGE
39
+ type = response.first
40
+
41
+ if type == MESSAGE || type == PMESSAGE || type == SMESSAGE
42
+ return response
43
+ end
38
44
  end
39
45
  end
40
46
 
@@ -62,6 +68,34 @@ module Async
62
68
  @connection.write_request ["UNSUBSCRIBE", *channels]
63
69
  @connection.flush
64
70
  end
71
+
72
+ # Subscribe to channel patterns.
73
+ # @parameter patterns [Array(String)] The channel patterns to subscribe to.
74
+ def psubscribe(patterns)
75
+ @connection.write_request ["PSUBSCRIBE", *patterns]
76
+ @connection.flush
77
+ end
78
+
79
+ # Unsubscribe from channel patterns.
80
+ # @parameter patterns [Array(String)] The channel patterns to unsubscribe from.
81
+ def punsubscribe(patterns)
82
+ @connection.write_request ["PUNSUBSCRIBE", *patterns]
83
+ @connection.flush
84
+ end
85
+
86
+ # Subscribe to sharded channels (Redis 7.0+).
87
+ # @parameter channels [Array(String)] The sharded channels to subscribe to.
88
+ def ssubscribe(channels)
89
+ @connection.write_request ["SSUBSCRIBE", *channels]
90
+ @connection.flush
91
+ end
92
+
93
+ # Unsubscribe from sharded channels (Redis 7.0+).
94
+ # @parameter channels [Array(String)] The sharded channels to unsubscribe from.
95
+ def sunsubscribe(channels)
96
+ @connection.write_request ["SUNSUBSCRIBE", *channels]
97
+ @connection.flush
98
+ end
65
99
  end
66
100
  end
67
101
  end
@@ -2,10 +2,12 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024-2025, by Samuel Williams.
5
+ # Copyright, 2025, by Joan Lledó.
5
6
 
6
7
  require "io/endpoint"
7
8
  require "io/endpoint/host_endpoint"
8
9
  require "io/endpoint/ssl_endpoint"
10
+ require "io/endpoint/unix_endpoint"
9
11
 
10
12
  require_relative "protocol/resp2"
11
13
  require_relative "protocol/authenticated"
@@ -41,6 +43,15 @@ module Async
41
43
  self.new(URI::Generic.build(scheme: "redis", host: host, port: port), **options)
42
44
  end
43
45
 
46
+ # Create a local Redis endpoint from a UNIX socket path.
47
+ # @parameter path [String] The path to the UNIX socket.
48
+ # @parameter options [Hash] Additional options for the endpoint.
49
+ # @returns [Endpoint] A local Redis endpoint.
50
+ def self.unix(path, **options)
51
+ endpoint = ::IO::Endpoint.unix(path)
52
+ self.new(URI::Generic.build(scheme: "redis", path:), endpoint, **options)
53
+ end
54
+
44
55
  SCHEMES = {
45
56
  "redis" => URI::Generic,
46
57
  "rediss" => URI::Generic,
@@ -58,11 +69,19 @@ module Async
58
69
  end
59
70
 
60
71
  # Construct an endpoint with a specified scheme, hostname, optional path, and options.
72
+ # If no scheme is provided, it will be auto-detected based on SSL context.
61
73
  #
62
- # @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
74
+ # @parameter scheme [String, nil] The scheme to use, e.g. "redis" or "rediss". If nil, will auto-detect.
63
75
  # @parameter hostname [String] The hostname to connect to (or bind to).
64
76
  # @parameter options [Hash] Additional options, passed to {#initialize}.
65
- def self.for(scheme, host, credentials: nil, port: nil, database: nil, **options)
77
+ def self.for(scheme, host, port: nil, database: nil, **options)
78
+ # Auto-detect scheme if not provided:
79
+ if default_scheme = options.delete(:scheme)
80
+ scheme ||= default_scheme
81
+ end
82
+
83
+ scheme ||= options.key?(:ssl_context) ? "rediss" : "redis"
84
+
66
85
  uri_klass = SCHEMES.fetch(scheme.downcase) do
67
86
  raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
68
87
  end
@@ -74,7 +93,6 @@ module Async
74
93
  self.new(
75
94
  uri_klass.build(
76
95
  scheme: scheme,
77
- userinfo: credentials&.join(":"),
78
96
  host: host,
79
97
  port: port,
80
98
  path: path,
@@ -210,6 +228,8 @@ module Async
210
228
  end
211
229
  end
212
230
 
231
+ # Get the credentials for authentication.
232
+ # @returns [Array(String) | Nil] The username and password credentials or nil if not specified.
213
233
  def credentials
214
234
  @options[:credentials] || extract_userinfo(@url.userinfo)
215
235
  end
@@ -224,6 +244,8 @@ module Async
224
244
  end
225
245
  end
226
246
 
247
+ # Check if the endpoint is connecting to localhost.
248
+ # @returns [Boolean] True if connecting to localhost.
227
249
  def localhost?
228
250
  @url.hostname =~ /^(.*?\.)?localhost\.?$/
229
251
  end
@@ -237,6 +259,8 @@ module Async
237
259
  end
238
260
  end
239
261
 
262
+ # Get the SSL context for secure connections.
263
+ # @returns [OpenSSL::SSL::SSLContext] The SSL context configured for this endpoint.
240
264
  def ssl_context
241
265
  @options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context|
242
266
  context.set_params(
@@ -245,8 +269,11 @@ module Async
245
269
  end
246
270
  end
247
271
 
272
+ # Build the underlying endpoint with optional SSL wrapping.
273
+ # @parameter endpoint [IO::Endpoint] Optional base endpoint to wrap.
274
+ # @returns [IO::Endpoint] The built endpoint, potentially wrapped with SSL.
248
275
  def build_endpoint(endpoint = nil)
249
- endpoint ||= tcp_endpoint
276
+ endpoint ||= make_endpoint
250
277
 
251
278
  if secure?
252
279
  # Wrap it in SSL:
@@ -260,22 +287,33 @@ module Async
260
287
  return endpoint
261
288
  end
262
289
 
290
+ # Get the underlying endpoint, building it if necessary.
291
+ # @returns [IO::Endpoint] The underlying endpoint for connections.
263
292
  def endpoint
264
293
  @endpoint ||= build_endpoint
265
294
  end
266
295
 
296
+ # Set the underlying endpoint.
297
+ # @parameter endpoint [IO::Endpoint] The endpoint to wrap and use.
267
298
  def endpoint=(endpoint)
268
299
  @endpoint = build_endpoint(endpoint)
269
300
  end
270
301
 
302
+ # Bind to the endpoint and yield the server socket.
303
+ # @parameter arguments [Array] Arguments to pass to the underlying endpoint bind method.
304
+ # @yields [IO] The bound server socket.
271
305
  def bind(*arguments, &block)
272
306
  endpoint.bind(*arguments, &block)
273
307
  end
274
308
 
309
+ # Connect to the endpoint and yield the client socket.
310
+ # @yields [IO] The connected client socket.
275
311
  def connect(&block)
276
312
  endpoint.connect(&block)
277
313
  end
278
314
 
315
+ # Iterate over each possible endpoint variation.
316
+ # @yields [Endpoint] Each endpoint variant.
279
317
  def each
280
318
  return to_enum unless block_given?
281
319
 
@@ -284,14 +322,21 @@ module Async
284
322
  end
285
323
  end
286
324
 
325
+ # Get the key for hashing and equality comparison.
326
+ # @returns [Array] The key components for this endpoint.
287
327
  def key
288
328
  [@url, @options]
289
329
  end
290
330
 
331
+ # Check if this endpoint is equal to another.
332
+ # @parameter other [Endpoint] The other endpoint to compare with.
333
+ # @returns [Boolean] True if the endpoints are equal.
291
334
  def eql? other
292
335
  self.key.eql? other.key
293
336
  end
294
337
 
338
+ # Get the hash code for this endpoint.
339
+ # @returns [Integer] The hash code based on the endpoint's key.
295
340
  def hash
296
341
  self.key.hash
297
342
  end
@@ -310,9 +355,21 @@ module Async
310
355
  return options
311
356
  end
312
357
 
358
+ def unix_endpoint
359
+ ::IO::Endpoint.unix(@url.path)
360
+ end
361
+
313
362
  def tcp_endpoint
314
363
  ::IO::Endpoint.tcp(self.hostname, port, **tcp_options)
315
364
  end
365
+
366
+ def make_endpoint
367
+ if @url.host
368
+ tcp_endpoint
369
+ else
370
+ unix_endpoint
371
+ end
372
+ end
316
373
  end
317
374
  end
318
375
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Redis
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2024, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/redis"
7
7
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Async
7
+ module Redis
8
+ # A map that stores ranges and their associated values for efficient lookup.
9
+ class RangeMap
10
+ # Initialize a new RangeMap.
11
+ def initialize
12
+ @ranges = []
13
+ end
14
+
15
+ # Add a range-value pair to the map.
16
+ # @parameter range [Range] The range to map.
17
+ # @parameter value [Object] The value to associate with the range.
18
+ # @returns [Object] The added value.
19
+ def add(range, value)
20
+ @ranges << [range, value]
21
+ return value
22
+ end
23
+
24
+ # Find the value associated with a key within any range.
25
+ # @parameter key [Object] The key to find.
26
+ # @yields {...} Block called if no range contains the key.
27
+ # @returns [Object] The value if found, result of block if given, or nil.
28
+ def find(key)
29
+ @ranges.each do |range, value|
30
+ return value if range.include?(key)
31
+ end
32
+ if block_given?
33
+ return yield
34
+ end
35
+ return nil
36
+ end
37
+
38
+ # Iterate over all values in the map.
39
+ # @yields {|value| ...} Block called for each value.
40
+ # @parameter value [Object] The value from the range-value pair.
41
+ def each
42
+ @ranges.each do |range, value|
43
+ yield value
44
+ end
45
+ end
46
+
47
+ # Get a random value from the map.
48
+ # @returns [Object] A randomly selected value, or nil if map is empty.
49
+ def sample
50
+ return nil if @ranges.empty?
51
+ range, value = @ranges.sample
52
+ return value
53
+ end
54
+
55
+ # Clear all ranges from the map.
56
+ def clear
57
+ @ranges.clear
58
+ end
59
+ end
60
+ end
61
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020, by David Ortiz.
5
- # Copyright, 2023-2024, by Samuel Williams.
5
+ # Copyright, 2023-2025, by Samuel Williams.
6
6
  # Copyright, 2024, by Joan Lledó.
7
7
 
8
8
  require_relative "client"
@@ -21,13 +21,15 @@ module Async
21
21
  #
22
22
  # @property endpoints [Array(Endpoint)] The list of sentinel endpoints.
23
23
  # @property master_name [String] The name of the master instance, defaults to 'mymaster'.
24
+ # @property master_options [Hash] Connection options for master instances.
25
+ # @property slave_options [Hash] Connection options for slave instances (defaults to master_options if not specified).
24
26
  # @property role [Symbol] The role of the instance that you want to connect to, either `:master` or `:slave`.
25
- # @property protocol [Protocol] The protocol to use when connecting to the actual Redis server, defaults to {Protocol::RESP2}.
26
- def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, role: :master, protocol: Protocol::RESP2, **options)
27
+ def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, master_options: nil, slave_options: nil, role: :master, **options)
27
28
  @endpoints = endpoints
28
29
  @master_name = master_name
30
+ @master_options = master_options || {}
31
+ @slave_options = slave_options || @master_options
29
32
  @role = role
30
- @protocol = protocol
31
33
 
32
34
  # A cache of sentinel connections.
33
35
  @sentinels = {}
@@ -94,9 +96,11 @@ module Async
94
96
  end
95
97
  end
96
98
 
99
+ private
100
+
97
101
  # Resolve the master endpoint address.
98
102
  # @returns [Endpoint | Nil] The master endpoint or nil if not found.
99
- def resolve_master
103
+ def resolve_master(options = @master_options)
100
104
  sentinels do |client|
101
105
  begin
102
106
  address = client.call("SENTINEL", "GET-MASTER-ADDR-BY-NAME", @master_name)
@@ -104,7 +108,7 @@ module Async
104
108
  next
105
109
  end
106
110
 
107
- return Endpoint.remote(address[0], address[1]) if address
111
+ return Endpoint.for(nil, address[0], port: address[1], **options) if address
108
112
  end
109
113
 
110
114
  return nil
@@ -112,7 +116,7 @@ module Async
112
116
 
113
117
  # Resolve a slave endpoint address.
114
118
  # @returns [Endpoint | Nil] A slave endpoint or nil if not found.
115
- def resolve_slave
119
+ def resolve_slave(options = @slave_options)
116
120
  sentinels do |client|
117
121
  begin
118
122
  reply = client.call("SENTINEL", "SLAVES", @master_name)
@@ -124,16 +128,14 @@ module Async
124
128
  next if slaves.empty?
125
129
 
126
130
  slave = select_slave(slaves)
127
- return Endpoint.remote(slave["ip"], slave["port"])
131
+ return Endpoint.for(nil, slave["ip"], port: slave["port"], **options)
128
132
  end
129
133
 
130
134
  return nil
131
135
  end
132
136
 
133
- protected
134
-
135
137
  def assign_default_tags(tags)
136
- tags[:protocol] = @protocol.to_s
138
+ tags[:role] ||= @role
137
139
  end
138
140
 
139
141
  # Override the parent method. The only difference is that this one needs to resolve the master/slave address.
@@ -145,7 +147,7 @@ module Async
145
147
  peer = endpoint.connect
146
148
  stream = ::IO::Stream(peer)
147
149
 
148
- @protocol.client(stream)
150
+ endpoint.protocol.client(stream)
149
151
  end
150
152
  end
151
153
 
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Redis
8
- VERSION = "0.11.2"
8
+ VERSION = "0.13.0"
9
9
  end
10
10
  end
data/lib/async/redis.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2024, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
  # Copyright, 2020, by David Ortiz.
6
6
 
7
7
  require_relative "redis/version"
data/license.md CHANGED
@@ -12,7 +12,7 @@ Copyright, 2021, by Olle Jonsson.
12
12
  Copyright, 2021, by Troex Nevelin.
13
13
  Copyright, 2022, by Tim Willard.
14
14
  Copyright, 2022, by Gleb Sinyavskiy.
15
- Copyright, 2024, by Joan Lledó.
15
+ Copyright, 2024-2025, by Joan Lledó.
16
16
  Copyright, 2025, by Travis Bell.
17
17
 
18
18
  Permission is hereby granted, free of charge, to any person obtaining a copy
data/readme.md CHANGED
@@ -14,10 +14,34 @@ Please see the [project documentation](https://socketry.github.io/async-redis/)
14
14
 
15
15
  - [Getting Started](https://socketry.github.io/async-redis/guides/getting-started/index) - This guide explains how to use the `async-redis` gem to connect to a Redis server and perform basic operations.
16
16
 
17
+ - [Transactions and Pipelines](https://socketry.github.io/async-redis/guides/transactions-and-pipelines/index) - This guide explains how to use Redis transactions and pipelines with `async-redis` for atomic operations and improved performance.
18
+
19
+ - [Subscriptions](https://socketry.github.io/async-redis/guides/subscriptions/index) - This guide explains how to use Redis pub/sub functionality with `async-redis` to publish and subscribe to messages.
20
+
21
+ - [Data Structures and Operations](https://socketry.github.io/async-redis/guides/data-structures/index) - This guide explains how to work with Redis data types and operations using `async-redis`.
22
+
23
+ - [Streams](https://socketry.github.io/async-redis/guides/streams/index) - This guide explains how to use Redis streams with `async-redis` for reliable message processing and event sourcing.
24
+
25
+ - [Scripting](https://socketry.github.io/async-redis/guides/scripting/index) - This guide explains how to use Redis Lua scripting with `async-redis` for atomic operations and advanced data processing.
26
+
27
+ - [Client Architecture](https://socketry.github.io/async-redis/guides/client-architecture/index) - This guide explains the different client types available in `async-redis` and when to use each one.
28
+
17
29
  ## Releases
18
30
 
19
31
  Please see the [project releases](https://socketry.github.io/async-redis/releases/index) for all releases.
20
32
 
33
+ ### v0.13.0
34
+
35
+ - Fix password with special characters when using sentinels.
36
+ - Fix support for UNIX domain socket endpoints. You can now create Unix socket endpoints using `Async::Redis::Endpoint.unix("/path/to/socket.sock")` or parse them from URLs like `redis:/path/to/socket.sock`.
37
+
38
+ ### v0.12.0
39
+
40
+ - Add agent context.
41
+ - Add support for pattern pub/sub.
42
+ - Add support for sharded pub/sub.
43
+ - Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
44
+
21
45
  ### v0.11.2
22
46
 
23
47
  - Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
data/releases.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Releases
2
2
 
3
+ ## v0.13.0
4
+
5
+ - Fix password with special characters when using sentinels.
6
+ - Fix support for UNIX domain socket endpoints. You can now create Unix socket endpoints using `Async::Redis::Endpoint.unix("/path/to/socket.sock")` or parse them from URLs like `redis:/path/to/socket.sock`.
7
+
8
+ ## v0.12.0
9
+
10
+ - Add agent context.
11
+ - Add support for pattern pub/sub.
12
+ - Add support for sharded pub/sub.
13
+ - Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
14
+
3
15
  ## v0.11.2
4
16
 
5
17
  - Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
data.tar.gz.sig CHANGED
Binary file