valkey-glide-rb 0.9.2 → 1.0.0.pre.rc2

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: 3637e780cfd586be7915d5a1bcd9993ce5772ca69afdee93e5ec1166f6f1fda6
4
+ data.tar.gz: 45c92cf3741eb914fadf412ae9cb26223133aebdce833045a935219fb4c79f7b
5
5
  SHA512:
6
- metadata.gz: e96a09b0077bd54a30722201d03ccd8bb8baf7a47c0005d5c3d87441bfeb015479181548dce68bd1e882ee9e266be4658e68b2562e32dca6c48c101ca4e0e2cc
7
- data.tar.gz: dbee972964d0a2e54dbf7f4916c2bab6427525ace1d97a679845409b9d55390419631c8407df7502ca9b3561bd51ea0bba1ebecd8bbef680cdaa45b5ba28f405
6
+ metadata.gz: 730a56e57f83549ea454af827d121461961858ca629a03dbdb723808845e718afe27db1ffc901564d393e070d496b2ecd16736c22a3d37461388e26430350d2c
7
+ data.tar.gz: aef8450541af162b9c38149589a6eccb45ece28005c2f1af26a5bd93e1639e3a2126c208276d72f72c7e9bd6b1fb1b81288ddb11d40ae38a17d612b9a9c24e3a
data/AGENTS.md CHANGED
@@ -83,10 +83,26 @@ RUBYOPT="-I$(pwd)/lib" ruby -r valkey -e 'p Valkey.new.ping'
83
83
  | Suite | Server requirement |
84
84
  |-------|-------------------|
85
85
  | `test:standalone` | Standalone Valkey/Redis on `localhost:6379` (DB 15) |
86
- | `test:cluster` | 6-node cluster on `127.0.0.1:7000`–`7005` |
87
- | SSL tests | TLS Valkey on port `6380` + certs in `test/fixtures/ssl/` |
86
+ | `test:cluster` | 6-node cluster on `127.0.0.1:7000`–`7005` (auto-started by the suite) |
87
+ | SSL tests | TLS Valkey on port `6380` + `export TLS_CERT_DIR=...` (or `SKIP_TLS_TESTS=true`) |
88
88
  | Module tests | JSON, Bloom, Search modules loaded (see CI workflow) |
89
89
 
90
+ Start test servers with `cluster_manager.py` (matching CI). The cluster is
91
+ auto-started by the suite; standalone and TLS are started manually:
92
+
93
+ ```bash
94
+ # Standalone on :6379
95
+ python3 valkey-glide/utils/cluster_manager.py start -r 0 -p 6379 --prefix standalone
96
+
97
+ # TLS on :6380 (generates certs in valkey-glide/utils/tls_crts/)
98
+ python3 valkey-glide/utils/cluster_manager.py --tls start -r 0 -p 6380 --prefix tls-standalone
99
+ export TLS_CERT_DIR=$(pwd)/valkey-glide/utils/tls_crts # required for TLS tests
100
+
101
+ # Stop when done
102
+ python3 valkey-glide/utils/cluster_manager.py stop --prefix standalone
103
+ python3 valkey-glide/utils/cluster_manager.py --tls stop --prefix tls-standalone
104
+ ```
105
+
90
106
  ### Rebuild Native FFI (when changing glide-core)
91
107
 
