tollmeshcache 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +20 -17
- data/lib/tollmeshcache.rb +403 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a729fb89101263229344300bf629f26ef8e48432144a5945197172bf3236c1d0
|
|
4
|
+
data.tar.gz: bf634e8110c6005710622bf69b3255591801dc2634ceb0f068de19865a633aa5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d88502bee359017c136c0fcbc8947ab0e992b1ae060bac0f4b3729323da2ddab551c19ed24539cd08d6c5568353127e0db1dfbbb3d28c12a03d2e01fa5f7200e
|
|
7
|
+
data.tar.gz: 0db93ddcc970f174623d92e3f93aafd23009f6df56d918e3c19e5e1ba7f540a29e96bf4d8396d167feb06ca0286288d0625aa3e10721556df83c79f29fa6d002
|
data/README.md
CHANGED
|
@@ -13,21 +13,25 @@ gem install tollmeshcache
|
|
|
13
13
|
```ruby
|
|
14
14
|
require 'tollmeshcache'
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Job Queues
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Sorted Sets (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
config = TollMeshCache::ClientConfig.new(host: 'localhost', port: 8080)
|
|
17
|
+
client = TollMeshCache::Client.new(config)
|
|
18
|
+
|
|
19
|
+
# Job Queues - distributed task processing
|
|
20
|
+
job = client.enqueue('tasks', 'my-job', priority: 5)
|
|
21
|
+
claimed = client.claim('tasks', 'worker-1')
|
|
22
|
+
client.complete('tasks', claimed['id'])
|
|
23
|
+
|
|
24
|
+
# Sorted Sets - O(log n) leaderboards
|
|
25
|
+
client.zadd('scores', 100, 'player-1')
|
|
26
|
+
client.zadd('scores', 150, 'player-2')
|
|
27
|
+
top_scores = client.zrevrange('scores', limit: 10) # highest first
|
|
28
|
+
|
|
29
|
+
# Streams - append-only event logs
|
|
30
|
+
entry = client.xadd('events', { 'event' => 'login', 'user' => 'alice' })
|
|
31
|
+
client.xgroup_create('events', 'analytics')
|
|
32
|
+
client.xreadgroup('analytics', 'worker-1', 'events').each do |e|
|
|
33
|
+
client.xack('events', 'analytics', 'worker-1', e['id'])
|
|
34
|
+
end
|
|
31
35
|
```
|
|
32
36
|
|
|
33
37
|
## Features
|
|
@@ -36,11 +40,10 @@ cache.xrange('events', '-', '+')
|
|
|
36
40
|
- **Sorted Sets**: O(log n) leaderboards and rankings
|
|
37
41
|
- **Streams**: Append-only event logs with consumer groups
|
|
38
42
|
- **CRDT-based**: Eventual consistency without central coordinator
|
|
39
|
-
- **Async/Await**: Full async support with Fiber
|
|
40
43
|
|
|
41
44
|
## Documentation
|
|
42
45
|
|
|
43
|
-
See https://github.com/toll-mesh
|
|
46
|
+
See https://github.com/TollMesh/toll-mesh-store for complete documentation.
|
|
44
47
|
|
|
45
48
|
## License
|
|
46
49
|
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
require 'httpclient'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module TollMeshCache
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class RateLimitError < Error; end
|
|
7
|
+
class ReplayError < Error; end
|
|
8
|
+
class CacheMissError < Error; end
|
|
9
|
+
|
|
10
|
+
class ClientConfig
|
|
11
|
+
attr_accessor :host, :port, :timeout, :verify_ssl, :api_key, :scheme
|
|
12
|
+
|
|
13
|
+
def initialize(host: 'localhost', port: 8080, timeout: 5, verify_ssl: true, scheme: 'http')
|
|
14
|
+
@host = host
|
|
15
|
+
@port = port
|
|
16
|
+
@timeout = timeout
|
|
17
|
+
@verify_ssl = verify_ssl
|
|
18
|
+
@scheme = scheme
|
|
19
|
+
@api_key = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def base_url
|
|
23
|
+
"#{@scheme}://#{@host}:#{@port}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Client
|
|
28
|
+
def initialize(config = nil)
|
|
29
|
+
@config = config || ClientConfig.new
|
|
30
|
+
@http = HTTPClient.new
|
|
31
|
+
@http.receive_timeout = @config.timeout
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def consume(key, limit, window_ms)
|
|
35
|
+
body = {
|
|
36
|
+
key: key,
|
|
37
|
+
limit: limit,
|
|
38
|
+
window: window_ms
|
|
39
|
+
}
|
|
40
|
+
post('/consume', body)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def seen(key, ttl_ms)
|
|
44
|
+
body = { key: key, ttl: ttl_ms }
|
|
45
|
+
post('/seen', body)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cache_get(namespace, key)
|
|
49
|
+
# /cache/get is a GET endpoint taking query params (see
|
|
50
|
+
# api/http.go handleCacheGet), not a POST with a JSON body.
|
|
51
|
+
get('/cache/get', query: { namespace: namespace, key: key })
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cache_set(namespace, key, value, ttl_ms = nil)
|
|
55
|
+
body = {
|
|
56
|
+
namespace: namespace,
|
|
57
|
+
key: key,
|
|
58
|
+
value: value
|
|
59
|
+
}
|
|
60
|
+
body[:ttl] = ttl_ms if ttl_ms
|
|
61
|
+
post('/cache/set', body)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def health
|
|
65
|
+
get('/health')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def get_peers
|
|
69
|
+
response = get('/peers')
|
|
70
|
+
response['peers'] || []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ===== Job Queues =====
|
|
74
|
+
|
|
75
|
+
def enqueue(queue, payload, priority: 5, max_retries: 3, deadline_ms: nil)
|
|
76
|
+
body = { queue: queue, payload: payload, priority: priority, max_retries: max_retries }
|
|
77
|
+
body[:deadline] = deadline_ms if deadline_ms
|
|
78
|
+
post('/queue/enqueue', body)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def claim(queue, worker_id)
|
|
82
|
+
post('/queue/claim', { queue: queue, worker_id: worker_id })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def complete(queue, job_id, result = '')
|
|
86
|
+
post('/queue/complete', { queue: queue, job_id: job_id, result: result })
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fail_job(queue, job_id, error)
|
|
90
|
+
post('/queue/fail', { queue: queue, job_id: job_id, error: error })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def job_status(queue, job_id)
|
|
94
|
+
get('/queue/status', query: { queue: queue, job_id: job_id })
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def queue_stats(queue)
|
|
98
|
+
get('/queue/stats', query: { queue: queue })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ===== Sorted Sets =====
|
|
102
|
+
|
|
103
|
+
def zadd(key, score, member)
|
|
104
|
+
post('/zset/add', { key: key, member: member, score: score })
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def zrem(key, member)
|
|
108
|
+
post('/zset/remove', { key: key, member: member })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def zscore(key, member)
|
|
112
|
+
response = get('/zset/score', query: { key: key, member: member })
|
|
113
|
+
[response['score'], response['exists']]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def zrank(key, member)
|
|
117
|
+
response = get('/zset/rank', query: { key: key, member: member })
|
|
118
|
+
[response['rank'], response['exists']]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def zrevrank(key, member)
|
|
122
|
+
response = get('/zset/revrank', query: { key: key, member: member })
|
|
123
|
+
[response['rank'], response['exists']]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def zrange(key, min: -Float::INFINITY, max: Float::INFINITY, limit: 100)
|
|
127
|
+
response = get('/zset/range', query: { key: key, min: min, max: max, limit: limit })
|
|
128
|
+
response['members'] || []
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def zrevrange(key, max: Float::INFINITY, min: -Float::INFINITY, limit: 100)
|
|
132
|
+
response = get('/zset/revrange', query: { key: key, max: max, min: min, limit: limit })
|
|
133
|
+
response['members'] || []
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def zcard(key)
|
|
137
|
+
response = get('/zset/card', query: { key: key })
|
|
138
|
+
response['card'] || 0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ===== Streams =====
|
|
142
|
+
|
|
143
|
+
def xadd(stream, fields)
|
|
144
|
+
post('/stream/add', { stream: stream, fields: fields })
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def xrange(stream, start = '0', end_id = '-', limit: 100)
|
|
148
|
+
response = get('/stream/range', query: { stream: stream, start: start, end: end_id, limit: limit })
|
|
149
|
+
response['entries'] || []
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def xlen(stream)
|
|
153
|
+
response = get('/stream/len', query: { stream: stream })
|
|
154
|
+
response['length'] || 0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def xgroup_create(stream, group)
|
|
158
|
+
post('/stream/group/create', { stream: stream, group: group })
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def xreadgroup(group, consumer, stream, limit: 100)
|
|
162
|
+
response = post('/stream/group/read', { stream: stream, group: group, consumer: consumer, limit: limit })
|
|
163
|
+
response['entries'] || []
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def xack(stream, group, consumer, entry_id)
|
|
167
|
+
post('/stream/group/ack', { stream: stream, group: group, consumer: consumer, id: entry_id })
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# ===== Pub/Sub =====
|
|
171
|
+
|
|
172
|
+
def subscribe(subscriber_id, topic, pattern: '')
|
|
173
|
+
post('/pubsub/subscribe', { subscriber_id: subscriber_id, topic: topic, pattern: pattern })
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def unsubscribe(subscriber_id, topic)
|
|
177
|
+
post('/pubsub/unsubscribe', { subscriber_id: subscriber_id, topic: topic })
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def publish(topic, publisher, payload)
|
|
181
|
+
response = post('/pubsub/publish', { topic: topic, publisher: publisher, payload: payload })
|
|
182
|
+
response['delivered_count'] || 0
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def poll(subscriber_id, limit: 10, timeout_ms: 5000)
|
|
186
|
+
response = post('/pubsub/poll', { subscriber_id: subscriber_id, limit: limit, timeout_ms: timeout_ms })
|
|
187
|
+
response['messages'] || []
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def get_topics
|
|
191
|
+
response = get('/pubsub/topics')
|
|
192
|
+
response['topics'] || []
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def get_topic_subscribers(topic)
|
|
196
|
+
response = get('/pubsub/subscribers', query: { topic: topic })
|
|
197
|
+
response['subscribers'] || []
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def pubsub_stats
|
|
201
|
+
get('/pubsub/stats')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# ===== Transactions =====
|
|
205
|
+
|
|
206
|
+
def begin_transaction(txn_id)
|
|
207
|
+
post('/txn/begin', { txn_id: txn_id })
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def add_transaction_operation(txn_id, type, namespace, key, value = '')
|
|
211
|
+
post('/txn/operation', { txn_id: txn_id, type: type, namespace: namespace, key: key, value: value })
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def commit_transaction(txn_id)
|
|
215
|
+
post('/txn/commit', { txn_id: txn_id })
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def rollback_transaction(txn_id)
|
|
219
|
+
post('/txn/rollback', { txn_id: txn_id })
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def transaction_status(txn_id)
|
|
223
|
+
response = get('/txn/status', query: { txn_id: txn_id })
|
|
224
|
+
response['status']
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# ===== Persistence =====
|
|
228
|
+
|
|
229
|
+
def create_snapshot
|
|
230
|
+
post('/persistence/snapshot', {})
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def get_latest_snapshot
|
|
234
|
+
get('/persistence/snapshot/latest')
|
|
235
|
+
rescue Error
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def restore_from_latest_snapshot
|
|
240
|
+
post('/persistence/restore', {})
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def persistence_stats
|
|
244
|
+
get('/persistence/stats')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# ===== Scripting: Pipelines (safe operation composition) =====
|
|
248
|
+
|
|
249
|
+
def register_pipeline(name, steps)
|
|
250
|
+
post('/pipeline/register', { name: name, steps: steps })
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def execute_pipeline(name)
|
|
254
|
+
post('/pipeline/execute', { name: name })
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def execute_inline_pipeline(steps)
|
|
258
|
+
post('/pipeline/execute-inline', { steps: steps })
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def get_pipeline(name)
|
|
262
|
+
get('/pipeline/get', query: { name: name })
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def list_pipelines
|
|
266
|
+
response = get('/pipeline/list')
|
|
267
|
+
response['pipelines'] || []
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def delete_pipeline(name)
|
|
271
|
+
post('/pipeline/delete', { name: name })
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# ===== Scripting: WASM (real arbitrary Go code execution) =====
|
|
275
|
+
|
|
276
|
+
def compile_script(name, source)
|
|
277
|
+
post('/script/compile', { name: name, source: source })
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def execute_script(name, input = '')
|
|
281
|
+
response = post('/script/execute', { name: name, input: input })
|
|
282
|
+
response['output'] || ''
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def execute_inline_script(source, input = '')
|
|
286
|
+
response = post('/script/execute-inline', { source: source, input: input })
|
|
287
|
+
response['output'] || ''
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def get_script(name)
|
|
291
|
+
get('/script/get', query: { name: name })
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def list_scripts
|
|
295
|
+
response = get('/script/list')
|
|
296
|
+
response['scripts'] || []
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def delete_script(name)
|
|
300
|
+
post('/script/delete', { name: name })
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# ===== Search =====
|
|
304
|
+
|
|
305
|
+
def index_document(id, content, metadata: nil, vector: nil)
|
|
306
|
+
body = { id: id, content: content }
|
|
307
|
+
body[:metadata] = metadata if metadata
|
|
308
|
+
body[:vector] = vector if vector
|
|
309
|
+
post('/search/index', body)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def search_bm25(query, top_k: 10)
|
|
313
|
+
response = get('/search/bm25', query: { query: query, topk: top_k })
|
|
314
|
+
response['results'] || []
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def search_vector(vector, top_k: 10)
|
|
318
|
+
response = post('/search/vector', { vector: vector, topk: top_k })
|
|
319
|
+
response['results'] || []
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def search_hybrid(query, vector, top_k: 10)
|
|
323
|
+
response = post('/search/hybrid', { query: query, vector: vector, topk: top_k })
|
|
324
|
+
response['results'] || []
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def delete_search_document(id)
|
|
328
|
+
post('/search/delete', { id: id })
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# ===== Ranking =====
|
|
332
|
+
|
|
333
|
+
def rank(items, strategy: 'bm25', boosts: nil)
|
|
334
|
+
body = { items: items, strategy: strategy }
|
|
335
|
+
body[:boosts] = boosts if boosts
|
|
336
|
+
response = post('/rank', body)
|
|
337
|
+
response['items'] || []
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# ===== Metrics =====
|
|
341
|
+
|
|
342
|
+
def get_metrics
|
|
343
|
+
get('/metrics')
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def get_prometheus_metrics
|
|
347
|
+
url = @config.base_url + '/metrics/prometheus'
|
|
348
|
+
response = @http.get(url, header: { 'User-Agent' => 'tollmeshcache-ruby/1.1.0' })
|
|
349
|
+
response.body
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def close
|
|
353
|
+
@http.close if @http
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
private
|
|
357
|
+
|
|
358
|
+
def post(endpoint, body)
|
|
359
|
+
url = @config.base_url + endpoint
|
|
360
|
+
headers = {
|
|
361
|
+
'Content-Type' => 'application/json',
|
|
362
|
+
'User-Agent' => 'tollmeshcache-ruby/1.1.0'
|
|
363
|
+
}
|
|
364
|
+
headers['X-API-Key'] = @config.api_key if @config.api_key
|
|
365
|
+
|
|
366
|
+
response = @http.post(url, JSON.generate(body), headers)
|
|
367
|
+
handle_response(response)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def get(endpoint, query: nil)
|
|
371
|
+
url = @config.base_url + endpoint
|
|
372
|
+
headers = { 'User-Agent' => 'tollmeshcache-ruby/1.1.0' }
|
|
373
|
+
headers['X-API-Key'] = @config.api_key if @config.api_key
|
|
374
|
+
|
|
375
|
+
response = @http.get(url, query: query, header: headers)
|
|
376
|
+
handle_response(response)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def handle_response(response)
|
|
380
|
+
if response.status >= 400
|
|
381
|
+
data = JSON.parse(response.body) rescue { 'code' => response.status }
|
|
382
|
+
code = data['code'] || response.status
|
|
383
|
+
# /consume, /seen, /cache/* use "message"; the job queue, sorted
|
|
384
|
+
# set, and stream endpoints use ErrorResponse{"error": ...} from
|
|
385
|
+
# api/http.go.
|
|
386
|
+
message = data['message'] || data['error'] || "HTTP #{response.status}"
|
|
387
|
+
|
|
388
|
+
case code
|
|
389
|
+
when 429
|
|
390
|
+
raise RateLimitError, message
|
|
391
|
+
when 1001
|
|
392
|
+
raise ReplayError, message
|
|
393
|
+
when 1002
|
|
394
|
+
raise CacheMissError, message
|
|
395
|
+
else
|
|
396
|
+
raise Error, message
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
JSON.parse(response.body)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tollmeshcache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- TollMesh Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: httpclient
|
|
@@ -131,6 +131,7 @@ extra_rdoc_files: []
|
|
|
131
131
|
files:
|
|
132
132
|
- LICENSE
|
|
133
133
|
- README.md
|
|
134
|
+
- lib/tollmeshcache.rb
|
|
134
135
|
homepage: https://github.com/toll-mesh/store
|
|
135
136
|
licenses:
|
|
136
137
|
- Apache-2.0
|