catdb-client 0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +26 -0
- data/lib/catdb.rb +155 -0
- metadata +45 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fe9866eedd48e0d1f987d21d4a19bfa7d6641602342b4a04d040b81652812730
|
|
4
|
+
data.tar.gz: 2dab17d31a4bb7b2af026be609c1ef356e59ca7734121d787428c7a33e10d56b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: eeb3e32f7fc9291d2bc3f9cd029e561001d07a1c5741cc0deadc929a6fa82e2f81fea934860efaafebe94644413a766727dcc4c465f09de9028ca4eadea19532
|
|
7
|
+
data.tar.gz: 5efd05b4895fc865ebf609ce37bede59ad80bd134aa2bb7353dbc1abcf639ffa42372c076ce6f27ac2f5ec89a37cc24b9d15b6d1d74e35e498a0867831119a5a
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dhq_boiler and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# catdb for Ruby
|
|
2
|
+
|
|
3
|
+
Use Ruby 3.1 or later. Install the client from RubyGems.org:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
gem install catdb-client -v 0.1.0
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
With Bundler, add `gem "catdb-client", "~> 0.1.0"` to your Gemfile and run `bundle install`. Load the client with `require "catdb"`.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "catdb"
|
|
13
|
+
|
|
14
|
+
CatDB::Client.connect(token: ENV.fetch("CATDB_TOKEN")) do |client|
|
|
15
|
+
key = "\xff\x00".b
|
|
16
|
+
version = client.put_commit(key, "\x01\x02".b)
|
|
17
|
+
value = client.get_latest(key)
|
|
18
|
+
p [version, value]
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use the shared token on a trusted local connection. For an account, pass `host:`, `username:`, `password:` and optionally `database:` (default `"main"`) and `ca_file:` (a PEM certificate file). Account connections require TLS 1.3 and verify the hostname and certificate chain. The default address is `127.0.0.1:7400`. The `timeout:` setting is in seconds and defaults to 10.
|
|
23
|
+
|
|
24
|
+
`get_latest` returns a binary string or `nil` for a missing key. An empty value returns an empty string. `put_commit` and `delete_commit` wait for a durable commit and return its version as a decimal string. For a transaction, use `begin_transaction` or `begin_read`, then `get`, `put`, `delete`, `commit` or `rollback`. One connection serializes requests and owns one transaction. Give independent transactions separate clients. The block form closes the connection on exit.
|
|
25
|
+
|
|
26
|
+
Use `request(op: "scan_latest", start: "", rows: 20)` for the remaining wire operations, with hex keys and values. A server rejection raises `CatDB::ServerError` with its `code`. A lost reply can leave a mutation's outcome unknown. The client closes the connection and leaves the decision to retry to the application.
|
data/lib/catdb.rb
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
module CatDB
|
|
9
|
+
class ServerError < StandardError
|
|
10
|
+
attr_reader :code
|
|
11
|
+
|
|
12
|
+
def initialize(code)
|
|
13
|
+
@code = code
|
|
14
|
+
super("catdb returned #{code}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Client
|
|
19
|
+
MAX_REQUEST = 1024 * 1024
|
|
20
|
+
MAX_RESPONSE = 2 * 1024 * 1024 + 8192
|
|
21
|
+
|
|
22
|
+
def self.connect(**options)
|
|
23
|
+
client = new(**options)
|
|
24
|
+
return client unless block_given?
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
yield client
|
|
28
|
+
ensure
|
|
29
|
+
client.close
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(host: "127.0.0.1", port: 7400, token: nil, username: nil, password: nil,
|
|
34
|
+
database: "main", ca_file: nil, timeout: 10)
|
|
35
|
+
@timeout = Float(timeout)
|
|
36
|
+
raise ArgumentError, "timeout must be finite and positive" unless @timeout.finite? && @timeout.positive?
|
|
37
|
+
account = !username.nil?
|
|
38
|
+
valid = if account
|
|
39
|
+
username.is_a?(String) && password.is_a?(String) && token.nil?
|
|
40
|
+
else
|
|
41
|
+
token.is_a?(String) && password.nil? && database == "main" && ca_file.nil?
|
|
42
|
+
end
|
|
43
|
+
raise ArgumentError, "choose a token or a TLS username and password" unless valid
|
|
44
|
+
|
|
45
|
+
@lock = Mutex.new
|
|
46
|
+
connected = false
|
|
47
|
+
begin
|
|
48
|
+
Timeout.timeout(@timeout) do
|
|
49
|
+
@socket = Socket.tcp(host, port, connect_timeout: @timeout)
|
|
50
|
+
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
51
|
+
if account
|
|
52
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
53
|
+
context.set_params
|
|
54
|
+
context.min_version = OpenSSL::SSL::TLS1_3_VERSION
|
|
55
|
+
if ca_file
|
|
56
|
+
trust = OpenSSL::X509::Store.new
|
|
57
|
+
trust.add_file(ca_file)
|
|
58
|
+
context.cert_store = trust
|
|
59
|
+
end
|
|
60
|
+
tls = OpenSSL::SSL::SSLSocket.new(@socket, context)
|
|
61
|
+
tls.sync_close = true
|
|
62
|
+
@socket = tls
|
|
63
|
+
tls.hostname = host
|
|
64
|
+
tls.connect
|
|
65
|
+
tls.post_connection_check(host)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
auth = account ? { op: "auth", version: 2, username: username, password: password, database: database } :
|
|
69
|
+
{ op: "auth", version: 1, token: token }
|
|
70
|
+
reply = request(**auth)
|
|
71
|
+
raise IOError, "unexpected catdb protocol version" unless reply["protocol"] == (account ? 2 : 1)
|
|
72
|
+
connected = true
|
|
73
|
+
ensure
|
|
74
|
+
close unless connected
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def request(op:, **fields)
|
|
79
|
+
body = JSON.generate(fields.merge(op: op)).b
|
|
80
|
+
raise ArgumentError, "catdb request exceeds 1 MiB" if body.bytesize > MAX_REQUEST
|
|
81
|
+
@lock.synchronize do
|
|
82
|
+
socket = @socket
|
|
83
|
+
raise IOError, "catdb connection is closed" unless socket
|
|
84
|
+
received = false
|
|
85
|
+
begin
|
|
86
|
+
reply = Timeout.timeout(@timeout) do
|
|
87
|
+
socket.write([body.bytesize].pack("N") + body)
|
|
88
|
+
size = read_exact(socket, 4).unpack1("N")
|
|
89
|
+
raise IOError, "invalid catdb response size" unless size.between?(1, MAX_RESPONSE)
|
|
90
|
+
text = read_exact(socket, size).force_encoding(Encoding::UTF_8)
|
|
91
|
+
raise IOError, "invalid catdb response encoding" unless text.valid_encoding?
|
|
92
|
+
JSON.parse(text)
|
|
93
|
+
end
|
|
94
|
+
unless reply.is_a?(Hash) && (reply["ok"] == true || (reply["ok"] == false && reply["error"].is_a?(String)))
|
|
95
|
+
raise IOError, "invalid catdb response"
|
|
96
|
+
end
|
|
97
|
+
received = true
|
|
98
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError, Timeout::Error, JSON::ParserError => e
|
|
99
|
+
raise IOError, "catdb response lost or invalid; a submitted mutation may have committed", cause: e
|
|
100
|
+
ensure
|
|
101
|
+
close unless received
|
|
102
|
+
end
|
|
103
|
+
raise ServerError, reply["error"] unless reply["ok"]
|
|
104
|
+
reply
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ping = version(op: "ping")
|
|
109
|
+
def get_latest(key) = read_value("get_latest", key)
|
|
110
|
+
def put_commit(key, value) = version(op: "put_commit", key: hex(key), value: hex(value))
|
|
111
|
+
def delete_commit(key) = version(op: "delete_commit", key: hex(key))
|
|
112
|
+
def begin_transaction = version(op: "begin")
|
|
113
|
+
def begin_read = version(op: "begin_read")
|
|
114
|
+
def get(key) = read_value("get", key)
|
|
115
|
+
def put(key, value) = request(op: "put", key: hex(key), value: hex(value))
|
|
116
|
+
def delete(key) = request(op: "delete", key: hex(key))
|
|
117
|
+
def commit = version(op: "commit")
|
|
118
|
+
def rollback = request(op: "rollback")
|
|
119
|
+
|
|
120
|
+
def close
|
|
121
|
+
socket = @socket
|
|
122
|
+
@socket = nil
|
|
123
|
+
socket&.close
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def hex(bytes)
|
|
129
|
+
raise TypeError, "expected a binary String" unless bytes.is_a?(String)
|
|
130
|
+
bytes.unpack1("H*")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def read_exact(socket, size)
|
|
134
|
+
data = socket.read(size)
|
|
135
|
+
raise EOFError, "catdb connection closed" unless data && data.bytesize == size
|
|
136
|
+
data
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def read_value(op, key)
|
|
140
|
+
reply = request(op: op, key: hex(key))
|
|
141
|
+
value = reply["value"]
|
|
142
|
+
return nil if reply.key?("value") && value.nil?
|
|
143
|
+
return [value].pack("H*") if value.is_a?(String) && value.bytesize.even? && value.match?(/\A[0-9a-fA-F]*\z/)
|
|
144
|
+
close
|
|
145
|
+
raise IOError, "invalid catdb value"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def version(**operation)
|
|
149
|
+
number = request(**operation)["version"]
|
|
150
|
+
return number if number.is_a?(String) && number.match?(/\A(?:0|[1-9][0-9]*)\z/)
|
|
151
|
+
close
|
|
152
|
+
raise IOError, "invalid catdb version"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: catdb-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- catdb contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1970-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Read and write binary keys and values, commit transactions, and connect
|
|
13
|
+
to catdb with token or TLS account authentication.
|
|
14
|
+
executables: []
|
|
15
|
+
extensions: []
|
|
16
|
+
extra_rdoc_files: []
|
|
17
|
+
files:
|
|
18
|
+
- LICENSE
|
|
19
|
+
- README.md
|
|
20
|
+
- lib/catdb.rb
|
|
21
|
+
homepage: https://github.com/dhq-boiler/catdb
|
|
22
|
+
licenses:
|
|
23
|
+
- MIT
|
|
24
|
+
metadata:
|
|
25
|
+
source_code_uri: https://github.com/dhq-boiler/catdb/tree/clients/ruby/v0.1.0/clients/ruby
|
|
26
|
+
bug_tracker_uri: https://github.com/dhq-boiler/catdb/issues
|
|
27
|
+
allowed_push_host: https://rubygems.org
|
|
28
|
+
rdoc_options: []
|
|
29
|
+
require_paths:
|
|
30
|
+
- lib
|
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
32
|
+
requirements:
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '3.1'
|
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
requirements: []
|
|
42
|
+
rubygems_version: 3.6.9
|
|
43
|
+
specification_version: 4
|
|
44
|
+
summary: Ruby client for the catdb TCP service
|
|
45
|
+
test_files: []
|