citrine-remote 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 +104 -0
- data/lib/citrine/remote/browser_ws_wire.rb +20 -0
- data/lib/citrine/remote/hub.rb +44 -0
- data/lib/citrine/remote/jsonl_wire.rb +33 -0
- data/lib/citrine/remote/session.rb +39 -0
- data/lib/citrine/remote/signal.rb +38 -0
- data/lib/citrine/remote/version.rb +5 -0
- data/lib/citrine/remote/wire_box.rb +30 -0
- data/lib/citrine/remote/ws_wire.rb +74 -0
- data/lib/citrine/remote.rb +23 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: acc440381ae676270dea4fb9c762015773ad20dc4e62c84b456ca548ebc46edd
|
|
4
|
+
data.tar.gz: 63c2f4ea3b6eacbb4ca2ec9e02e95d949f20450a01b501e06f581abb19a632e4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0ae758da6eac4c119a5e646b17e91c25f50953454ec71db41479be8fe2e0a2c8ff70d59e6637f4c550aa5637a8a5d2f162e8cb456e3662df5be1c7b084f163b4
|
|
7
|
+
data.tar.gz: ac024f18565080b922c3655f023aa2cc251ec23892b7c7545d1097b1e0e3cf2a18d88529e876b120ea8242d41ebca8cae976ee7d6e9fda5ec64ebbe5cd5ac02b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shiningray
|
|
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,104 @@
|
|
|
1
|
+
# Citrine::Remote — 服务器权威状态的前端镜像原语
|
|
2
|
+
|
|
3
|
+
从 citrine-remote spike 沉淀的可复用组件。核心主张:**视图只是信号的投影**——
|
|
4
|
+
只要把服务器状态以 Signal 形态镜像到前端,整个 Citrine 视图层(Effect/Component/
|
|
5
|
+
渲染器)零修改获得实时同步。前端状态 = 服务器共享状态的镜像 + 本地瞬时 UI 状态。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem install citrine-remote # 依赖 citrine (>= 0.2.0),自动带入
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
或 Gemfile:`gem "citrine-remote"`。仓库内的 examples/ 需克隆本仓库后按下文运行
|
|
14
|
+
(示例用工作区内的 citrine 源码,不消费 rubygems 版本)。
|
|
15
|
+
|
|
16
|
+
目录布局对齐 gem 惯例,未来可原样迁入 `citrine/lib` 或独立成 gem。
|
|
17
|
+
|
|
18
|
+
## 架构
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
权威侧(CRuby 服务器) 镜像侧(CRuby / Opal 浏览器)
|
|
22
|
+
Citrine::Signal(普通信号) Citrine::Remote::Session(注册表+分发)
|
|
23
|
+
↓ Effect 广播 ↓ apply_remote(广播但不回传,防回声)
|
|
24
|
+
Citrine::Remote::Hub Citrine::Remote::Signal
|
|
25
|
+
│ patch 流 │ set = 乐观生效 + propose
|
|
26
|
+
└────── Wire(传输适配器,双向) ─────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Wire 契约(回调式鸭子类型,两侧一致):
|
|
30
|
+
|
|
31
|
+
| 方法 | 语义 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `send_msg(hash)` | 发送一条消息 |
|
|
34
|
+
| `on_msg { |hash| }` | 收包回调(字符串键,Session 负责归一化) |
|
|
35
|
+
| `on_close { }` | 断线回调 |
|
|
36
|
+
|
|
37
|
+
消息协议 v0(SPEC-endpoint-protocol 信封的退化形):
|
|
38
|
+
`propose`(client→server,写入提案)与 `patch`(server→client,权威广播)。
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "citrine/remote" # 平台无关四件(Opal 可编译)
|
|
44
|
+
require "citrine/remote/jsonl_wire" # CRuby TCP 适配器(按需)
|
|
45
|
+
require "citrine/remote/ws_wire" # CRuby WS 服务器适配器(按需,spike 级手写)
|
|
46
|
+
require "citrine/remote/browser_ws_wire" # Opal 浏览器原生 WS 适配器(按需)
|
|
47
|
+
|
|
48
|
+
# 权威侧:发布信号。Effect 首跑即快照,之后每次 set 自动增量下发。
|
|
49
|
+
hub = Citrine::Remote::Hub.new(wire)
|
|
50
|
+
hub.publish("win:1:geom", geom_signal)
|
|
51
|
+
hub.watch("wm:summary") { list_signal.get.map { ... } } # 派生值:块内读到的信号即依赖
|
|
52
|
+
hub.unpublish("win:1:geom")
|
|
53
|
+
hub.dispose
|
|
54
|
+
|
|
55
|
+
# 镜像侧:注册表跨连接存活(WireBox 换线),视图只读信号。
|
|
56
|
+
box = Citrine::Remote::WireBox.new
|
|
57
|
+
session = Citrine::Remote::Session.new(box)
|
|
58
|
+
session.on_patch = ->(key) { ... } # 可选:来源标注(先于渲染触发)
|
|
59
|
+
box.wire = transport # 任一 Wire 契约实现
|
|
60
|
+
geom = session.sig("win:1:geom", 初始值) # 同一 key 恒为同一实例
|
|
61
|
+
geom.get # 视图订阅,本地命中零延迟
|
|
62
|
+
geom.set(v) # 乐观生效 + 提案(服务器可回滚)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 运行
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 本目录即 .ruby-version(3.3.5)
|
|
69
|
+
rake # 16 项测试(纯 CRuby + 内存 wire)
|
|
70
|
+
|
|
71
|
+
ruby examples/window_server.rb # 终端 1:demo1 权威侧
|
|
72
|
+
ruby examples/window_client.rb # 终端 2:demo1 镜像侧(单窗口,拖拽提案/回滚)
|
|
73
|
+
|
|
74
|
+
ruby examples/desktop_server.rb # 终端 1:demo2 桌面权威侧(多窗口+断线重连)
|
|
75
|
+
ruby examples/desktop_client.rb # 终端 2:demo2 镜像侧(ASCII 渲染)
|
|
76
|
+
|
|
77
|
+
ruby examples/browser/ws_server.rb # 浏览器版服务器(无限循环模式)
|
|
78
|
+
open http://127.0.0.1:4470/ # 真 DOM 桌面投影
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
浏览器端改动 `examples/browser/app.rb` 后重新编译:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd ../citrine && bundle exec opal -c -Ilib -I../citrine-remote/lib -I../citrine-remote/examples/browser \
|
|
85
|
+
-o ../citrine-remote/examples/browser/app.js ../citrine-remote/examples/browser/app.rb
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 设计要点(spike 结论)
|
|
89
|
+
|
|
90
|
+
- **Signal 是远程同步的正确粒度**:视图对远程零感知,ASCII 与 DOM 两个渲染器同验。
|
|
91
|
+
- **防回声在原语内**:`apply_remote`(生效+广播+不回传)一个方法终结回声循环。
|
|
92
|
+
- **幂等收敛白捡**:相等短路使重连快照零冗余渲染;`Effect` 首跑即快照。
|
|
93
|
+
- **权威侧零新原语**:普通 Signal + Effect 即广播器;列表 Effect reconcile 出
|
|
94
|
+
每窗口的 publish 生命周期(见 `world.rb` 的 `DesktopMirror`)。
|
|
95
|
+
- **平台无关纪律**:`remote.rb` 入口禁止 Thread/Mutex/IO/Native——这是 Opal 编译
|
|
96
|
+
一次通过的原因。阻塞循环只能待在 CRuby 适配器内部。
|
|
97
|
+
|
|
98
|
+
## 已知限制与下一步
|
|
99
|
+
|
|
100
|
+
- [ ] `Citrine::Effect.stack` 跨线程共享——CRuby 多连接服务器需内核线程本地化
|
|
101
|
+
- [ ] Wire 回调槽独占制——正式契约应多监听者或显式独占标注
|
|
102
|
+
- [ ] 断线窗口期提案丢失——正式协议需提案排队 + ack
|
|
103
|
+
- [ ] 部分状态间隙(列表 patch 先于几何 patch)——视图须容忍,或引入复合消息/barrier
|
|
104
|
+
- [ ] 消息信封升格 SPEC-endpoint-protocol(message_id / ask-result / priority)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
require "native"
|
|
3
|
+
|
|
4
|
+
module Citrine
|
|
5
|
+
module Remote
|
|
6
|
+
# Opal 传输适配器:浏览器原生 WebSocket。仅 Opal 环境加载。
|
|
7
|
+
# 规则:绝不在 Opal 里手写 WS 协议——浏览器给什么用什么。
|
|
8
|
+
class BrowserWsWire
|
|
9
|
+
def initialize(url)
|
|
10
|
+
@ws = Native(`new WebSocket(#{url})`)
|
|
11
|
+
@ws.addEventListener("message", ->(ev) { @on_msg&.call(JSON.parse(Native(ev)[:data])) })
|
|
12
|
+
@ws.addEventListener("close", ->(*) { @on_close&.call })
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def send_msg(h) = @ws.send(JSON.generate(h))
|
|
16
|
+
def on_msg(&b) = @on_msg = b
|
|
17
|
+
def on_close(&b) = @on_close = b
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Citrine
|
|
2
|
+
module Remote
|
|
3
|
+
# 权威侧广播器:把本地信号发布为远程可镜像的 patch 流。
|
|
4
|
+
# 权威侧不需要新原语——普通 Citrine::Signal + Effect 即广播器:
|
|
5
|
+
# Effect 创建即首跑,所以连接建立(Hub 创建或 publish 调用)立刻下发快照;
|
|
6
|
+
# 之后每次 set 自动作为增量 patch 下发;unpublish/dispose 停止。
|
|
7
|
+
class Hub
|
|
8
|
+
def initialize(wire)
|
|
9
|
+
@wire = wire
|
|
10
|
+
@pubs = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# 发布一个信号:每次变更广播一条 patch,首跑即发当前值(快照)
|
|
14
|
+
def publish(key, signal)
|
|
15
|
+
watch(key) { signal.get }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 发布一个派生值:块内读到的信号即依赖(如列表摘要、计算属性)
|
|
19
|
+
def watch(key, &block)
|
|
20
|
+
unpublish(key)
|
|
21
|
+
@pubs[key] = Citrine::Effect.create { emit_patch(key, block.call) }
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unpublish(key)
|
|
26
|
+
@pubs.delete(key)&.dispose
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dispose
|
|
31
|
+
@pubs.keys.each { |k| unpublish(k) }
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def emit_patch(key, value)
|
|
38
|
+
@wire.send_msg("kind" => "patch", "key" => key, "value" => value)
|
|
39
|
+
rescue StandardError # 写已断开的连接:自愈(Opal 侧 JS 异常同口径)
|
|
40
|
+
dispose
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Citrine
|
|
2
|
+
module Remote
|
|
3
|
+
# CRuby 传输适配器:JSON Lines over TCPSocket。
|
|
4
|
+
# 阻塞读循环藏在 #start 的线程里,对外只暴露回调契约。仅 CRuby 加载。
|
|
5
|
+
class JsonlWire
|
|
6
|
+
def initialize(io)
|
|
7
|
+
@io = io
|
|
8
|
+
@wlock = Mutex.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def on_msg(&b) = @on_msg = b
|
|
12
|
+
def on_close(&b) = @on_close = b
|
|
13
|
+
|
|
14
|
+
def send_msg(h)
|
|
15
|
+
@wlock.synchronize do
|
|
16
|
+
@io.write(JSON.generate(h) + "\n")
|
|
17
|
+
@io.flush
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
Thread.new do
|
|
23
|
+
begin
|
|
24
|
+
@io.each_line { |l| @on_msg&.call(JSON.parse(l)) }
|
|
25
|
+
rescue SystemCallError, IOError, EOFError, JSON::ParserError
|
|
26
|
+
end
|
|
27
|
+
@on_close&.call
|
|
28
|
+
end
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Citrine
|
|
2
|
+
module Remote
|
|
3
|
+
# 客户端会话:消息分发 + 信号注册表(key → Signal)。
|
|
4
|
+
# 视图层只读 #sig 拿到的信号,对"远程"零感知。
|
|
5
|
+
class Session
|
|
6
|
+
# on_patch:每条 patch 应用**之前**回调(广播是同步渲染,标注来源必须先置位)
|
|
7
|
+
attr_accessor :on_patch
|
|
8
|
+
|
|
9
|
+
def initialize(box)
|
|
10
|
+
@box = box
|
|
11
|
+
@registry = {}
|
|
12
|
+
box.on_msg { |m| dispatch(m) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# 同一 key 始终返回同一个信号实例(注册表即身份映射)
|
|
16
|
+
def sig(key, init)
|
|
17
|
+
@registry[key] ||= Signal.new(key, init, wire: @box)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dispatch(msg)
|
|
21
|
+
return unless msg["kind"] == "patch"
|
|
22
|
+
|
|
23
|
+
@on_patch&.call(msg["key"])
|
|
24
|
+
sig(msg["key"], nil).apply_remote(deep_sym(msg["value"]))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# 消息一律字符串键(两侧 JSON 解析口径不同),值统一转符号
|
|
30
|
+
def deep_sym(v)
|
|
31
|
+
case v
|
|
32
|
+
when Hash then v.each_with_object({}) { |(k, val), h| h[k.to_sym] = deep_sym(val) }
|
|
33
|
+
when Array then v.map { |x| deep_sym(x) }
|
|
34
|
+
else v
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Citrine
|
|
2
|
+
module Remote
|
|
3
|
+
# 远程镜像信号:一个普通的 Citrine::Signal,写入路径分两条腿——
|
|
4
|
+
#
|
|
5
|
+
# #set(v) 本地写入 = 乐观生效 + 向服务器提交提案(propose)。
|
|
6
|
+
# 服务器是权威,有权修正:修正经 patch 回滚本地值。
|
|
7
|
+
# #apply_remote(v) 服务器下行:生效并广播(视图重渲染),但不回传。
|
|
8
|
+
# 防回声的关键操作——回声循环终结在原语里,与应用无关。
|
|
9
|
+
#
|
|
10
|
+
# 相等短路继承自 Citrine::Signal:重连快照里值未变的 patch 零渲染,
|
|
11
|
+
# 幂等收敛是底层语义白捡的,协议层无需设计。
|
|
12
|
+
class Signal < Citrine::Signal
|
|
13
|
+
attr_reader :key
|
|
14
|
+
|
|
15
|
+
def initialize(key, value, wire:)
|
|
16
|
+
super(value)
|
|
17
|
+
@key = key
|
|
18
|
+
@wire = wire
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set(v)
|
|
22
|
+
return self if v == peek
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
@wire.send_msg("kind" => "propose", "key" => @key, "value" => v)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def apply_remote(v)
|
|
30
|
+
return self if v == peek
|
|
31
|
+
|
|
32
|
+
@value = v
|
|
33
|
+
broadcast
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Citrine
|
|
2
|
+
module Remote
|
|
3
|
+
# 可换底层连接的转发器:重连时替换 #wire,信号注册表跨连接存活。
|
|
4
|
+
# 回调注册在盒子上(盒子稳定、wire 是耗材),换线自动转挂。
|
|
5
|
+
# 注意:wire 的回调槽归本盒子独占——同一 wire 上不要再注册别的 on_msg/on_close。
|
|
6
|
+
# 断线期间的本地提案直接丢弃(真实系统应排队或拒绝——v0 从简)。
|
|
7
|
+
class WireBox
|
|
8
|
+
def initialize
|
|
9
|
+
@wire = nil
|
|
10
|
+
@on_msg = nil
|
|
11
|
+
@on_close = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def wire=(w)
|
|
15
|
+
@wire = w
|
|
16
|
+
w.on_msg { |m| @on_msg&.call(m) }
|
|
17
|
+
w.on_close { @on_close&.call }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def on_msg(&b) = @on_msg = b
|
|
21
|
+
def on_close(&b) = @on_close = b
|
|
22
|
+
|
|
23
|
+
def send_msg(h)
|
|
24
|
+
@wire&.send_msg(h)
|
|
25
|
+
rescue StandardError # 死线写入(IOError/SystemCallError;Opal 侧 JS 异常同口径):丢弃
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require "digest/sha1"
|
|
2
|
+
require "base64"
|
|
3
|
+
|
|
4
|
+
module Citrine
|
|
5
|
+
module Remote
|
|
6
|
+
# CRuby 传输适配器:零依赖手写 WebSocket 服务器端(RFC6455 文本帧、
|
|
7
|
+
# 客户端帧带掩码)。spike 级实现——正式部署应换现成 gem 或 AgentOS 传输。
|
|
8
|
+
# 仅 CRuby 加载。用法:sock = TCPServer.accept 后 WsWire.new(sock) 完成握手。
|
|
9
|
+
class WsWire
|
|
10
|
+
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
11
|
+
|
|
12
|
+
def initialize(sock)
|
|
13
|
+
@sock = sock
|
|
14
|
+
@wlock = Mutex.new
|
|
15
|
+
sock.gets # 请求行
|
|
16
|
+
headers = {}
|
|
17
|
+
while (line = sock.gets) && line != "\r\n"
|
|
18
|
+
k, v = line.split(": ", 2)
|
|
19
|
+
headers[k.downcase] = v.strip
|
|
20
|
+
end
|
|
21
|
+
accept = Base64.strict_encode64(Digest::SHA1.digest(headers["sec-websocket-key"] + GUID))
|
|
22
|
+
sock.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n" \
|
|
23
|
+
"Connection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_msg(&b) = @on_msg = b
|
|
27
|
+
def on_close(&b) = @on_close = b
|
|
28
|
+
|
|
29
|
+
def send_msg(h)
|
|
30
|
+
payload = JSON.generate(h)
|
|
31
|
+
head = [0x81].pack("C")
|
|
32
|
+
len = payload.bytesize
|
|
33
|
+
head += if len < 126 then [len].pack("C")
|
|
34
|
+
elsif len < 65_536 then [126, len].pack("Cn")
|
|
35
|
+
else [127, len].pack("CQ>")
|
|
36
|
+
end
|
|
37
|
+
@wlock.synchronize { @sock.write(head + payload) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start
|
|
41
|
+
Thread.new do
|
|
42
|
+
begin
|
|
43
|
+
loop { read_frame }
|
|
44
|
+
rescue SystemCallError, IOError, EOFError, JSON::ParserError
|
|
45
|
+
end
|
|
46
|
+
@on_close&.call
|
|
47
|
+
end
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def read_frame
|
|
54
|
+
b1, b2 = read_exact(2).unpack("CC")
|
|
55
|
+
opcode = b1 & 0x0F
|
|
56
|
+
len = b2 & 0x7F
|
|
57
|
+
len = read_exact(2).unpack1("n") if len == 126
|
|
58
|
+
len = read_exact(8).unpack1("Q>") if len == 127
|
|
59
|
+
mask = (b2 & 0x80).zero? ? nil : read_exact(4).unpack("C4")
|
|
60
|
+
payload = read_exact(len)
|
|
61
|
+
payload = payload.bytes.each_with_index.map { |b, i| b ^ mask[i % 4] }.pack("C*") if mask
|
|
62
|
+
case opcode
|
|
63
|
+
when 1 then @on_msg&.call(JSON.parse(payload))
|
|
64
|
+
when 8 then raise EOFError
|
|
65
|
+
when 9 then @wlock.synchronize { @sock.write([0x8A, payload.bytesize].pack("CC") + payload) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_exact(n)
|
|
70
|
+
n.zero? ? "" : (@sock.read(n) or raise EOFError)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Citrine::Remote — 服务器权威状态的前端镜像原语。
|
|
2
|
+
#
|
|
3
|
+
# 平台无关核心纪律:本入口加载的四个文件禁止出现 Thread/Mutex/IO/Native,
|
|
4
|
+
# CRuby 与 Opal 均可加载。传输适配器按需单独 require:
|
|
5
|
+
# CRuby: citrine/remote/jsonl_wire(TCP)· citrine/remote/ws_wire(WS 服务器)
|
|
6
|
+
# Opal: citrine/remote/browser_ws_wire(浏览器原生 WebSocket)
|
|
7
|
+
#
|
|
8
|
+
# Wire 契约(回调式鸭子类型,两侧一致):
|
|
9
|
+
# send_msg(hash) 发送一条消息
|
|
10
|
+
# on_msg { |hash| } 收包回调(消息一律字符串键,由 Session 归一化)
|
|
11
|
+
# on_close { } 断线回调
|
|
12
|
+
#
|
|
13
|
+
# 消息协议(v0,SPEC-endpoint-protocol 信封的退化形):
|
|
14
|
+
# client→server {"kind":"propose","key":...,"value":...} 写入提案
|
|
15
|
+
# server→client {"kind":"patch", "key":...,"value":...} 权威广播
|
|
16
|
+
require "json"
|
|
17
|
+
require "citrine/signal"
|
|
18
|
+
|
|
19
|
+
require "citrine/remote/version"
|
|
20
|
+
require "citrine/remote/signal"
|
|
21
|
+
require "citrine/remote/wire_box"
|
|
22
|
+
require "citrine/remote/session"
|
|
23
|
+
require "citrine/remote/hub"
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: citrine-remote
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ShiningRay
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: citrine
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.2.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.2.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: minitest
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '5.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '5.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
description: Citrine::Remote:把服务器权威状态以 Signal 形态镜像到前端。视图层零修改获得实时同步——前端状态 = 服务器共享状态的镜像
|
|
56
|
+
+ 本地瞬时 UI 状态。提供 Remote::Signal(乐观提案/权威回滚)、Hub(权威侧广播器)、Session(镜像侧分发)与可替换的传输适配器(CRuby
|
|
57
|
+
TCP/WebSocket、Opal 浏览器原生 WebSocket)。
|
|
58
|
+
email:
|
|
59
|
+
- shiningray@users.noreply.github.com
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- LICENSE
|
|
65
|
+
- README.md
|
|
66
|
+
- lib/citrine/remote.rb
|
|
67
|
+
- lib/citrine/remote/browser_ws_wire.rb
|
|
68
|
+
- lib/citrine/remote/hub.rb
|
|
69
|
+
- lib/citrine/remote/jsonl_wire.rb
|
|
70
|
+
- lib/citrine/remote/session.rb
|
|
71
|
+
- lib/citrine/remote/signal.rb
|
|
72
|
+
- lib/citrine/remote/version.rb
|
|
73
|
+
- lib/citrine/remote/wire_box.rb
|
|
74
|
+
- lib/citrine/remote/ws_wire.rb
|
|
75
|
+
homepage: https://github.com/ShiningRay/citrine-remote
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
source_code_uri: https://github.com/ShiningRay/citrine-remote
|
|
80
|
+
changelog_uri: https://github.com/ShiningRay/citrine-remote/blob/main/README.md
|
|
81
|
+
rubygems_mfa_required: 'true'
|
|
82
|
+
post_install_message:
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: 3.1.0
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 3.5.22
|
|
98
|
+
signing_key:
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: 'Server-authoritative state mirroring for Citrine: views stay plain Signal
|
|
101
|
+
projections'
|
|
102
|
+
test_files: []
|