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,397 @@
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 Namespaces
6
+ # SCACHE.* (semantic cache; needs a license that includes the
7
+ # SemanticCache feature). See also HotKV::AI::SemanticCache for the higher
8
+ # level, provider neutral helper built on this.
9
+ class SCache
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ def create(ns, dims, metric: nil, threshold: nil, ttl: nil)
15
+ args = ["SCACHE.CREATE", ns, dims]
16
+ args.push("METRIC", metric) if metric
17
+ args.push("THRESHOLD", Wire.finite(threshold)) if threshold
18
+ args.push("TTL", ttl) if ttl
19
+ @client.invoke(*args) { |r| r == "OK" }
20
+ end
21
+
22
+ # +usage+ (a HotKV::TokenUsage), if given, records the original
23
+ # (uncached) call's token usage so a later hit attributes it (and
24
+ # its cost, once the model is priced) to #stats's usage_saved_*
25
+ # counters.
26
+ def set(ns, query_text, response, vector, ttl: nil, usage: nil)
27
+ args = ["SCACHE.SET", ns, query_text, response, *Wire.finite_vector(vector)]
28
+ args.push("TTL", ttl) if ttl
29
+ args.concat(usage.wire_args) if usage
30
+ @client.invoke(*args) { |r| r == "OK" }
31
+ end
32
+
33
+ def get(ns, vector, threshold: nil)
34
+ args = ["SCACHE.GET", ns, *Wire.finite_vector(vector)]
35
+ args.push("THRESHOLD", Wire.finite(threshold)) if threshold
36
+ @client.invoke(*args) { |r| r.nil? ? nil : { response: r[0], similarity: Wire.parse_float(r[1]) } }
37
+ end
38
+
39
+ # entries, hits, misses, hit_rate, p50_us and the usage_saved_*
40
+ # savings counters for +ns+.
41
+ def stats(ns)
42
+ @client.invoke("SCACHE.STATS", ns) do |r|
43
+ Commands::Support.pairs_to_h(r, float_keys: %i[hit_rate usage_saved_cost_usd])
44
+ end
45
+ end
46
+
47
+ def flush(ns)
48
+ @client.invoke("SCACHE.FLUSH", ns) { |r| r == 1 }
49
+ end
50
+
51
+ def drop(ns)
52
+ @client.invoke("SCACHE.DROP", ns) { |r| r == 1 }
53
+ end
54
+
55
+ def list
56
+ @client.invoke("SCACHE.LIST")
57
+ end
58
+
59
+ def threshold(ns, value)
60
+ @client.invoke("SCACHE.THRESHOLD", ns, Wire.finite(value)) { |r| r == "OK" }
61
+ end
62
+ end
63
+
64
+ # PCACHE.* (prompt/LLM response cache; needs the PromptCache license
65
+ # feature). See also HotKV::AI::PromptCache for the higher level helper.
66
+ class PCache
67
+ def initialize(client)
68
+ @client = client
69
+ end
70
+
71
+ def create(ns, max_entries: nil, ttl: nil, cost_per_hit: nil)
72
+ args = ["PCACHE.CREATE", ns]
73
+ args.push("MAX_ENTRIES", max_entries) if max_entries
74
+ args.push("TTL", ttl) if ttl
75
+ args.push("COST_PER_HIT", Wire.finite(cost_per_hit)) if cost_per_hit
76
+ @client.invoke(*args) { |r| r == "OK" }
77
+ end
78
+
79
+ # +usage+ (a HotKV::TokenUsage), if given, records the original
80
+ # (uncached) call's token usage so a later hit attributes it (and
81
+ # its cost, once the model is priced) to #stats's usage_saved_*
82
+ # counters.
83
+ def set(ns, prompt, response, ttl: nil, meta: nil, usage: nil)
84
+ args = ["PCACHE.SET", ns, prompt, response]
85
+ args.push("TTL", ttl) if ttl
86
+ args.push("META", meta) if meta
87
+ args.concat(usage.wire_args) if usage
88
+ @client.invoke(*args) { |r| r == "OK" }
89
+ end
90
+
91
+ def get(ns, prompt)
92
+ @client.invoke("PCACHE.GET", ns, prompt)
93
+ end
94
+
95
+ def getex(ns, prompt)
96
+ @client.invoke("PCACHE.GETEX", ns, prompt) do |r|
97
+ r.nil? ? nil : { response: r[0], metadata: r[1], hit_count: r[2] }
98
+ end
99
+ end
100
+
101
+ def del(ns, prompt)
102
+ @client.invoke("PCACHE.DEL", ns, prompt) { |r| r == 1 }
103
+ end
104
+
105
+ # entry/hit/miss/eviction/latency/cost statistics for +ns+, including
106
+ # the usage_saved_* savings counters.
107
+ def stats(ns)
108
+ @client.invoke("PCACHE.STATS", ns) do |r|
109
+ Commands::Support.pairs_to_h(r, float_keys: %i[hit_rate cost_saved_usd usage_saved_cost_usd])
110
+ end
111
+ end
112
+
113
+ def flush(ns)
114
+ @client.invoke("PCACHE.FLUSH", ns)
115
+ end
116
+
117
+ def drop(ns)
118
+ @client.invoke("PCACHE.DROP", ns) { |r| r == 1 }
119
+ end
120
+
121
+ def list
122
+ @client.invoke("PCACHE.LIST")
123
+ end
124
+
125
+ def config_set(ns, key, value)
126
+ @client.invoke("PCACHE.CONFIG", ns, "SET", key, value) { |r| r == "OK" }
127
+ end
128
+ end
129
+
130
+ # Shared engine behind TOOL.CACHE.*, NCACHE.*, GUARD.* and RERANK.*:
131
+ # four identical key/value caches, one per HotKV::Client#tool_cache,
132
+ # #ncache, #guard and #rerank accessor.
133
+ class KeyedCache
134
+ def initialize(client, wire_prefix)
135
+ @client = client
136
+ @prefix = wire_prefix
137
+ end
138
+
139
+ def create(ns, max_entries: nil, ttl: nil)
140
+ args = ["#{@prefix}.CREATE", ns]
141
+ args.push("MAX_ENTRIES", max_entries) if max_entries
142
+ args.push("TTL", ttl) if ttl
143
+ @client.invoke(*args) { |r| r == "OK" }
144
+ end
145
+
146
+ # +usage+ (a HotKV::TokenUsage), if given, records the original
147
+ # (uncached) call's token usage so a later hit attributes it (and
148
+ # its cost, once the model is priced) to #stats's usage_saved_*
149
+ # counters.
150
+ def set(ns, key, value, ttl: nil, meta: nil, usage: nil)
151
+ args = ["#{@prefix}.SET", ns, key, value]
152
+ args.push("TTL", ttl) if ttl
153
+ args.push("META", meta) if meta
154
+ args.concat(usage.wire_args) if usage
155
+ @client.invoke(*args) { |r| r == "OK" }
156
+ end
157
+
158
+ def get(ns, key)
159
+ @client.invoke("#{@prefix}.GET", ns, key)
160
+ end
161
+
162
+ def del(ns, key)
163
+ @client.invoke("#{@prefix}.DEL", ns, key) { |r| r == 1 }
164
+ end
165
+
166
+ # entries, hits, misses, evictions, memory_bytes and the
167
+ # usage_saved_* savings counters for +ns+.
168
+ def stats(ns)
169
+ @client.invoke("#{@prefix}.STATS", ns) do |r|
170
+ Commands::Support.pairs_to_h(r, float_keys: %i[usage_saved_cost_usd])
171
+ end
172
+ end
173
+
174
+ def flush(ns)
175
+ @client.invoke("#{@prefix}.FLUSH", ns)
176
+ end
177
+
178
+ def drop(ns)
179
+ @client.invoke("#{@prefix}.DROP", ns) { |r| r == 1 }
180
+ end
181
+
182
+ def list
183
+ @client.invoke("#{@prefix}.LIST")
184
+ end
185
+ end
186
+
187
+ # LLMSTATS.* (needs the LLMStats license feature).
188
+ class LLMStats
189
+ def initialize(client)
190
+ @client = client
191
+ end
192
+
193
+ def bucket(name)
194
+ @client.invoke("LLMSTATS.BUCKET", name) { |r| r == "OK" }
195
+ end
196
+
197
+ # +cached_tokens+ is cache-READ tokens. With +model+, the server
198
+ # prices the call itself from its LLMSTATS.PRICE table and ignores
199
+ # +cost_usd+ (pass 0.0); an unpriced model still records tokens,
200
+ # costs 0 and counts toward +unpriced_requests+. For OpenAI, pass
201
+ # <tt>prompt_tokens - cached_tokens</tt> as +input_tokens+ (OpenAI
202
+ # reports cache hits as part of prompt_tokens, unlike Anthropic).
203
+ def record(bucket, input_tokens, output_tokens, cached_tokens, cost_usd, latency_ms,
204
+ error: nil, model: nil, cache_write_5m_tokens: nil, cache_write_1h_tokens: nil)
205
+ args = ["LLMSTATS.RECORD", bucket, input_tokens, output_tokens, cached_tokens, Wire.finite(cost_usd), latency_ms]
206
+ args.push("ERROR", error ? 1 : 0) unless error.nil?
207
+ args.push("MODEL", model) if model
208
+ args.push("CACHEWRITE5M", cache_write_5m_tokens) if cache_write_5m_tokens
209
+ args.push("CACHEWRITE1H", cache_write_1h_tokens) if cache_write_1h_tokens
210
+ @client.invoke(*args) { |r| r == "OK" }
211
+ end
212
+
213
+ # requests/errors/token/cost/latency stats for +bucket+, including a
214
+ # trailing 1-minute rolling window and unpriced_requests.
215
+ def get(bucket)
216
+ @client.invoke("LLMSTATS.GET", bucket) do |r|
217
+ Commands::Support.pairs_to_h(r, float_keys: %i[cost_usd avg_latency_ms cost_usd_1m])
218
+ end
219
+ end
220
+
221
+ def reset(bucket)
222
+ @client.invoke("LLMSTATS.RESET", bucket) { |r| r == 1 }
223
+ end
224
+
225
+ def drop(bucket)
226
+ @client.invoke("LLMSTATS.DROP", bucket) { |r| r == 1 }
227
+ end
228
+
229
+ def list
230
+ @client.invoke("LLMSTATS.LIST")
231
+ end
232
+
233
+ # -- LLMSTATS.PRICE (per model price table, used by #record and
234
+ # Namespaces::LLMBudget#consume when given +model+/+usage+) --------
235
+
236
+ # Sets a custom USD-per-1,000,000-token price for +model+. Prices
237
+ # may be a Numeric or a decimal String; an omitted cache price
238
+ # defaults to 0.1x/1.25x/2x +input_usd_per_mtok+.
239
+ def price_set(model, input_usd_per_mtok, output_usd_per_mtok, cache_read_usd_per_mtok: nil,
240
+ cache_write_5m_usd_per_mtok: nil, cache_write_1h_usd_per_mtok: nil)
241
+ args = [
242
+ "LLMSTATS.PRICE", "SET", model,
243
+ "INPUT", LLMPrice.format_price(input_usd_per_mtok),
244
+ "OUTPUT", LLMPrice.format_price(output_usd_per_mtok)
245
+ ]
246
+ args.push("CACHEREAD", LLMPrice.format_price(cache_read_usd_per_mtok)) if cache_read_usd_per_mtok
247
+ args.push("CACHEWRITE5M", LLMPrice.format_price(cache_write_5m_usd_per_mtok)) if cache_write_5m_usd_per_mtok
248
+ args.push("CACHEWRITE1H", LLMPrice.format_price(cache_write_1h_usd_per_mtok)) if cache_write_1h_usd_per_mtok
249
+ @client.invoke(*args) { |r| r == "OK" }
250
+ end
251
+
252
+ # +model+'s price table entry. An unknown model comes back with
253
+ # <tt>source == "unpriced"</tt> and every price 0.0, not an error.
254
+ def price_get(model)
255
+ @client.invoke("LLMSTATS.PRICE", "GET", model) { |r| price_from_reply(r) }
256
+ end
257
+
258
+ # Every known model's price table entry (built in and custom).
259
+ def price_list
260
+ @client.invoke("LLMSTATS.PRICE", "LIST") { |r| r.map { |item| price_from_reply(item) } }
261
+ end
262
+
263
+ # Removes +model+'s custom price override, if any (false if there
264
+ # was none; the model then falls back to its built-in price, if any).
265
+ def price_del(model)
266
+ @client.invoke("LLMSTATS.PRICE", "DEL", model) { |r| r == 1 }
267
+ end
268
+
269
+ private
270
+
271
+ def price_from_reply(reply)
272
+ LLMPrice.from_fields(Commands::Support.pairs_to_h(reply, float_keys: LLMPrice::FLOAT_FIELDS))
273
+ end
274
+ end
275
+
276
+ # LLMBUDGET.* (needs the LLMBudget license feature).
277
+ class LLMBudget
278
+ def initialize(client)
279
+ @client = client
280
+ end
281
+
282
+ def set(name, token_limit, cost_limit_usd, window_seconds)
283
+ @client.invoke("LLMBUDGET.SET", name, token_limit, Wire.usd(cost_limit_usd), window_seconds) { |r| r == "OK" }
284
+ end
285
+
286
+ # Tries to consume +tokens+/+cost_usd+ from budget +name+; true if
287
+ # allowed, false if it would exceed the cap. Pass +usage+ (a
288
+ # HotKV::TokenUsage) instead to have the server compute both from
289
+ # its LLMSTATS.PRICE table (this sends tokens/cost_usd as 0/0.0
290
+ # regardless of what is passed, since the server ignores them).
291
+ def consume(name, tokens = 0, cost_usd = 0.0, usage: nil)
292
+ args = ["LLMBUDGET.CONSUME", name, tokens, Wire.finite(cost_usd)]
293
+ args.concat(usage.wire_args) if usage
294
+ @client.invoke(*args) { |r| r == 1 }
295
+ end
296
+
297
+ # usage, limits, the current window's remaining time and
298
+ # unpriced_requests for +name+.
299
+ def status(name)
300
+ @client.invoke("LLMBUDGET.STATUS", name) do |r|
301
+ Commands::Support.pairs_to_h(r, float_keys: %i[cost_used_usd cost_limit_usd])
302
+ end
303
+ end
304
+
305
+ def reset(name)
306
+ @client.invoke("LLMBUDGET.RESET", name) { |r| r == 1 }
307
+ end
308
+
309
+ def drop(name)
310
+ @client.invoke("LLMBUDGET.DROP", name) { |r| r == 1 }
311
+ end
312
+
313
+ def list
314
+ @client.invoke("LLMBUDGET.LIST")
315
+ end
316
+ end
317
+
318
+ # PROMPT.* (versioned prompt/template registry; needs the PromptRegistry license feature).
319
+ class Prompt
320
+ def initialize(client)
321
+ @client = client
322
+ end
323
+
324
+ def put(name, content, meta: nil)
325
+ args = ["PROMPT.PUT", name, content]
326
+ args.push("META", meta) if meta
327
+ @client.invoke(*args)
328
+ end
329
+
330
+ def get(name, version = nil)
331
+ args = version ? ["PROMPT.GET", name, version] : ["PROMPT.GET", name]
332
+ @client.invoke(*args) { |r| prompt_version(r) }
333
+ end
334
+
335
+ def latest(name)
336
+ @client.invoke("PROMPT.LATEST", name) { |r| prompt_version(r) }
337
+ end
338
+
339
+ def setactive(name, version)
340
+ @client.invoke("PROMPT.SETACTIVE", name, version) { |r| r == "OK" }
341
+ end
342
+
343
+ def versions(name)
344
+ @client.invoke("PROMPT.VERSIONS", name)
345
+ end
346
+
347
+ def drop(name)
348
+ @client.invoke("PROMPT.DROP", name) { |r| r == 1 }
349
+ end
350
+
351
+ def list
352
+ @client.invoke("PROMPT.LIST")
353
+ end
354
+
355
+ private
356
+
357
+ def prompt_version(r)
358
+ r.nil? ? nil : { version: r[0], content: r[1], metadata: r[2], created_unix: r[3] }
359
+ end
360
+ end
361
+
362
+ # EMBED.DEDUP.* (needs the EmbedDedup license feature).
363
+ class EmbedDedup
364
+ def initialize(client)
365
+ @client = client
366
+ end
367
+
368
+ def put(ns, hash, model, embedding)
369
+ @client.invoke("EMBED.DEDUP.PUT", ns, hash, model, embedding) { |r| r == 1 }
370
+ end
371
+
372
+ def get(ns, hash)
373
+ @client.invoke("EMBED.DEDUP.GET", ns, hash)
374
+ end
375
+
376
+ def release(ns, hash)
377
+ @client.invoke("EMBED.DEDUP.RELEASE", ns, hash) { |r| r == 1 }
378
+ end
379
+
380
+ def stats(ns)
381
+ @client.invoke("EMBED.DEDUP.STATS", ns) { |r| Commands::Support.pairs_to_h(r) }
382
+ end
383
+
384
+ def drop(ns)
385
+ @client.invoke("EMBED.DEDUP.DROP", ns) { |r| r == 1 }
386
+ end
387
+
388
+ def purge(ns)
389
+ @client.invoke("EMBED.DEDUP.PURGE", ns) { |r| r == 1 }
390
+ end
391
+
392
+ def list
393
+ @client.invoke("EMBED.DEDUP.LIST")
394
+ end
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,190 @@
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 Namespaces
6
+ # TAG.* (core): freeform tags on any key.
7
+ class Tag
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ def set(key, *tags)
13
+ @client.invoke("TAG.SET", key, *tags.flatten)
14
+ end
15
+
16
+ def rem(key, *tags)
17
+ @client.invoke("TAG.REM", key, *tags.flatten)
18
+ end
19
+
20
+ def get(key)
21
+ @client.invoke("TAG.GET", key)
22
+ end
23
+
24
+ def members(*tags)
25
+ @client.invoke("TAG.MEMBERS", *tags.flatten)
26
+ end
27
+
28
+ def inter(*tags)
29
+ @client.invoke("TAG.INTER", *tags.flatten)
30
+ end
31
+
32
+ def count(key)
33
+ @client.invoke("TAG.COUNT", key)
34
+ end
35
+
36
+ def exists(key, tag)
37
+ @client.invoke("TAG.EXISTS", key, tag) { |r| r == 1 }
38
+ end
39
+ end
40
+
41
+ # GCRA.* (core): the generic cell rate algorithm limiter primitive.
42
+ class Gcra
43
+ def initialize(client)
44
+ @client = client
45
+ end
46
+
47
+ def limit(key, period_ms, burst, count = nil)
48
+ args = ["GCRA.LIMIT", key, period_ms, burst]
49
+ args.push(count) if count
50
+ @client.invoke(*args) do |r|
51
+ { denied: r[0] == 1, limit: r[1], remaining: r[2], retry_after_ms: r[3], reset_after_ms: r[4] }
52
+ end
53
+ end
54
+
55
+ def reset(*keys)
56
+ @client.invoke("GCRA.RESET", *keys.flatten)
57
+ end
58
+ end
59
+
60
+ # VSIM.* (core, except SAVE/LOAD/LIST/DROP persistence which need a
61
+ # license that includes the VSIMPersist feature): a vector similarity
62
+ # index.
63
+ class VSim
64
+ def initialize(client)
65
+ @client = client
66
+ end
67
+
68
+ def createindex(name, dims, metric: nil, m: nil, ef_construction: nil, ef_search: nil)
69
+ args = ["VSIM.CREATEINDEX", name, dims]
70
+ args.push("METRIC", metric) if metric
71
+ args.push("M", m) if m
72
+ args.push("EF_CONSTRUCTION", ef_construction) if ef_construction
73
+ args.push("EF_SEARCH", ef_search) if ef_search
74
+ @client.invoke(*args) { |r| r == "OK" }
75
+ end
76
+
77
+ def dropindex(name)
78
+ @client.invoke("VSIM.DROPINDEX", name) { |r| r == "OK" }
79
+ end
80
+
81
+ def set(index, key, vector)
82
+ @client.invoke("VSIM.SET", index, key, *Wire.finite_vector(vector))
83
+ end
84
+
85
+ def get(index, key)
86
+ @client.invoke("VSIM.GET", index, key) { |r| r&.map { |v| Wire.parse_float(v) } }
87
+ end
88
+
89
+ def del(index, key)
90
+ @client.invoke("VSIM.DEL", index, key)
91
+ end
92
+
93
+ def search(index, k, vector, filter: nil, ef: nil)
94
+ args = ["VSIM.SEARCH", index, k, *Wire.finite_vector(vector)]
95
+ args.push("FILTER", *Array(filter)) if filter
96
+ args.push("EF", ef) if ef
97
+ @client.invoke(*args) { |r| r.each_slice(2).map { |(key, dist)| [key, Wire.parse_float(dist)] } }
98
+ end
99
+
100
+ def info(index = nil)
101
+ return @client.invoke("VSIM.INFO") if index.nil?
102
+
103
+ @client.invoke("VSIM.INFO", index) { |r| Commands::Support.pairs_to_h(r) }
104
+ end
105
+
106
+ # Persistence: needs the VSIMPersist license feature.
107
+ def save(snapshot_name, blob)
108
+ @client.invoke("VSIM.SAVE", snapshot_name, blob)
109
+ end
110
+
111
+ def load(snapshot_name)
112
+ @client.invoke("VSIM.LOAD", snapshot_name)
113
+ end
114
+
115
+ def list
116
+ @client.invoke("VSIM.LIST")
117
+ end
118
+
119
+ def drop(snapshot_name)
120
+ @client.invoke("VSIM.DROP", snapshot_name) { |r| r == 1 }
121
+ end
122
+ end
123
+
124
+ # OBSERVE (needs the Observability license feature; admin).
125
+ class Observe
126
+ def initialize(client)
127
+ @client = client
128
+ end
129
+
130
+ def info
131
+ @client.invoke("OBSERVE", "INFO") { |r| Commands::Support.pairs_to_h(r) }
132
+ end
133
+
134
+ def metrics
135
+ @client.invoke("OBSERVE", "METRICS") { |r| Commands::Support.pairs_to_h(r) }
136
+ end
137
+ end
138
+
139
+ # ALERTS (needs the Observability license feature; admin).
140
+ class Alerts
141
+ def initialize(client)
142
+ @client = client
143
+ end
144
+
145
+ def info
146
+ @client.invoke("ALERTS", "INFO") { |r| Commands::Support.pairs_to_h(r) }
147
+ end
148
+
149
+ def test(severity = nil, *message)
150
+ args = ["ALERTS", "TEST"]
151
+ args.push(severity) if severity
152
+ flat_message = message.flatten
153
+ args.concat(flat_message) unless flat_message.empty?
154
+ @client.invoke(*args)
155
+ end
156
+
157
+ def seturl(url)
158
+ @client.invoke("ALERTS", "SETURL", url)
159
+ end
160
+
161
+ def setseverity(severity)
162
+ @client.invoke("ALERTS", "SETSEVERITY", severity)
163
+ end
164
+
165
+ def on
166
+ @client.invoke("ALERTS", "ON")
167
+ end
168
+
169
+ def off
170
+ @client.invoke("ALERTS", "OFF")
171
+ end
172
+ end
173
+
174
+ # HOTKEYS (core, admin).
175
+ class Hotkeys
176
+ def initialize(client)
177
+ @client = client
178
+ end
179
+
180
+ def top(count = nil)
181
+ args = count ? ["HOTKEYS", "TOP", count] : ["HOTKEYS", "TOP"]
182
+ @client.invoke(*args)
183
+ end
184
+
185
+ def reset
186
+ @client.invoke("HOTKEYS", "RESET") { |r| r == "OK" }
187
+ end
188
+ end
189
+ end
190
+ end