92
108
  ```bash
@@ -141,7 +157,7 @@ cargo fmt --manifest-path ./Cargo.toml --all
141
157
  - `*.gem` — built gem packages
142
158
  - `coverage/` — coverage reports
143
159
  - `tmp/`, `test/tmp/` — temporary test artifacts
144
- - Regenerated SSL certs unless intentionally updated (`test/fixtures/ssl/*.pem` may be committed for CI)
160
+ - SSL certs never commit them; TLS tests read certs from `TLS_CERT_DIR` (generated by `cluster_manager.py --tls`)
145
161
  - Wrong-platform `libglide_ffi` binaries (build per OS/arch)
146
162
 
147
163
  ### Ruby-Specific Rules
@@ -185,7 +201,7 @@ valkey-glide-ruby/
185
201
  ├── test/lint/ # shared lint
186
202
  ├── valkey.gemspec
187
203
  ├── Rakefile
188
- └── .github/workflows/CI.yml
204
+ └── .github/workflows/ci.yml
189
205
  ```
190
206
 
191
207
  ## Quality Gates (Agent Checklist)
data/CONTRIBUTING.md CHANGED
@@ -39,7 +39,7 @@ For issues that affect the shared Rust core or other language clients, consider
39
39
  ```
40
40
  Configure automatic signoff: `git config --global format.signOff true`
41
41
 
42
- 5. Open a PR and respond to CI feedback (RuboCop + test matrix in `.github/workflows/CI.yml`).
42
+ 5. Open a PR and respond to CI feedback (RuboCop + test matrix in `.github/workflows/ci.yml`).
43
43
 
44
44
  GitHub guides: [fork a repo](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo), [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
45
45
 
data/DEVELOPER.md CHANGED
@@ -43,7 +43,7 @@ valkey-glide-ruby/
43
43
  │ ├── setup # bundle install
44
44
  │ └── console # IRB with gem loaded
45
45
  ├── .github/workflows/
46
- │ ├── CI.yml # RuboCop + test matrix
46
+ │ ├── ci.yml # RuboCop + test matrix
47
47
  │ └── cd.yml # Build and publish gem
48
48
  ├── valkey.gemspec
49
49
  ├── Gemfile
@@ -226,13 +226,14 @@ CI=1 bundle exec rake test:standalone
226
226
 
227
227
  ### SSL Tests
228
228
 
229
- The preferred approach (matching CI and all other GLIDE clients) uses `cluster_manager.py`:
229
+ TLS tests require a TLS server plus `TLS_CERT_DIR` pointing at its certs. Use
230
+ `cluster_manager.py --tls` which will prepare a self-sign certs for use:
230
231
 
231
232
  ```bash
232
233
  # Start a TLS-only server on port 6380 (generates certs in valkey-glide/utils/tls_crts/)
233
234
  python3 valkey-glide/utils/cluster_manager.py --tls start -r 0 -p 6380 --prefix tls-standalone
234
235
 
235
- # Point tests at the generated certs
236
+ # Point tests at the generated certs (required — the TLS tests raise if unset)
236
237
  export TLS_CERT_DIR=$(pwd)/valkey-glide/utils/tls_crts
237
238
 
238
239
  # Run tests
@@ -242,12 +243,8 @@ bundle exec rake test:standalone
242
243
  python3 valkey-glide/utils/cluster_manager.py --tls stop --prefix tls-standalone
243
244
  ```
244
245
 
245
- Alternatively, for local development without Python, generate certs and start manually:
246
-
247
- ```bash
248
- ruby test/fixtures/ssl/generate_certs.rb
249
- # Then start a TLS Valkey server on port 6380 using those certs
250
- ```
246
+ If you don't want to run the TLS tests, set `SKIP_TLS_TESTS=true` and they are
247
+ skipped gracefully (this is what macOS CI does).
251
248
 
252
249
  ### Module Tests (JSON, Bloom, Search)
253
250
 
@@ -270,7 +267,7 @@ VALKEY_PORT=6379 TIMEOUT=10 bundle exec rake test:standalone
270
267
  bundle exec rubocop
271
268
  ```
272
269
 
273
- CI runs RuboCop on every push and pull request (see `.github/workflows/CI.yml`).
270
+ CI runs RuboCop on every push and pull request (see `.github/workflows/ci.yml`).
274
271
 
275
272
  Auto-correct safe offenses:
276
273
 
@@ -350,7 +347,7 @@ bundle exec ruby test/valkey/test_opentelemetry.rb
350
347
 
351
348
  ## CI Overview
352
349
 
353
- GitHub Actions (`.github/workflows/CI.yml`):
350
+ GitHub Actions (`.github/workflows/ci.yml`):
354
351
 
355
352
  | Job | Matrix |
356
353
  |-----|--------|
@@ -443,7 +440,7 @@ ruby -e "require 'valkey'; c = Valkey.new; puts c.ping; c.close"
443
440
  | `LoadError` / FFI library not found | Confirm `lib/valkey/libglide_ffi.{so,dylib}` exists and matches your OS/arch. |
444
441
  | Wrong architecture after FFI rebuild | Rebuild `glide-ffi` on the target platform; do not copy Linux `.so` to macOS. |
445
442
  | Cluster tests flaky | Wait for `cluster_state:ok`; increase `TIMEOUT` env var. |
446
- | SSL test failures | Use `cluster_manager.py --tls` (see SSL Tests above), or regenerate local certs: `ruby test/fixtures/ssl/generate_certs.rb`. |
443
+ | SSL test failures / `TLS_CERT_DIR is not set` | Start a TLS server via `cluster_manager.py --tls` and `export TLS_CERT_DIR=...` (see SSL Tests above), or set `SKIP_TLS_TESTS=true`. |
447
444
  | Pipeline / MULTI crashes | Transaction commands in `pipelined` use sequential fallback by design. |
448
445
 
449
446
  ## Recommended Editor Extensions
@@ -70,7 +70,7 @@ class Valkey
70
70
  #
71
71
  # @param [Integer] protover Protocol version (2 or 3)
72
72
  # @param [Hash] options Optional parameters like AUTH, SETNAME
73
- # @return [Array] Server information as flat array (TODO: should be Hash for RESP3)
73
+ # @return [Hash] Server information (server, version, proto, id, mode, role, modules)
74
74
  def hello(protover = 3, **options)
75
75
  args = [protover]
76
76
 
@@ -209,7 +209,7 @@ class Valkey
209
209
  # - when `"RIGHT"` - the elements popped are those from the right of the list
210
210
  # @params count [Integer] a number of elements to pop
211
211
  #
212
- # @return [Array<String, Array<String, Float>>] list of popped elements or nil
212
+ # @return [Hash, nil] hash mapping key to popped elements, or nil on timeout
213
213
  def blmpop(timeout, *keys, modifier: "LEFT", count: nil)
214
214
  raise ArgumentError, "Pick either LEFT or RIGHT" unless %w[LEFT RIGHT].include?(modifier)
215
215
 
@@ -235,7 +235,7 @@ class Valkey
235
235
  # - when `"RIGHT"` - the elements popped are those from the right of the list
236
236
  # @params count [Integer] a number of elements to pop
237
237
  #
238
- # @return [Array<String, Array<String, Float>>] list of popped elements or nil
238
+ # @return [Hash, nil] hash mapping key to popped elements, or nil if no elements
239
239
  def lmpop(*keys, modifier: "LEFT", count: nil)
240
240
  raise ArgumentError, "Pick either LEFT or RIGHT" unless %w[LEFT RIGHT].include?(modifier)
241
241
 
@@ -168,7 +168,7 @@ class Valkey
168
168
  # # => ["channel1", 5, "channel2", 3]
169
169
  #
170
170
  # @param [Array<String>] channels the channels to check
171
- # @return [Array] channel names and subscriber counts
171
+ # @return [Hash] channel names mapped to subscriber counts
172
172
  #
173
173
  # @see https://valkey.io/commands/pubsub-numsub/
174
174
  def pubsub_numsub(*channels)
@@ -200,7 +200,7 @@ class Valkey
200
200
  # # => ["shard1", 2, "shard2", 1]
201
201
  #
202
202
  # @param [Array<String>] channels the shard channels to check
203
- # @return [Array] shard channel names and subscriber counts
203
+ # @return [Hash] shard channel names mapped to subscriber counts
204
204
  #
205
205
  # @see https://valkey.io/commands/pubsub-shardnumsub/
206
206
  def pubsub_shardnumsub(*channels)
@@ -137,22 +137,13 @@ class Valkey
137
137
  # @example Execute script that returns different data types
138
138
  # valkey.eval("return {1, 'hello', true, nil}")
139
139
  # # => [1, "hello", true, nil]
140
- # @example Positional form, matching redis-rb's eval(script, keys, argv)
141
- # valkey.eval("return KEYS[1] .. ARGV[1]", ["mykey"], ["myarg"])
142
- # # => "mykeynyarg"
143
140
  # Since the eval is not available in the rust backend
144
141
  # using the load and invoke script
145
- def eval(script, *rest, keys: nil, args: nil)
142
+ def eval(script, keys: [], args: [])
146
143
  # Validate script parameter
147
144
  raise ArgumentError, "script must be a string" unless script.is_a?(String)
148
145
  raise ArgumentError, "script cannot be empty" if script.empty?
149
146
 
150
- # Accept redis-rb's flexible positional form - eval(script, keys, argv)
151
- # - in addition to the keyword form, mirroring the same treatment
152
- # applied to evalsha (see that method for the full rationale).
153
- keys ||= rest[0] || []
154
- args ||= rest[1] || []
155
-
156
147
  # Validate and convert keys and args to strings
157
148
  begin
158
149
  keys = Array(keys).map(&:to_s)
@@ -190,22 +181,13 @@ class Valkey
190
181
  # rescue Valkey::CommandError => e
191
182
  # puts "Script not found: #{e.message}"
192
183
  # end
193
- # @example Positional form, matching redis-rb's evalsha(sha, keys, argv)
194
- # valkey.evalsha(sha, ["user"], ["123"])
195
- # # => "user:123"
196
184
  # Since evalsha is not available in rust backend
197
185
  # using invoke script
198
- def evalsha(sha, *rest, keys: nil, args: nil)
186
+ def evalsha(sha, keys: [], args: [])
199
187
  # Validate SHA1 hash parameter
200
188
  raise ArgumentError, "sha1 hash must be a string" unless sha.is_a?(String)
201
189
  raise ArgumentError, "sha1 hash must be a 40-character hexadecimal string" unless valid_sha1?(sha)
202
190
 
203
- # Accept redis-rb's flexible positional form - evalsha(sha, keys, argv)
204
- # - in addition to the keyword form, so code written against redis-rb's
205
- # API (e.g. redis_display_id) doesn't need to special-case this client.
206
- keys ||= rest[0] || []
207
- args ||= rest[1] || []
208
-
209
191
  # Validate and convert keys and args to strings
210
192
  begin
211
193
  keys = Array(keys).map(&:to_s)
@@ -271,8 +253,8 @@ class Valkey
271
253
  end
272
254
 
273
255
  def invoke_script(script, args: [], keys: [])
274
- arg_ptrs, arg_lens = build_command_args(args)
275
- keys_ptrs, keys_lens = build_command_args(keys)
256
+ arg_ptrs, arg_lens, _arg_bufs = build_command_args(args)
257
+ keys_ptrs, keys_lens, _keys_bufs = build_command_args(keys)
276
258
 
277
259
  route = ""
278
260
  route_buf = FFI::MemoryPointer.from_string(route)
@@ -565,7 +565,7 @@ class Valkey
565
565
  # @param [Hash] options optional parameters
566
566
  # - `:full => true`: return full information including entries
567
567
  # - `:count => Integer`: limit number of entries (requires :full)
568
- # @return [Array] stream information as flat array of key-value pairs
568
+ # @return [Hash] stream information as a hash of key-value pairs
569
569
  #
570
570
  # @example Get basic stream info
571
571
  # valkey.xinfo_stream("mystream")
@@ -129,14 +129,7 @@ class Valkey
129
129
  # @param [String] value
130
130
  # @return [Boolean] whether the key was set or not
131
131
  def setnx(key, value)
132
- # &Utils::Boolify (not glide-core's generic Boolean-coercion table) is
133
- # deliberately used here: glide-core's per-command coercion is keyed by
134
- # command name alone, so it can't distinguish this dedicated SETNX
135
- # RequestType from a raw `customCommand(["SETNX", ...])` call, which
136
- # other GLIDE bindings' existing contracts expect to keep returning a
137
- # plain 0/1 integer. Doing the conversion here keeps it scoped to this
138
- # one Ruby-level method - see hexists/hsetnx for the same pattern.
139
- send_command(RequestType::SET_NX, [key, value], &Utils::Boolify)
132
+ send_command(RequestType::SET_NX, [key, value])
140
133
  end
141
134
 
142
135
  # Set one or more values.
@@ -327,7 +320,7 @@ class Valkey
327
320
  # - `:idx => true`: Return the positions of the LCS
328
321
  # - `:min_match_len => Integer`: Minimum match length
329
322
  # - `:with_match_len => true`: Include match length in results
330
- # @return [String, Integer, Array] the LCS result based on options
323
+ # @return [String, Integer, Hash] the LCS result based on options (Hash when idx: true)
331
324
  def lcs(key1, key2, len: nil, idx: nil, min_match_len: nil, with_match_len: nil)
332
325
  args = [key1, key2]
333
326
  args << "LEN" if len
@@ -16,12 +16,8 @@ class Valkey
16
16
  # end # => ["OK", 6]
17
17
  #
18
18
  # @yield [multi] the commands that are called inside this block are cached
19
- # locally (no server round-trip per command) and sent to the server as a
20
- # single atomic batch once the block returns - GLIDE wraps them in a real
21
- # MULTI/EXEC transaction internally. If the block raises, nothing has been
22
- # sent to the server yet, so the exception simply propagates - there is no
23
- # transaction to discard.
24
- # @yieldparam [Valkey::Pipeline] multi collects the block's commands
19
+ # and written to the server upon returning from it
20
+ # @yieldparam [Valkey] multi `self`
25
21
  #
26
22
  # @return [Array<...>]
27
23
  # - an array with replies
@@ -30,12 +26,17 @@ class Valkey
30
26
  # @see #unwatch
31
27
  def multi
32
28
  if block_given?
33
- pipeline = Pipeline.new
34
- yield pipeline
35
-
36
- return [] if pipeline.commands.empty?
37
-
38
- send_batch_commands(pipeline.commands, exception: true, is_atomic: true)
29
+ begin
30
+ @in_multi_block = true
31
+ start_multi
32
+ yield(self)
33
+ exec
34
+ rescue StandardError
35
+ discard
36
+ raise
37
+ ensure
38
+ @in_multi_block = false
39
+ end
39
40
  else
40
41
  start_multi
41
42
  self
@@ -113,12 +114,11 @@ class Valkey
113
114
  # @see #discard
114
115
  def exec
115
116
  if @in_multi
116
- queued_commands = @queued_commands
117
117
  begin
118
118
  begin
119
119
  result = send_command(RequestType::EXEC)
120
120
  # If EXEC returns an error object (from array), it's already handled
121
- result.is_a?(Array) ? reconvert_queued_replies(result, queued_commands) : result
121
+ result
122
122
  rescue CommandError => e
123
123
  # If EXEC itself raises an error (like when transaction is aborted),
124
124
  # return an array with the error to match expected behavior in tests
@@ -157,59 +157,8 @@ class Valkey
157
157
  @queued_commands = []
158
158
  end
159
159
 
160
- # Commands whose reply is boolean when run standalone (via native return-type
161
- # coercion server-side), but arrives as a raw 0/1 integer inside an EXEC array,
162
- # since that coercion is keyed by the single command actually being run - which,
163
- # for a queued command, is EXEC itself, not the original command.
164
- #
165
- # This list mirrors glide-core's own Boolean-coercion table in
166
- # `value_conversion.rs::expected_type_for_cmd` (HEXISTS, HSETNX, EXPIRE, EXPIREAT,
167
- # PEXPIRE, PEXPIREAT, SISMEMBER, PERSIST, SMOVE, PFADD, RENAMENX, MOVE, COPY,
168
- # MSETNX, XGROUP DESTROY, XGROUP CREATECONSUMER) MINUS the commands whose Ruby
169
- # method already passes its own explicit conversion block (`hexists`/`hsetnx`
170
- # use `&Utils::Boolify`; `xgroup_destroy`/`xgroup_createconsumer` use a custom
171
- # bool->int block). Those are already handled correctly by the `if block`
172
- # branch below, before this list is even consulted - listing them here too
173
- # would be redundant, not wrong. Every RequestType below calls `send_command`
174
- # with NO block at all, so this static list is the only place their boolean-ness
175
- # is recorded.
176
- #
177
- # NOTE: SETNX is deliberately NOT in glide-core's coercion table (and so isn't
178
- # here as a "no block" entry either) - it's `redis-rb`-style boolean-ness only,
179
- # not something the server/`glide-core` treats as boolean, since coercion there
180
- # is keyed by command name alone and can't distinguish this dedicated RequestType
181
- # from a raw `customCommand(["SETNX", ...])` call, which other bindings' existing
182
- # contracts expect to keep returning a plain integer. `setnx`'s own Ruby method
183
- # passes `&Utils::Boolify` directly instead - see string_commands.rb - so it's
184
- # already covered by the `if block` branch, same as hexists/hsetnx.
185
- BOOLEAN_REQUEST_TYPES = [
186
- RequestType::EXPIRE, RequestType::EXPIRE_AT, RequestType::PEXPIRE, RequestType::PEXPIRE_AT,
187
- RequestType::PERSIST, RequestType::SISMEMBER, RequestType::S_MOVE, RequestType::PFADD,
188
- RequestType::RENAME_NX, RequestType::MOVE, RequestType::COPY, RequestType::MSET_NX
189
- ].freeze
190
-
191
160
  private
192
161
 
193
- # Re-applies each queued command's own reply conversion (e.g. `&Utils::Boolify`)
194
- # to EXEC's raw array, since redis-rb's Future-based design does the equivalent
195
- # (a Future remembers its conversion and re-applies it once EXEC resolves), but
196
- # queued commands here go through the plain single-command path with no such
197
- # memory - see BOOLEAN_REQUEST_TYPES above for the case with no explicit block.
198
- def reconvert_queued_replies(result, queued_commands)
199
- return result unless result.size == queued_commands.size
200
-
201
- result.each_with_index.map do |value, i|
202
- command_type, _args, block = queued_commands[i]
203
- if block
204
- block.call(value)
205
- elsif BOOLEAN_REQUEST_TYPES.include?(command_type)
206
- Utils::Boolify.call(value)
207
- else
208
- value
209
- end
210
- end
211
- end
212
-
213
162
  # Start a MULTI block if one isn't already active.
214
163
  #
215
164
  # This mirrors the behaviour of popular Valkey/Redis clients where
@@ -181,7 +181,7 @@ class Valkey
181
181
  # # => ["index_name", "myIndex", "fields", [...], ...]
182
182
  #
183
183
  # @param [String] index the index name
184
- # @return [Array] index information as array of key-value pairs
184
+ # @return [Hash] index information as a hash of key-value pairs
185
185
  #
186
186
  # @see https://redis.io/commands/ft.info/
187
187
  def ft_info(index)
data/lib/valkey/route.rb CHANGED
@@ -67,6 +67,8 @@ class Valkey
67
67
  # Returns both the struct and any pinned memory buffers that must remain
68
68
  # alive until the FFI call completes.
69
69
  #
70
+ # TODO: Refactor to return a struct (https://github.com/valkey-io/valkey-glide-ruby/issues/179)
71
+ #
70
72
  # @return [Array(Bindings::RouteInfo, Array)] the struct and pinned buffers
71
73
  def to_ffi
72
74
  info = Bindings::RouteInfo.new
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Valkey
4
- VERSION = "0.9.2"
4
+ VERSION = "1.0.0-rc2"
5
5
  end
data/lib/valkey.rb CHANGED
@@ -156,50 +156,8 @@ class Valkey
156
156
  # TLS/SSL certificates
157
157
  root_certs = []
158
158
  if options[:ssl_params].is_a?(Hash)
159
- # ca_file - read CA certificate file (PEM or DER format)
160
- if options[:ssl_params][:ca_file]
161
- ca_file = options[:ssl_params][:ca_file]
162
- raise ArgumentError, "CA file does not exist: #{ca_file}" unless File.exist?(ca_file)
163
- raise ArgumentError, "CA file is not readable: #{ca_file}" unless File.readable?(ca_file)
164
-
165
- root_certs << File.binread(ca_file)
166
- end
167
-
168
- # cert - client certificate (file path or OpenSSL::X509::Certificate)
169
- if options[:ssl_params][:cert]
170
- cert_data = if options[:ssl_params][:cert].is_a?(String)
171
- cert_file = options[:ssl_params][:cert]
172
- raise ArgumentError, "Cert file does not exist: #{cert_file}" unless File.exist?(cert_file)
173
- raise ArgumentError, "Cert file is not readable: #{cert_file}" unless File.readable?(cert_file)
174
-
175
- File.binread(cert_file)
176
- elsif options[:ssl_params][:cert].respond_to?(:to_pem)
177
- options[:ssl_params][:cert].to_pem
178
- elsif options[:ssl_params][:cert].respond_to?(:to_der)
179
- options[:ssl_params][:cert].to_der
180
- else
181
- options[:ssl_params][:cert].to_s
182
- end
183
- root_certs << cert_data
184
- end
185
-
186
- # key - client key (file path or OpenSSL::PKey)
187
- if options[:ssl_params][:key]
188
- key_data = if options[:ssl_params][:key].is_a?(String)
189
- key_file = options[:ssl_params][:key]
190
- raise ArgumentError, "Key file does not exist: #{key_file}" unless File.exist?(key_file)
191
- raise ArgumentError, "Key file is not readable: #{key_file}" unless File.readable?(key_file)
192
-
193
- File.binread(key_file)
194
- elsif options[:ssl_params][:key].respond_to?(:to_pem)
195
- options[:ssl_params][:key].to_pem
196
- elsif options[:ssl_params][:key].respond_to?(:to_der)
197
- options[:ssl_params][:key].to_der
198
- else
199
- options[:ssl_params][:key].to_s
200
- end
201
- root_certs << key_data
202
- end
159
+ # ca_file - CA certificate file path
160
+ root_certs << read_ssl_value(options[:ssl_params][:ca_file], "CA") if options[:ssl_params][:ca_file]
203
161
 
204
162
  # Additional root certificates from ca_path
205
163
  if options[:ssl_params][:ca_path]
@@ -214,6 +172,12 @@ class Valkey
214
172
  end
215
173
  end
216
174
 
175
+ # cert - file path or OpenSSL::X509::Certificate
176
+ json_options["client_cert"] = read_ssl_value(options[:ssl_params][:cert], "Cert") if options[:ssl_params][:cert]
177
+
178
+ # key - file path or OpenSSL::PKey
179
+ json_options["client_key"] = read_ssl_value(options[:ssl_params][:key], "Key") if options[:ssl_params][:key]
180
+
217
181
  # Direct root_certs array support
218
182
  root_certs.concat(options[:ssl_params][:root_certs]) if options[:ssl_params][:root_certs].is_a?(Array)
219
183
  end
@@ -291,6 +255,11 @@ class Valkey
291
255
  # Store cluster mode flag for response handling (MAP returns Hash in cluster, Array in standalone)
292
256
  @cluster_mode = options[:cluster_mode] ? true : false
293
257
 
258
+ # Returns GLIDE Core map as a flattened-map array
259
+ # Ex: { "key1" => "value1", "key2" => "value2" } becomes ["key1", "value1", "key2", "value2"]
260
+ # Compatibility for redis-rb 4.x
261
+ @flatten_map = options[:flatten_map] ? true : false
262
+
294
263
  # Track transactional state for `MULTI` / `EXEC` / `DISCARD` helpers.
295
264
  # This avoids Ruby warnings about uninitialised instance variables and
296
265
  # gives us a single source of truth for whether we're inside a TX.
@@ -435,7 +404,7 @@ class Valkey
435
404
  )
436
405
  end
437
406
 
438
- result = convert_response(res, return_map_as_hash: @cluster_mode, &block)
407
+ result = convert_response(res, &block)
439
408
  ensure
440
409
  # Free the native CommandResult (arena + response + error) to prevent memory leak
441
410
  Bindings.free_command_result(res) if res && !res.null?
@@ -457,9 +426,7 @@ class Valkey
457
426
  RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD,
458
427
  RequestType::WATCH, RequestType::UNWATCH
459
428
  ]
460
- if !tx_commands.include?(command_type) && result == "QUEUED"
461
- @queued_commands << [command_type, command_args.dup, block]
462
- end
429
+ @queued_commands << [command_type, command_args.dup] if !tx_commands.include?(command_type) && result == "QUEUED"
463
430
  end
464
431
 
465
432
  result
@@ -467,29 +434,41 @@ class Valkey
467
434
 
468
435
  private
469
436
 
470
- def send_batch_commands(commands, exception: true, is_atomic: false)
471
- # WORKAROUND: The underlying Glide FFI backend has stability issues when
472
- # batching LITERAL MULTI / EXEC / DISCARD commands (e.g. a `pipelined` block
473
- # that manually calls `pipeline.multi`/`pipeline.exec`). To avoid native
474
- # crashes we fall back to issuing those commands sequentially instead of via
475
- # `Bindings.batch`. This never applies to a real `is_atomic: true` batch
476
- # (see `multi`'s block form) - that path never contains literal MULTI/EXEC
477
- # commands, since the server-side transaction wrapping is handled by GLIDE
478
- # itself based on the `is_atomic` flag, not by commands in the list.
479
- unless is_atomic
480
- tx_types = [RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD]
481
-
482
- if commands.any? { |(command_type, _args, _block)| tx_types.include?(command_type) }
483
- results = []
484
-
485
- commands.each do |command_type, command_args, block|
486
- res = send_command(command_type, command_args)
487
- res = block.call(res) if block
488
- results << res
489
- end
437
+ # Read an SSL value
438
+ # Accepts a file path (String), an OpenSSL object (#to_pem / #to_der), or a fallback #to_s.
439
+ def read_ssl_value(value, label)
440
+ if value.is_a?(String)
441
+ raise ArgumentError, "#{label} file does not exist: #{value}" unless File.exist?(value)
442
+ raise ArgumentError, "#{label} file is not readable: #{value}" unless File.readable?(value)
443
+
444
+ File.binread(value)
445
+ # Duck-typing check
446
+ elsif value.respond_to?(:to_pem)
447
+ value.to_pem
448
+ elsif value.respond_to?(:to_der)
449
+ value.to_der
450
+ else
451
+ value.to_s
452
+ end
453
+ end
490
454
 
491
- return results
455
+ def send_batch_commands(commands, exception: true)
456
+ # WORKAROUND: The underlying Glide FFI backend has stability issues when
457
+ # batching transactional commands like MULTI / EXEC / DISCARD. To avoid
458
+ # native crashes we fall back to issuing those commands sequentially
459
+ # instead of via `Bindings.batch`.
460
+ tx_types = [RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD]
461
+
462
+ if commands.any? { |(command_type, _args, _block)| tx_types.include?(command_type) }
463
+ results = []
464
+
465
+ commands.each do |command_type, command_args, block|
466
+ res = send_command(command_type, command_args)
467
+ res = block.call(res) if block
468
+ results << res
492
469
  end
470
+
471
+ return results
493
472
  end
494
473
 
495
474
  cmds = []
@@ -519,7 +498,7 @@ class Valkey
519
498
  batch_info = Bindings::BatchInfo.new
520
499
  batch_info[:cmd_count] = cmds.size
521
500
  batch_info[:cmds] = cmd_ptrs
522
- batch_info[:is_atomic] = is_atomic
501
+ batch_info[:is_atomic] = false
523
502
 
524
503
  batch_options = Bindings::BatchOptionsInfo.new
525
504
  batch_options[:retry_server_error] = true
@@ -601,6 +580,9 @@ class Valkey
601
580
  { "manual_interval" => { "duration_in_sec" => duration_in_sec } }
602
581
  end
603
582
 
583
+ # Returns [ptrs, lens, buffers]. Callers must retain buffers until the FFI call returns.
584
+ # Otherwise, GC may free the buffers making the ptrs invalid.
585
+ # TODO: Refactor to return a struct (https://github.com/valkey-io/valkey-glide-ruby/issues/179)
604
586
  def build_command_args(command_args)
605
587
  # For empty arrays, pass NULL pointers as per Rust FFI contract
606
588
  # This matches Go's approach which successfully uses nil pointers
@@ -622,7 +604,7 @@ class Valkey
622
604
  [arg_ptrs, arg_lens, buffers]
623
605
  end
624
606
 
625
- def convert_response(res, return_map_as_hash: false, &block)
607
+ def convert_response(res, &block)
626
608
  result = Bindings::CommandResult.new(res)
627
609
 
628
610
  if result[:response].null?
@@ -678,8 +660,7 @@ class Valkey
678
660
  map[map_key] = map_value
679
661
  end
680
662
 
681
- # Return as Hash in cluster mode, flatten to pairs in standalone mode.
682
- return_map_as_hash ? map : map.to_a.flatten(1)
663
+ @flatten_map ? map.to_a.flatten(1) : map
683
664
  when ResponseType::SETS
684
665
  ptr = response_item[:sets_value]
685
666
  count = response_item[:sets_value_len].to_i
@@ -708,10 +689,7 @@ class Valkey
708
689
 
709
690
  response = convert_response.call(result)
710
691
 
711
- # Don't run the caller's converter (e.g. Utils::Boolify) over the MULTI-queued
712
- # "QUEUED" sentinel - send_command's own `result == "QUEUED"` check (used to
713
- # track queued commands) needs to see the literal string, not e.g. `true`.
714
- if block_given? && response != "QUEUED"
692
+ if block_given?
715
693
  block.call(response)
716
694
  else
717
695
  response
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: 1.0.0.pre.rc2
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-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi