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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -17
  3. data/lib/tollmeshcache.rb +403 -0
  4. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 949f75ceedd8c6f02fe2d4e67ba84179a453fbb4835937d79a47b6cf6ca816cf
4
- data.tar.gz: cbef69fcc4592c51e6a4aa77e46831a4984d8a9f24b921a2857f69f42431e7aa
3
+ metadata.gz: a729fb89101263229344300bf629f26ef8e48432144a5945197172bf3236c1d0
4
+ data.tar.gz: bf634e8110c6005710622bf69b3255591801dc2634ceb0f068de19865a633aa5
5
5
  SHA512:
6
- metadata.gz: 954645035c80188ebd46400863da7c662c79294eca88957c75afcb7cca6c5cb94a7b5ecea33800a1b680186200dc41944ee44a1702e50cf8451e2f554e624883
7
- data.tar.gz: 899bb8e79b81a97f2f977cd35dbad72dfa12b0c6ccd88035d23c291d18200ae75c9a29845efb1f8cd7147da55d00f720ac45918e2c868a27348c17b85a3a4a63
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
- # Create a cache client
17
- cache = TollMeshCache::Client.new('localhost:8080')
18
-
19
- # Job Queues
20
- cache.enqueue('tasks', 'my-job', priority: 5)
21
- job = cache.claim('tasks')
22
- cache.complete('tasks', job.id)
23
-
24
- # Sorted Sets (Leaderboards)
25
- cache.zadd('scores', 100, 'player-1')
26
- cache.zrange('scores', 0, -1)
27
-
28
- # Streams (Event Logs)
29
- cache.xadd('events', { 'event' => 'login', 'user' => 'alice' })
30
- cache.xrange('events', '-', '+')
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/store for complete documentation.
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.0.0
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-01 00:00:00.000000000 Z
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