hotkv 1.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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/NOTICE +6 -0
  4. data/README.md +459 -0
  5. data/SECURITY.md +30 -0
  6. data/lib/hotkv/ai/call_result.rb +25 -0
  7. data/lib/hotkv/ai/prompt_cache.rb +135 -0
  8. data/lib/hotkv/ai/semantic_cache.rb +66 -0
  9. data/lib/hotkv/client.rb +201 -0
  10. data/lib/hotkv/cluster/crc16.rb +43 -0
  11. data/lib/hotkv/cluster.rb +184 -0
  12. data/lib/hotkv/commands/admin.rb +36 -0
  13. data/lib/hotkv/commands/all.rb +49 -0
  14. data/lib/hotkv/commands/bitmaps.rb +52 -0
  15. data/lib/hotkv/commands/geo.rb +146 -0
  16. data/lib/hotkv/commands/hashes.rb +169 -0
  17. data/lib/hotkv/commands/hyperloglog.rb +21 -0
  18. data/lib/hotkv/commands/keys.rb +170 -0
  19. data/lib/hotkv/commands/lists.rb +113 -0
  20. data/lib/hotkv/commands/probabilistic.rb +23 -0
  21. data/lib/hotkv/commands/publishing.rb +46 -0
  22. data/lib/hotkv/commands/scan.rb +82 -0
  23. data/lib/hotkv/commands/scripting.rb +128 -0
  24. data/lib/hotkv/commands/server.rb +175 -0
  25. data/lib/hotkv/commands/sets.rb +75 -0
  26. data/lib/hotkv/commands/sorted_sets.rb +258 -0
  27. data/lib/hotkv/commands/streams.rb +207 -0
  28. data/lib/hotkv/commands/strings.rb +178 -0
  29. data/lib/hotkv/commands/support.rb +69 -0
  30. data/lib/hotkv/connection.rb +221 -0
  31. data/lib/hotkv/errors.rb +145 -0
  32. data/lib/hotkv/extensions.rb +118 -0
  33. data/lib/hotkv/future.rb +55 -0
  34. data/lib/hotkv/llm_price.rb +53 -0
  35. data/lib/hotkv/namespaces/admin.rb +261 -0
  36. data/lib/hotkv/namespaces/amem.rb +172 -0
  37. data/lib/hotkv/namespaces/caching.rb +397 -0
  38. data/lib/hotkv/namespaces/core.rb +190 -0
  39. data/lib/hotkv/namespaces/ops.rb +166 -0
  40. data/lib/hotkv/namespaces/probabilistic.rb +180 -0
  41. data/lib/hotkv/namespaces/rag.rb +63 -0
  42. data/lib/hotkv/pipeline.rb +57 -0
  43. data/lib/hotkv/pool.rb +123 -0
  44. data/lib/hotkv/pubsub.rb +60 -0
  45. data/lib/hotkv/resp.rb +176 -0
  46. data/lib/hotkv/transaction.rb +115 -0
  47. data/lib/hotkv/url.rb +100 -0
  48. data/lib/hotkv/usage.rb +42 -0
  49. data/lib/hotkv/version.rb +7 -0
  50. data/lib/hotkv/wire.rb +145 -0
  51. data/lib/hotkv.rb +28 -0
  52. metadata +137 -0
