unison-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/Cargo.lock +3182 -0
- data/Cargo.toml +11 -0
- data/README.md +110 -0
- data/ext/unison_client/Cargo.toml +32 -0
- data/ext/unison_client/extconf.rb +8 -0
- data/ext/unison_client/src/lib.rs +255 -0
- data/lib/unison/version.rb +7 -0
- data/lib/unison.rb +21 -0
- metadata +65 -0
data/Cargo.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Ruby client gem の native 拡張 workspace。
|
|
2
|
+
#
|
|
3
|
+
# gem root に置くのは rb_sys(rake-compiler)の要請: `cargo metadata` を
|
|
4
|
+
# gem root から実行するため、ここに manifest が無いと cargo が親へ遡り
|
|
5
|
+
# club-unison の workspace を誤って拾う。
|
|
6
|
+
#
|
|
7
|
+
# 親 (club-unison) の Cargo workspace は `crates/*` のみを member とするため
|
|
8
|
+
# この workspace とは独立。両者が干渉することはない。
|
|
9
|
+
[workspace]
|
|
10
|
+
members = ["ext/unison_client"]
|
|
11
|
+
resolver = "3"
|
data/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# unison-client (Ruby)
|
|
2
|
+
|
|
3
|
+
[Unison protocol](https://github.com/chronista-club/club-unison) の Ruby client。
|
|
4
|
+
|
|
5
|
+
## アーキテクチャ
|
|
6
|
+
|
|
7
|
+
これは **言語バインディング**であって protocol の再実装ではない。
|
|
8
|
+
QUIC トランスポート・channel 多重化・wire framing は Rust の `club-unison`
|
|
9
|
+
crate が実装しており、この gem はそれを **Magnus**(Rust 製 Ruby native
|
|
10
|
+
extension)経由で薄く包む。
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Ruby (require "unison")
|
|
14
|
+
└─ native ext (Magnus, ext/unison_client/)
|
|
15
|
+
└─ club-unison crate (ProtocolClient — QUIC / channel / wire)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
理由: Ruby に成熟した native QUIC スタックが無いため、protocol を Ruby で
|
|
19
|
+
再実装するより Rust core を FFI で binding する方が経済的。TS SDK は browser
|
|
20
|
+
の WebTransport に乗れたので完全再実装したが、Ruby にはその前提が無い。
|
|
21
|
+
|
|
22
|
+
## 状態
|
|
23
|
+
|
|
24
|
+
接続ライフサイクルと channel 層を実装済み。
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "unison"
|
|
28
|
+
|
|
29
|
+
client = Unison::Client.new
|
|
30
|
+
client.connect("quic://[::1]:7878")
|
|
31
|
+
|
|
32
|
+
ch = client.open_channel("greeter")
|
|
33
|
+
ch.request("Hello", { "name" => "Mako" }) #=> レスポンス Hash
|
|
34
|
+
ch.send_event("Ping", { "seq" => 1 }) # 応答不要
|
|
35
|
+
ch.recv # 次の event を待つ(Hash)
|
|
36
|
+
ch.close
|
|
37
|
+
|
|
38
|
+
client.disconnect
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> **注意**: `Unison::Client.new` は証明書検証を行わない insecure な client を
|
|
42
|
+
> 構築する(loopback / 開発用途)。trust anchor を明示する secure constructor は
|
|
43
|
+
> 今後のフェーズ。
|
|
44
|
+
|
|
45
|
+
channel payload は native な Ruby 値(`Hash` / `Array` / scalar)で渡せる。
|
|
46
|
+
Rust 側で `serde_magnus` が `serde_json::Value` へ双方向変換し、channel の
|
|
47
|
+
JSON codec が処理する。
|
|
48
|
+
|
|
49
|
+
async は extension 内に埋めた tokio runtime で `block_on` する。ブロッキング
|
|
50
|
+
呼び出しは `rb_thread_call_without_gvl` で **GVL を解放**するため、待機中も他の
|
|
51
|
+
Ruby スレッドは動き続ける(呼び出し自体の中断・タイムアウトは未対応 — 今後の
|
|
52
|
+
refinement)。
|
|
53
|
+
|
|
54
|
+
失敗はすべて `Unison::Error`(`< StandardError`)として raise される。
|
|
55
|
+
|
|
56
|
+
次フェーズ: GVL 解放中の呼び出しの中断(unblock function)、`recv` の timeout 版。
|
|
57
|
+
|
|
58
|
+
## ビルド・テスト
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
bundle install
|
|
62
|
+
bundle exec rake compile # native 拡張をビルド
|
|
63
|
+
bundle exec rake test # compile → 単体テスト(ネットワーク不要)
|
|
64
|
+
bundle exec rake test:e2e # compile → E2E(`unison mock` を subprocess 起動)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`rake test:e2e` は `unison` バイナリ(`cargo build -p unison-cli` で生成、または
|
|
68
|
+
`UNISON_MOCK_BIN` で指定)を要する。見つからない場合は skip される。
|
|
69
|
+
|
|
70
|
+
**Ruby 3.4 以上が必須。** Ruby 3.4 系と 4.0 系の両方で動作する。開発環境の
|
|
71
|
+
version は `.mise.toml` に固定(既定 3.4.9、 4.0.5 も同居)。
|
|
72
|
+
|
|
73
|
+
## ベンチマーク
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
ruby bench/bench.rb > bench/runs/<date>-<tag>.kdl
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`unison mock` を subprocess 起動し、(1) `Channel#request` の RTT / throughput と
|
|
80
|
+
(2) GVL 解放の効果(ブロッキング呼び出し中に背景スレッドが進む割合)を計測し、
|
|
81
|
+
structured-log KDL を出力する。run は `bench/runs/` に immutable に蓄積し、
|
|
82
|
+
`bench/index.kdl` が append-only インデックスとして参照する。
|
|
83
|
+
|
|
84
|
+
## 対応 protocol 世代
|
|
85
|
+
|
|
86
|
+
`1.0.0` GA — npm [`@chronista-club/unison-client@1.0.0`](https://www.npmjs.com/package/@chronista-club/unison-client) /
|
|
87
|
+
crates.io [`club-unison@1.0.0`](https://crates.io/crates/club-unison) と同世代。
|
|
88
|
+
gem 側の API は scaffold stage を抜けるまで gem 単独で stabilize する方針
|
|
89
|
+
(gem version は `0.x` 系)。
|
|
90
|
+
|
|
91
|
+
## インストール
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# Gemfile
|
|
95
|
+
gem "unison-client"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bundle install
|
|
100
|
+
# または
|
|
101
|
+
gem install unison-client
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
source-only gem(gem ファイルに Rust source を bundle)なので、 install 時に
|
|
105
|
+
**Rust toolchain (`rustc` / `cargo`、 1.85 以上推奨)** が要求される。 toolchain が
|
|
106
|
+
無い環境では rustup の `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
|
107
|
+
等で先に入れる。
|
|
108
|
+
|
|
109
|
+
> 将来的に platform-specific な prebuilt binary gem へ移行予定(nokogiri / grpc
|
|
110
|
+
> 等と同じ rake-compiler-dock + GitHub Actions matrix)。 0.1.0 では source-only。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# crate 名は拡張ターゲット名(unison_client)と一致させる。gem 名はハイフン
|
|
2
|
+
# 区切りの `unison-client`(gem 慣習)で、両者が別表記なのは想定どおり。
|
|
3
|
+
[package]
|
|
4
|
+
name = "unison_client"
|
|
5
|
+
version = "0.1.0"
|
|
6
|
+
edition = "2024"
|
|
7
|
+
publish = false
|
|
8
|
+
license = "MIT"
|
|
9
|
+
description = "Native extension for the unison-client Ruby gem — Magnus binding over club-unison."
|
|
10
|
+
|
|
11
|
+
[lib]
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
magnus = "0.8"
|
|
16
|
+
# club-unison 本体(QUIC / channel / wire)。crate の package 名は club-unison、
|
|
17
|
+
# lib 名は unison なので Rust 側は `use unison::...`。
|
|
18
|
+
club-unison = "1.0.0"
|
|
19
|
+
# block_on で async API を Ruby の同期呼び出しへ橋渡しするための runtime。
|
|
20
|
+
tokio = { version = "1", features = ["rt-multi-thread"] }
|
|
21
|
+
# channel payload の Ruby 値 ⇄ serde_json::Value 変換。serde_magnus は magnus
|
|
22
|
+
# エコシステム標準で、magnus version を一致させる必要がある(0.11 → magnus 0.8)。
|
|
23
|
+
serde_json = "1"
|
|
24
|
+
serde_magnus = "0.11"
|
|
25
|
+
# GVL 解放(rb_thread_call_without_gvl)の raw FFI。magnus 0.8 はこの C API を
|
|
26
|
+
# 安全ラッパとして公開していないため rb-sys を直接使う。magnus が間接依存する
|
|
27
|
+
# のと同じ crate(version 一致で unify)。
|
|
28
|
+
rb-sys = "0.9"
|
|
29
|
+
|
|
30
|
+
# cargo 単体(clippy / check)は rb-sys が Ruby ABI を要求するため通らない。
|
|
31
|
+
# ビルド・lint は `bundle exec rake compile` 経由で行う(rb_sys が Ruby env を
|
|
32
|
+
# 注入する)。
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
//! Native extension for the `unison-client` Ruby gem.
|
|
2
|
+
//!
|
|
3
|
+
//! Wraps the Rust `club-unison` crate as Ruby objects. The protocol itself
|
|
4
|
+
//! (QUIC transport, channel multiplexing, wire framing) lives in Rust; this
|
|
5
|
+
//! layer only bridges Ruby ⇄ Rust values and Ruby's blocking calls ⇄ the
|
|
6
|
+
//! async runtime.
|
|
7
|
+
//!
|
|
8
|
+
//! - `Unison::Client` — connection lifecycle, wraps `ProtocolClient`
|
|
9
|
+
//! - `Unison::Channel` — request/response + event push, wraps `UnisonChannel`
|
|
10
|
+
//! - `Unison::Error` — base class for every failure this binding raises
|
|
11
|
+
//!
|
|
12
|
+
//! Channel payloads cross the boundary as native Ruby values: `serde_magnus`
|
|
13
|
+
//! converts Ruby `Hash`/`Array`/… ⇄ `serde_json::Value`, which the channel's
|
|
14
|
+
//! JSON codec consumes.
|
|
15
|
+
//!
|
|
16
|
+
//! Blocking calls release the GVL while parked on the network (see
|
|
17
|
+
//! [`without_gvl`]), so other Ruby threads keep running.
|
|
18
|
+
|
|
19
|
+
use std::ffi::c_void;
|
|
20
|
+
use std::sync::OnceLock;
|
|
21
|
+
|
|
22
|
+
use magnus::{Error, ExceptionClass, Ruby, Value, function, method, prelude::*};
|
|
23
|
+
use serde_json::Value as JsonValue;
|
|
24
|
+
use tokio::runtime::Runtime;
|
|
25
|
+
use unison::{NetworkError, ProtocolClient, UnisonChannel};
|
|
26
|
+
|
|
27
|
+
/// Process-wide multi-thread tokio runtime backing every blocking bridge.
|
|
28
|
+
///
|
|
29
|
+
/// One long-lived runtime: the QUIC reactor should outlive individual calls,
|
|
30
|
+
/// and building a runtime per call would be wasteful. Created lazily on first
|
|
31
|
+
/// use.
|
|
32
|
+
fn runtime() -> &'static Runtime {
|
|
33
|
+
static RT: OnceLock<Runtime> = OnceLock::new();
|
|
34
|
+
RT.get_or_init(|| Runtime::new().expect("failed to build the Unison tokio runtime"))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Runs `f` with Ruby's GVL released, letting other Ruby threads proceed.
|
|
38
|
+
///
|
|
39
|
+
/// `f` MUST NOT touch any Ruby value or Ruby C API — it executes without the
|
|
40
|
+
/// GVL. It runs on the *calling* thread (`rb_thread_call_without_gvl` does not
|
|
41
|
+
/// move work elsewhere), so non-`Send` captures are fine.
|
|
42
|
+
///
|
|
43
|
+
/// Limitations, both future refinements:
|
|
44
|
+
/// - No unblock function is registered, so a blocked call cannot be
|
|
45
|
+
/// interrupted by Ruby (e.g. `Thread#kill`).
|
|
46
|
+
/// - A panic inside `f` crosses an `extern "C"` boundary and aborts the
|
|
47
|
+
/// process. The closures here (`block_on` of QUIC ops) are not expected to
|
|
48
|
+
/// panic.
|
|
49
|
+
fn without_gvl<F, R>(f: F) -> R
|
|
50
|
+
where
|
|
51
|
+
F: FnOnce() -> R,
|
|
52
|
+
{
|
|
53
|
+
/// Carries the closure in and its result out across the C boundary.
|
|
54
|
+
struct Payload<F, R> {
|
|
55
|
+
func: Option<F>,
|
|
56
|
+
result: Option<R>,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
unsafe extern "C" fn trampoline<F, R>(arg: *mut c_void) -> *mut c_void
|
|
60
|
+
where
|
|
61
|
+
F: FnOnce() -> R,
|
|
62
|
+
{
|
|
63
|
+
// SAFETY: `arg` is the `&mut Payload` passed below; Ruby invokes this
|
|
64
|
+
// exactly once, on this thread, before `rb_thread_call_without_gvl`
|
|
65
|
+
// returns.
|
|
66
|
+
let payload = unsafe { &mut *(arg as *mut Payload<F, R>) };
|
|
67
|
+
let func = payload.func.take().expect("without_gvl closure already ran");
|
|
68
|
+
payload.result = Some(func());
|
|
69
|
+
std::ptr::null_mut()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let mut payload = Payload::<F, R> {
|
|
73
|
+
func: Some(f),
|
|
74
|
+
result: None,
|
|
75
|
+
};
|
|
76
|
+
// SAFETY: `trampoline::<F, R>` interprets the data pointer as
|
|
77
|
+
// `&mut Payload<F, R>`, which is exactly what we hand it; no unblock
|
|
78
|
+
// function is used.
|
|
79
|
+
unsafe {
|
|
80
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
81
|
+
Some(trampoline::<F, R>),
|
|
82
|
+
(&raw mut payload).cast(),
|
|
83
|
+
None,
|
|
84
|
+
std::ptr::null_mut(),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
payload.result.expect("without_gvl closure did not run")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Current `Ruby` handle.
|
|
91
|
+
///
|
|
92
|
+
/// Safe inside any function bound into Ruby: such code only runs while a Ruby
|
|
93
|
+
/// method is on the stack, so the handle is always available.
|
|
94
|
+
fn ruby() -> Ruby {
|
|
95
|
+
Ruby::get().expect("Unison binding used outside a Ruby thread")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Builds a `Unison::Error` exception carrying `msg`.
|
|
99
|
+
///
|
|
100
|
+
/// `Unison::Error` is defined once in `init`; this re-fetches it via the
|
|
101
|
+
/// (idempotent) module handle rather than caching — error construction is
|
|
102
|
+
/// never a hot path.
|
|
103
|
+
fn unison_error(msg: impl Into<String>) -> Error {
|
|
104
|
+
let class: ExceptionClass = ruby()
|
|
105
|
+
.define_module("Unison")
|
|
106
|
+
.and_then(|m| m.const_get("Error"))
|
|
107
|
+
.expect("Unison::Error class is not defined");
|
|
108
|
+
Error::new(class, msg.into())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Turns a `NetworkError` into a `Unison::Error`.
|
|
112
|
+
fn net_err(e: NetworkError) -> Error {
|
|
113
|
+
unison_error(e.to_string())
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// The Unison protocol generation this client is built against.
|
|
117
|
+
fn protocol_target() -> &'static str {
|
|
118
|
+
"1.0.0-rc.1"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// `Unison::Client` — a QUIC-backed Unison protocol client.
|
|
122
|
+
///
|
|
123
|
+
/// `ProtocolClient`'s methods all take `&self`, so no interior mutability is
|
|
124
|
+
/// needed: the wrapped value is shared read-only and the QUIC state is
|
|
125
|
+
/// managed internally by the Rust crate.
|
|
126
|
+
#[magnus::wrap(class = "Unison::Client", free_immediately, size)]
|
|
127
|
+
struct Client {
|
|
128
|
+
inner: ProtocolClient,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
impl Client {
|
|
132
|
+
/// `Unison::Client.new` — builds a default QUIC-backed client.
|
|
133
|
+
///
|
|
134
|
+
/// Does not open a connection; call `#connect` for that.
|
|
135
|
+
///
|
|
136
|
+
/// **Warning**: backed by `ProtocolClient::new_default()`, which builds an
|
|
137
|
+
/// **insecure** client — TLS certificate verification is skipped (intended
|
|
138
|
+
/// for loopback / development). A secure constructor taking explicit trust
|
|
139
|
+
/// anchors is future work.
|
|
140
|
+
fn new() -> Result<Self, Error> {
|
|
141
|
+
let inner = ProtocolClient::new_default()
|
|
142
|
+
.map_err(|e| unison_error(format!("Unison::Client.new failed: {e}")))?;
|
|
143
|
+
Ok(Self { inner })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// `client.connect(url)` — opens the QUIC connection to `url`.
|
|
147
|
+
///
|
|
148
|
+
/// Blocks the calling thread until the handshake completes (the GVL is
|
|
149
|
+
/// released, so other Ruby threads keep running). Raises `Unison::Error`
|
|
150
|
+
/// on failure.
|
|
151
|
+
fn connect(&self, url: String) -> Result<(), Error> {
|
|
152
|
+
without_gvl(|| runtime().block_on(self.inner.connect(&url))).map_err(net_err)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// `client.connected?` — whether the QUIC connection is currently open.
|
|
156
|
+
fn connected(&self) -> bool {
|
|
157
|
+
without_gvl(|| runtime().block_on(self.inner.is_connected()))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// `client.disconnect` — closes the QUIC connection.
|
|
161
|
+
///
|
|
162
|
+
/// Raises `Unison::Error` only if the close itself errors.
|
|
163
|
+
fn disconnect(&self) -> Result<(), Error> {
|
|
164
|
+
without_gvl(|| runtime().block_on(self.inner.disconnect())).map_err(net_err)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// `client.open_channel(name)` — opens a named channel, returning a
|
|
168
|
+
/// `Unison::Channel`. Raises `Unison::Error` on failure.
|
|
169
|
+
fn open_channel(&self, name: String) -> Result<Channel, Error> {
|
|
170
|
+
let inner = without_gvl(|| runtime().block_on(self.inner.open_channel(&name)))
|
|
171
|
+
.map_err(net_err)?;
|
|
172
|
+
Ok(Channel { inner })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// `Unison::Channel` — a request/response + event-push channel.
|
|
177
|
+
///
|
|
178
|
+
/// Constructed only via `Unison::Client#open_channel`; it has no public
|
|
179
|
+
/// allocator. Payloads are native Ruby values (`Hash`/`Array`/scalars),
|
|
180
|
+
/// carried over the channel's JSON codec.
|
|
181
|
+
#[magnus::wrap(class = "Unison::Channel", free_immediately, size)]
|
|
182
|
+
struct Channel {
|
|
183
|
+
inner: UnisonChannel,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
impl Channel {
|
|
187
|
+
/// `channel.request(method, payload)` — sends a request and blocks until
|
|
188
|
+
/// the matching response arrives. Returns the response payload as a Ruby
|
|
189
|
+
/// value. Raises `Unison::Error` on a protocol error or timeout.
|
|
190
|
+
fn request(&self, method: String, payload: Value) -> Result<Value, Error> {
|
|
191
|
+
let ruby = ruby();
|
|
192
|
+
// Ruby → Rust conversion needs the GVL; do it before releasing it.
|
|
193
|
+
let req: JsonValue = serde_magnus::deserialize(&ruby, payload)?;
|
|
194
|
+
let resp: JsonValue = without_gvl(|| {
|
|
195
|
+
runtime().block_on(self.inner.request::<JsonValue, JsonValue>(&method, &req))
|
|
196
|
+
})
|
|
197
|
+
.map_err(net_err)?;
|
|
198
|
+
serde_magnus::serialize(&ruby, &resp)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// `channel.send_event(method, payload)` — sends a fire-and-forget event
|
|
202
|
+
/// (no response is awaited).
|
|
203
|
+
fn send_event(&self, method: String, payload: Value) -> Result<(), Error> {
|
|
204
|
+
let event: JsonValue = serde_magnus::deserialize(&ruby(), payload)?;
|
|
205
|
+
without_gvl(|| runtime().block_on(self.inner.send_event(&method, &event)))
|
|
206
|
+
.map_err(net_err)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// `channel.recv` — blocks until the next inbound event (server push or
|
|
210
|
+
/// other non-response message), returned as a Ruby `Hash` with keys
|
|
211
|
+
/// `"id"`, `"type"`, `"method"`, `"payload"`.
|
|
212
|
+
///
|
|
213
|
+
/// The GVL is released while waiting, so other Ruby threads run; the call
|
|
214
|
+
/// itself is not interruptible and has no timeout (future refinements).
|
|
215
|
+
fn recv(&self) -> Result<Value, Error> {
|
|
216
|
+
let msg = without_gvl(|| runtime().block_on(self.inner.recv())).map_err(net_err)?;
|
|
217
|
+
let payload = msg.payload_as_value().map_err(net_err)?;
|
|
218
|
+
let out = serde_json::json!({
|
|
219
|
+
"id": msg.id,
|
|
220
|
+
"type": msg.msg_type,
|
|
221
|
+
"method": msg.method,
|
|
222
|
+
"payload": payload,
|
|
223
|
+
});
|
|
224
|
+
serde_magnus::serialize(&ruby(), &out)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// `channel.close` — closes the channel and stops its receive loop.
|
|
228
|
+
fn close(&self) -> Result<(), Error> {
|
|
229
|
+
without_gvl(|| runtime().block_on(self.inner.close())).map_err(net_err)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[magnus::init]
|
|
234
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
235
|
+
let module = ruby.define_module("Unison")?;
|
|
236
|
+
module.define_module_function("protocol_target", function!(protocol_target, 0))?;
|
|
237
|
+
|
|
238
|
+
// `Unison::Error` — base class for every failure raised by this binding.
|
|
239
|
+
module.define_error("Error", ruby.exception_standard_error())?;
|
|
240
|
+
|
|
241
|
+
let client = module.define_class("Client", ruby.class_object())?;
|
|
242
|
+
client.define_singleton_method("new", function!(Client::new, 0))?;
|
|
243
|
+
client.define_method("connect", method!(Client::connect, 1))?;
|
|
244
|
+
client.define_method("connected?", method!(Client::connected, 0))?;
|
|
245
|
+
client.define_method("disconnect", method!(Client::disconnect, 0))?;
|
|
246
|
+
client.define_method("open_channel", method!(Client::open_channel, 1))?;
|
|
247
|
+
|
|
248
|
+
let channel = module.define_class("Channel", ruby.class_object())?;
|
|
249
|
+
channel.define_method("request", method!(Channel::request, 2))?;
|
|
250
|
+
channel.define_method("send_event", method!(Channel::send_event, 2))?;
|
|
251
|
+
channel.define_method("recv", method!(Channel::recv, 0))?;
|
|
252
|
+
channel.define_method("close", method!(Channel::close, 0))?;
|
|
253
|
+
|
|
254
|
+
Ok(())
|
|
255
|
+
}
|
data/lib/unison.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "unison/version"
|
|
4
|
+
|
|
5
|
+
# Load the Rust native extension (built by `rake compile` into
|
|
6
|
+
# `lib/unison_client/`). It defines the Rust-backed `Unison` module functions.
|
|
7
|
+
require "unison_client/unison_client"
|
|
8
|
+
|
|
9
|
+
# Unison — a Ruby client for the Unison protocol.
|
|
10
|
+
#
|
|
11
|
+
# This gem is a thin **language binding**, not a re-implementation: the
|
|
12
|
+
# protocol itself (QUIC transport, channel multiplexing, wire framing) lives
|
|
13
|
+
# in the Rust `club-unison` crate. The native extension (Magnus) wraps that
|
|
14
|
+
# crate's `ProtocolClient`.
|
|
15
|
+
#
|
|
16
|
+
# `Unison::Client` wraps the crate's `ProtocolClient` (connection lifecycle:
|
|
17
|
+
# `new` / `connect` / `connected?` / `disconnect` / `open_channel`), and
|
|
18
|
+
# `Unison::Channel` wraps `UnisonChannel` (`request` / `send_event` / `recv` /
|
|
19
|
+
# `close`). Channel payloads are native Ruby values.
|
|
20
|
+
module Unison
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: unison-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mako
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
26
|
+
description: A thin Ruby binding over the Rust `club-unison` crate's ProtocolClient,
|
|
27
|
+
via a Magnus native extension. The protocol — QUIC transport, channel multiplexing,
|
|
28
|
+
wire framing — is implemented in Rust; this gem is the language binding.
|
|
29
|
+
email:
|
|
30
|
+
- mito@chronista.club
|
|
31
|
+
executables: []
|
|
32
|
+
extensions:
|
|
33
|
+
- ext/unison_client/extconf.rb
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- Cargo.lock
|
|
37
|
+
- Cargo.toml
|
|
38
|
+
- README.md
|
|
39
|
+
- ext/unison_client/Cargo.toml
|
|
40
|
+
- ext/unison_client/extconf.rb
|
|
41
|
+
- ext/unison_client/src/lib.rs
|
|
42
|
+
- lib/unison.rb
|
|
43
|
+
- lib/unison/version.rb
|
|
44
|
+
homepage: https://github.com/chronista-club/club-unison
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata: {}
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.4.0
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.6.9
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Ruby client for the Unison protocol.
|
|
65
|
+
test_files: []
|