@@ -0,0 +1,113 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # List commands.
7
+ module Lists
8
+ def lpush(key, *values)
9
+ invoke("LPUSH", key, *values.flatten)
10
+ end
11
+
12
+ def rpush(key, *values)
13
+ invoke("RPUSH", key, *values.flatten)
14
+ end
15
+
16
+ def lpushx(key, *values)
17
+ invoke("LPUSHX", key, *values.flatten)
18
+ end
19
+
20
+ def rpushx(key, *values)
21
+ invoke("RPUSHX", key, *values.flatten)
22
+ end
23
+
24
+ # Without :count, returns the popped element or nil. With :count,
25
+ # returns an Array of popped elements (possibly empty) or nil when
26
+ # the key does not exist.
27
+ def lpop(key, count = nil)
28
+ count ? invoke("LPOP", key, count) : invoke("LPOP", key)
29
+ end
30
+
31
+ def rpop(key, count = nil)
32
+ count ? invoke("RPOP", key, count) : invoke("RPOP", key)
33
+ end
34
+
35
+ def llen(key)
36
+ invoke("LLEN", key)
37
+ end
38
+
39
+ def lrange(key, start, stop)
40
+ invoke("LRANGE", key, start, stop)
41
+ end
42
+
43
+ def lindex(key, index)
44
+ invoke("LINDEX", key, index)
45
+ end
46
+
47
+ def lset(key, index, value)
48
+ invoke("LSET", key, index, value) { |r| r == "OK" }
49
+ end
50
+
51
+ def linsert(key, where, pivot, value)
52
+ invoke("LINSERT", key, where, pivot, value)
53
+ end
54
+
55
+ def lrem(key, count, value)
56
+ invoke("LREM", key, count, value)
57
+ end
58
+
59
+ def ltrim(key, start, stop)
60
+ invoke("LTRIM", key, start, stop) { |r| r == "OK" }
61
+ end
62
+
63
+ def rpoplpush(source, destination)
64
+ invoke("RPOPLPUSH", source, destination)
65
+ end
66
+
67
+ def lmove(source, destination, wherefrom, whereto)
68
+ invoke("LMOVE", source, destination, wherefrom, whereto)
69
+ end
70
+
71
+ def lpos(key, element, rank: nil, count: nil, maxlen: nil)
72
+ args = ["LPOS", key, element]
73
+ args.push("RANK", rank) if rank
74
+ args.push("COUNT", count) if count
75
+ args.push("MAXLEN", maxlen) if maxlen
76
+ invoke(*args)
77
+ end
78
+
79
+ # -> [key, values] on success, or nil when every source list was empty.
80
+ def lmpop(keys, where, count: nil)
81
+ args = ["LMPOP", keys.length, *keys, where]
82
+ args.push("COUNT", count) if count
83
+ invoke(*args)
84
+ end
85
+
86
+ def blpop(*keys, timeout: 0)
87
+ invoke("BLPOP", *keys.flatten, timeout)
88
+ end
89
+
90
+ def brpop(*keys, timeout: 0)
91
+ invoke("BRPOP", *keys.flatten, timeout)
92
+ end
93
+
94
+ def blmove(source, destination, wherefrom, whereto, timeout: 0)
95
+ invoke("BLMOVE", source, destination, wherefrom, whereto, timeout)
96
+ end
97
+
98
+ # Pops from the first non-empty list of +keys+ (LMPOP) or waits up to
99
+ # +timeout+ seconds (BLMPOP). +where+ is "LEFT" or "RIGHT". Returns
100
+ # [key, [elements]] or nil on timeout.
101
+ def blmpop(keys, where, timeout: 0, count: nil)
102
+ args = ["BLMPOP", timeout, keys.length, *keys, where]
103
+ args.push("COUNT", count) if count
104
+ invoke(*args)
105
+ end
106
+
107
+ # Deprecated blocking RPOPLPUSH.
108
+ def brpoplpush(source, destination, timeout: 0)
109
+ invoke("BRPOPLPUSH", source, destination, timeout)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,23 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ require_relative "../namespaces/probabilistic"
5
+
6
+ module HotKV
7
+ module Commands
8
+ # Bloom filter, Count-Min Sketch and Top-K accessors.
9
+ module Probabilistic
10
+ def bf
11
+ @hotkv_ns_bf ||= Namespaces::BF.new(self)
12
+ end
13
+
14
+ def cms
15
+ @hotkv_ns_cms ||= Namespaces::CMS.new(self)
16
+ end
17
+
18
+ def topk
19
+ @hotkv_ns_topk ||= Namespaces::TopK.new(self)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # Publishing and Pub/Sub introspection. Subscribing is not here: it needs
7
+ # a connection of its own, see Client#subscribe.
8
+ module Publishing
9
+ # Publishes +message+ to +channel+. Returns the number of subscribers
10
+ # that received it.
11
+ def publish(channel, message)
12
+ invoke("PUBLISH", channel, message)
13
+ end
14
+
15
+ # Publishes +message+ to a shard channel (SPUBLISH).
16
+ def spublish(channel, message)
17
+ invoke("SPUBLISH", channel, message)
18
+ end
19
+
20
+ # Active channels, optionally only those matching the glob +pattern+.
21
+ def pubsub_channels(pattern = nil)
22
+ pattern.nil? ? invoke("PUBSUB", "CHANNELS") : invoke("PUBSUB", "CHANNELS", pattern)
23
+ end
24
+
25
+ # Hash of channel => subscriber count for the given channels.
26
+ def pubsub_numsub(*channels)
27
+ invoke("PUBSUB", "NUMSUB", *channels.flatten) { |r| r.is_a?(Hash) ? r : r.each_slice(2).to_h }
28
+ end
29
+
30
+ # Number of pattern subscriptions.
31
+ def pubsub_numpat
32
+ invoke("PUBSUB", "NUMPAT")
33
+ end
34
+
35
+ # Active shard channels, optionally only those matching +pattern+.
36
+ def pubsub_shardchannels(pattern = nil)
37
+ pattern.nil? ? invoke("PUBSUB", "SHARDCHANNELS") : invoke("PUBSUB", "SHARDCHANNELS", pattern)
38
+ end
39
+
40
+ # Hash of shard channel => subscriber count.
41
+ def pubsub_shardnumsub(*channels)
42
+ invoke("PUBSUB", "SHARDNUMSUB", *channels.flatten) { |r| r.is_a?(Hash) ? r : r.each_slice(2).to_h }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # SCAN family: one-shot cursor calls plus Enumerator based iterators
7
+ # that page through an entire key space, hash, set or sorted set.
8
+ module Scan
9
+ # -> [next_cursor, keys]. next_cursor is "0" once the scan completes.
10
+ def scan(cursor = "0", match: nil, count: nil, type: nil)
11
+ args = ["SCAN", cursor]
12
+ args.push("MATCH", match) if match
13
+ args.push("COUNT", count) if count
14
+ args.push("TYPE", type) if type
15
+ invoke(*args)
16
+ end
17
+
18
+ # Enumerator over every key, paging with SCAN until exhausted.
19
+ def scan_each(match: nil, count: nil, type: nil, &block)
20
+ return to_enum(:scan_each, match: match, count: count, type: type) unless block
21
+
22
+ each_scan_page(:scan, [], match: match, count: count, type: type) { |page| page.each(&block) }
23
+ end
24
+
25
+ def hscan(key, cursor = "0", match: nil, count: nil, novalues: false)
26
+ args = ["HSCAN", key, cursor]
27
+ args.push("MATCH", match) if match
28
+ args.push("COUNT", count) if count
29
+ args.push("NOVALUES") if novalues
30
+ invoke(*args)
31
+ end
32
+
33
+ def hscan_each(key, match: nil, count: nil, &block)
34
+ return to_enum(:hscan_each, key, match: match, count: count) unless block
35
+
36
+ each_scan_page(:hscan, [key], match: match, count: count) { |page| page.each_slice(2) { |(f, v)| yield f, v } }
37
+ end
38
+
39
+ def sscan(key, cursor = "0", match: nil, count: nil)
40
+ args = ["SSCAN", key, cursor]
41
+ args.push("MATCH", match) if match
42
+ args.push("COUNT", count) if count
43
+ invoke(*args)
44
+ end
45
+
46
+ def sscan_each(key, match: nil, count: nil, &block)
47
+ return to_enum(:sscan_each, key, match: match, count: count) unless block
48
+
49
+ each_scan_page(:sscan, [key], match: match, count: count) { |page| page.each(&block) }
50
+ end
51
+
52
+ def zscan(key, cursor = "0", match: nil, count: nil)
53
+ args = ["ZSCAN", key, cursor]
54
+ args.push("MATCH", match) if match
55
+ args.push("COUNT", count) if count
56
+ invoke(*args)
57
+ end
58
+
59
+ def zscan_each(key, match: nil, count: nil, &block)
60
+ return to_enum(:zscan_each, key, match: match, count: count) unless block
61
+
62
+ each_scan_page(:zscan, [key], match: match, count: count) do |page|
63
+ page.each_slice(2) { |(m, s)| yield m, Wire.parse_float(s) }
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def each_scan_page(method, prefix_args, match:, count:, type: nil, novalues: nil)
70
+ cursor = "0"
71
+ loop do
72
+ kwargs = { match: match, count: count }
73
+ kwargs[:type] = type if method == :scan
74
+ kwargs[:novalues] = novalues if method == :hscan && !novalues.nil?
75
+ cursor, page = public_send(method, *prefix_args, cursor, **kwargs)
76
+ yield page
77
+ break if cursor == "0"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,128 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ require "digest"
5
+
6
+ module HotKV
7
+ module Commands
8
+ # Lua scripting commands.
9
+ module Scripting
10
+ def eval_script(script, keys: [], args: [])
11
+ invoke("EVAL", script, keys.length, *keys, *args)
12
+ end
13
+
14
+ # Runs EVALSHA for +sha+ (or Digest::SHA1.hexdigest(script) when
15
+ # :script is given instead of :sha); on NOSCRIPT, falls back to a
16
+ # plain EVAL of :script if one was given. The fallback only fires
17
+ # for an immediate call (HotKV::Client, inside a #watch block); a
18
+ # queued Pipeline/Transaction call cannot inspect its own reply
19
+ # before the batch runs, so there it behaves like a plain EVALSHA.
20
+ def evalsha(sha = nil, script: nil, keys: [], args: [])
21
+ sha ||= Digest::SHA1.hexdigest(script)
22
+ invoke("EVALSHA", sha, keys.length, *keys, *args)
23
+ rescue Error::NoScriptError
24
+ raise unless script
25
+
26
+ invoke("EVAL", script, keys.length, *keys, *args)
27
+ end
28
+
29
+ def script_load(script)
30
+ invoke("SCRIPT", "LOAD", script)
31
+ end
32
+
33
+ def script_exists(*shas)
34
+ invoke("SCRIPT", "EXISTS", *shas.flatten) { |r| r.map { |v| v == 1 } }
35
+ end
36
+
37
+ def script_flush
38
+ invoke("SCRIPT", "FLUSH") { |r| r == "OK" }
39
+ end
40
+
41
+ # Read-only EVAL (the script may not write).
42
+ def eval_ro(script, keys: [], args: [])
43
+ invoke("EVAL_RO", script, keys.length, *keys, *args)
44
+ end
45
+
46
+ # Read-only EVALSHA.
47
+ def evalsha_ro(sha, keys: [], args: [])
48
+ invoke("EVALSHA_RO", sha, keys.length, *keys, *args)
49
+ end
50
+
51
+ # Stops a running script (NOTBUSY if none is running).
52
+ def script_kill
53
+ invoke("SCRIPT", "KILL") { |r| r == "OK" }
54
+ end
55
+
56
+ # Calls a function registered by FUNCTION LOAD.
57
+ def fcall(function, keys: [], args: [])
58
+ invoke("FCALL", function, keys.length, *keys, *args)
59
+ end
60
+
61
+ # FCALL for a function flagged no-writes.
62
+ def fcall_ro(function, keys: [], args: [])
63
+ invoke("FCALL_RO", function, keys.length, *keys, *args)
64
+ end
65
+
66
+ # Loads a function library (`#!lua name=<lib>` header). Returns the
67
+ # library name. :replace overwrites an existing library of that name.
68
+ def function_load(code, replace: false)
69
+ replace ? invoke("FUNCTION", "LOAD", "REPLACE", code) : invoke("FUNCTION", "LOAD", code)
70
+ end
71
+
72
+ def function_delete(library)
73
+ invoke("FUNCTION", "DELETE", library) { |r| r == "OK" }
74
+ end
75
+
76
+ # Libraries as an Array of Hashes ("library_name", "engine",
77
+ # "functions" => [{"name", "description", "flags"}], plus "library_code"
78
+ # with :withcode).
79
+ def function_list(libraryname: nil, withcode: false)
80
+ args = ["FUNCTION", "LIST"]
81
+ args.push("LIBRARYNAME", libraryname) if libraryname
82
+ args.push("WITHCODE") if withcode
83
+ invoke(*args) { |r| r.map { |lib| lib.is_a?(Hash) ? lib : function_entry(lib) } }
84
+ end
85
+
86
+ # Deletes every library. +mode+ is "ASYNC" or "SYNC".
87
+ def function_flush(mode = nil)
88
+ mode ? invoke("FUNCTION", "FLUSH", mode) { |r| r == "OK" } : invoke("FUNCTION", "FLUSH") { |r| r == "OK" }
89
+ end
90
+
91
+ # Serialized libraries (binary String) for #function_restore.
92
+ def function_dump
93
+ invoke("FUNCTION", "DUMP")
94
+ end
95
+
96
+ # Restores a #function_dump payload. +policy+: "APPEND", "REPLACE" or "FLUSH".
97
+ def function_restore(payload, policy = nil)
98
+ args = ["FUNCTION", "RESTORE", payload]
99
+ args.push(policy) if policy
100
+ invoke(*args) { |r| r == "OK" }
101
+ end
102
+
103
+ # Hash with "running_script" and "engines".
104
+ def function_stats
105
+ invoke("FUNCTION", "STATS") { |r| r.is_a?(Hash) ? r : { "running_script" => r[1], "engines" => engines_hash(r[3]) } }
106
+ end
107
+
108
+ # Stops a running function.
109
+ def function_kill
110
+ invoke("FUNCTION", "KILL") { |r| r == "OK" }
111
+ end
112
+
113
+ private
114
+
115
+ def function_entry(flat)
116
+ entry = flat.each_slice(2).to_h
117
+ entry["functions"] = entry["functions"].map { |f| f.is_a?(Hash) ? f : f.each_slice(2).to_h } if entry["functions"]
118
+ entry
119
+ end
120
+
121
+ def engines_hash(flat)
122
+ return flat if flat.is_a?(Hash)
123
+
124
+ flat.each_slice(2).to_h { |(name, stats)| [name, stats.is_a?(Hash) ? stats : stats.each_slice(2).to_h] }
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,175 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # Server and connection commands.
7
+ module Server
8
+ def ping(message = nil)
9
+ message ? invoke("PING", message) : invoke("PING")
10
+ end
11
+
12
+ def echo(message)
13
+ invoke("ECHO", message)
14
+ end
15
+
16
+ def info(section = nil)
17
+ section ? invoke("INFO", section) : invoke("INFO")
18
+ end
19
+
20
+ def dbsize
21
+ invoke("DBSIZE")
22
+ end
23
+
24
+ def flushdb(async: false)
25
+ args = ["FLUSHDB"]
26
+ args.push(async ? "ASYNC" : "SYNC")
27
+ invoke(*args) { |r| r == "OK" }
28
+ end
29
+
30
+ def flushall(async: false)
31
+ args = ["FLUSHALL"]
32
+ args.push(async ? "ASYNC" : "SYNC")
33
+ invoke(*args) { |r| r == "OK" }
34
+ end
35
+
36
+ def time
37
+ invoke("TIME") { |r| [Integer(r[0]), Integer(r[1])] }
38
+ end
39
+
40
+ def lastsave
41
+ invoke("LASTSAVE")
42
+ end
43
+
44
+ def config_get(parameter)
45
+ invoke("CONFIG", "GET", parameter) { |r| r.is_a?(Hash) ? r : r.each_slice(2).to_h }
46
+ end
47
+
48
+ def config_set(parameter, value)
49
+ invoke("CONFIG", "SET", parameter, value) { |r| r == "OK" }
50
+ end
51
+
52
+ def config_resetstat
53
+ invoke("CONFIG", "RESETSTAT") { |r| r == "OK" }
54
+ end
55
+
56
+ def client_setname(name)
57
+ invoke("CLIENT", "SETNAME", name) { |r| r == "OK" }
58
+ end
59
+
60
+ def client_getname
61
+ invoke("CLIENT", "GETNAME")
62
+ end
63
+
64
+ def client_id
65
+ invoke("CLIENT", "ID")
66
+ end
67
+
68
+ def client_info
69
+ invoke("CLIENT", "INFO")
70
+ end
71
+
72
+ def client_list
73
+ invoke("CLIENT", "LIST")
74
+ end
75
+
76
+ def select(db)
77
+ invoke("SELECT", db) { |r| r == "OK" }
78
+ end
79
+
80
+ def swapdb(index1, index2)
81
+ invoke("SWAPDB", index1, index2) { |r| r == "OK" }
82
+ end
83
+
84
+ # Replication role: ["master", offset, [[host, port, offset], ...]] or
85
+ # ["slave", host, port, state, offset].
86
+ def role
87
+ invoke("ROLE")
88
+ end
89
+
90
+ # Saves a snapshot now, blocking the server until done.
91
+ def save
92
+ invoke("SAVE") { |r| r == "OK" }
93
+ end
94
+
95
+ # Saves a snapshot in the background. Returns the status line.
96
+ def bgsave(schedule: false)
97
+ schedule ? invoke("BGSAVE", "SCHEDULE") : invoke("BGSAVE")
98
+ end
99
+
100
+ # Rewrites the append-only file in the background. Returns the status line.
101
+ def bgrewriteaof
102
+ invoke("BGREWRITEAOF")
103
+ end
104
+
105
+ # Slow log entries (the newest +count+, default 10).
106
+ def slowlog_get(count = nil)
107
+ count ? invoke("SLOWLOG", "GET", count) : invoke("SLOWLOG", "GET")
108
+ end
109
+
110
+ def slowlog_len
111
+ invoke("SLOWLOG", "LEN")
112
+ end
113
+
114
+ def slowlog_reset
115
+ invoke("SLOWLOG", "RESET") { |r| r == "OK" }
116
+ end
117
+
118
+ # Bytes used by the value at +key+ (nil if missing); :samples limits
119
+ # the nested elements sampled.
120
+ def memory_usage(key, samples: nil)
121
+ samples ? invoke("MEMORY", "USAGE", key, "SAMPLES", samples) : invoke("MEMORY", "USAGE", key)
122
+ end
123
+
124
+ # Number of commands the server knows.
125
+ def command_count
126
+ invoke("COMMAND", "COUNT")
127
+ end
128
+
129
+ # The keys +command+ (a full command line as separate arguments) touches.
130
+ def command_getkeys(*command)
131
+ invoke("COMMAND", "GETKEYS", *command.flatten)
132
+ end
133
+
134
+ # Suspends client command processing for +milliseconds+ on the whole
135
+ # server. +mode+ is "WRITE" or "ALL". Lift it with #client_unpause.
136
+ def client_pause(milliseconds, mode = nil)
137
+ mode ? invoke("CLIENT", "PAUSE", milliseconds, mode) { |r| r == "OK" } : invoke("CLIENT", "PAUSE", milliseconds) { |r| r == "OK" }
138
+ end
139
+
140
+ def client_unpause
141
+ invoke("CLIENT", "UNPAUSE") { |r| r == "OK" }
142
+ end
143
+
144
+ # Marks the pooled connection that runs it as exempt from (or subject
145
+ # to) client eviction. +enabled+ true/false. Applies to one pooled
146
+ # connection only, so it is mostly useful on a pool_size 1 client.
147
+ def client_no_evict(enabled)
148
+ invoke("CLIENT", "NO-EVICT", enabled ? "ON" : "OFF") { |r| r == "OK" }
149
+ end
150
+
151
+ # As #client_no_evict, for the LRU/LFU touch on reads.
152
+ def client_no_touch(enabled)
153
+ invoke("CLIENT", "NO-TOUCH", enabled ? "ON" : "OFF") { |r| r == "OK" }
154
+ end
155
+
156
+ # CLIENT SETINFO LIB-NAME / LIB-VER on the pooled connection that runs it.
157
+ def client_setinfo(attribute, value)
158
+ invoke("CLIENT", "SETINFO", attribute, value) { |r| r == "OK" }
159
+ end
160
+
161
+ # Unblocks the client blocked with id +client_id+. +reason+ is
162
+ # "TIMEOUT" or "ERROR". Returns true when a client was unblocked.
163
+ def client_unblock(client_id, reason = nil)
164
+ args = ["CLIENT", "UNBLOCK", client_id]
165
+ args.push(reason) if reason
166
+ invoke(*args) { |r| r == 1 }
167
+ end
168
+
169
+ # Closes the connection with id +client_id+. Returns the number closed.
170
+ def client_kill_id(client_id)
171
+ invoke("CLIENT", "KILL", "ID", client_id)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # Set commands.
7
+ module Sets
8
+ def sadd(key, *members)
9
+ invoke("SADD", key, *members.flatten)
10
+ end
11
+
12
+ def srem(key, *members)
13
+ invoke("SREM", key, *members.flatten)
14
+ end
15
+
16
+ def smembers(key)
17
+ invoke("SMEMBERS", key)
18
+ end
19
+
20
+ def sismember(key, member)
21
+ invoke("SISMEMBER", key, member) { |r| Support.truthy(r) }
22
+ end
23
+
24
+ def smismember(key, *members)
25
+ invoke("SMISMEMBER", key, *members.flatten) { |r| r.map { |m| Support.truthy(m) } }
26
+ end
27
+
28
+ def scard(key)
29
+ invoke("SCARD", key)
30
+ end
31
+
32
+ def spop(key, count = nil)
33
+ count ? invoke("SPOP", key, count) : invoke("SPOP", key)
34
+ end
35
+
36
+ def srandmember(key, count = nil)
37
+ count ? invoke("SRANDMEMBER", key, count) : invoke("SRANDMEMBER", key)
38
+ end
39
+
40
+ def sinter(*keys)
41
+ invoke("SINTER", *keys.flatten)
42
+ end
43
+
44
+ def sunion(*keys)
45
+ invoke("SUNION", *keys.flatten)
46
+ end
47
+
48
+ def sdiff(*keys)
49
+ invoke("SDIFF", *keys.flatten)
50
+ end
51
+
52
+ def sintercard(keys, limit: nil)
53
+ args = ["SINTERCARD", keys.length, *keys]
54
+ args.push("LIMIT", limit) if limit
55
+ invoke(*args)
56
+ end
57
+
58
+ def sinterstore(destination, *keys)
59
+ invoke("SINTERSTORE", destination, *keys.flatten)
60
+ end
61
+
62
+ def sunionstore(destination, *keys)
63
+ invoke("SUNIONSTORE", destination, *keys.flatten)
64
+ end
65
+
66
+ def sdiffstore(destination, *keys)
67
+ invoke("SDIFFSTORE", destination, *keys.flatten)
68
+ end
69
+
70
+ def smove(source, destination, member)
71
+ invoke("SMOVE", source, destination, member) { |r| r == 1 }
72
+ end
73
+ end
74
+ end
75
+ end