citrine 0.2.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 +249 -0
- data/bin/citrine +39 -0
- data/desktop/main.swift +44 -0
- data/lib/citrine/browser.rb +11 -0
- data/lib/citrine/canvas.rb +503 -0
- data/lib/citrine/component.rb +692 -0
- data/lib/citrine/debug.rb +109 -0
- data/lib/citrine/dev_server.rb +305 -0
- data/lib/citrine/dom.rb +292 -0
- data/lib/citrine/event.rb +31 -0
- data/lib/citrine/key_event.rb +39 -0
- data/lib/citrine/list_signal.rb +354 -0
- data/lib/citrine/node.rb +36 -0
- data/lib/citrine/num.rb +75 -0
- data/lib/citrine/packager.rb +129 -0
- data/lib/citrine/reactive.rb +40 -0
- data/lib/citrine/renderer.rb +828 -0
- data/lib/citrine/signal.rb +236 -0
- data/lib/citrine/sourcemap.rb +143 -0
- data/lib/citrine/string_renderer.rb +116 -0
- data/lib/citrine/style.rb +79 -0
- data/lib/citrine/theme.rb +99 -0
- data/lib/citrine/version.rb +5 -0
- data/lib/citrine.rb +92 -0
- data/lib/rubocop/cop/citrine/no_raw_ivar_assignment.rb +75 -0
- metadata +157 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DevTools 构建(T-B3):显式 `require "citrine/debug"` 才会加载本文件——
|
|
4
|
+
# 生产构建不 require 它,产物中不含任何埋点字符串与依赖图导出代码。
|
|
5
|
+
#
|
|
6
|
+
# 能力:
|
|
7
|
+
# 1. 依赖图数据层:Citrine.debug_dependency_graph 导出 signal→effect 边
|
|
8
|
+
# 与重跑计数快照(可 to_json,供 Cytoscape 依赖图 UI 与信号泳道时序消费)。
|
|
9
|
+
# 2. 诊断接口:Effect#disposed? / debug_info、Signal#debug_info。
|
|
10
|
+
#
|
|
11
|
+
# 实现方式:以 prepend 模块包住 Signal/Effect 的 initialize/run——
|
|
12
|
+
# 内核方法保持原样,不因埋点改变行为。
|
|
13
|
+
module Citrine
|
|
14
|
+
# DevTools 埋点(仅 citrine/debug 加载时生效)
|
|
15
|
+
module Debug
|
|
16
|
+
module SignalClassTracking
|
|
17
|
+
attr_accessor :debug_tracking
|
|
18
|
+
|
|
19
|
+
def all
|
|
20
|
+
@all ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def track(signal)
|
|
24
|
+
all << signal if debug_tracking
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module EffectClassTracking
|
|
29
|
+
attr_accessor :debug_tracking
|
|
30
|
+
|
|
31
|
+
def all
|
|
32
|
+
@all ||= []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def track(effect)
|
|
36
|
+
all << effect if debug_tracking
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module SignalInstanceTracking
|
|
41
|
+
def initialize(*args, &block)
|
|
42
|
+
super
|
|
43
|
+
Signal.track(self)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def debug_info
|
|
47
|
+
{ id: object_id, subscribers: @subs.size }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module EffectInstanceTracking
|
|
52
|
+
def initialize(*args, &block)
|
|
53
|
+
super
|
|
54
|
+
Effect.track(self)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run(*args, &block)
|
|
58
|
+
super.tap { @runs = (@runs || 0) + 1 }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def disposed?
|
|
62
|
+
@deps.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def debug_info
|
|
66
|
+
{ id: object_id, deps: (@deps || []).map(&:object_id), runs: @runs, disposed: disposed? }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class << Signal
|
|
71
|
+
prepend SignalClassTracking
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << Effect
|
|
75
|
+
prepend EffectClassTracking
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class Signal
|
|
80
|
+
prepend Debug::SignalInstanceTracking
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Effect
|
|
84
|
+
prepend Debug::EffectInstanceTracking
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class << self
|
|
88
|
+
def debug_tracking?
|
|
89
|
+
@debug_tracking == true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def debug_tracking=(value)
|
|
93
|
+
@debug_tracking = value == true
|
|
94
|
+
Signal.debug_tracking = self.debug_tracking?
|
|
95
|
+
Effect.debug_tracking = self.debug_tracking?
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# DevTools 依赖图数据:开启埋点 → 返回当前 signal→effect 边与
|
|
100
|
+
# 各 Effect 的重跑计数快照(可 to_json)。UI 晚一步、数据先行。
|
|
101
|
+
def debug_dependency_graph
|
|
102
|
+
self.debug_tracking = true
|
|
103
|
+
{
|
|
104
|
+
signals: Signal.all.map(&:debug_info),
|
|
105
|
+
effects: Effect.all.reject(&:disposed?).map(&:debug_info)
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Citrine 开发服务器(T-B1 瘦身版:Rack + Puma + Listen 替代手写 socket 服务)。
|
|
4
|
+
#
|
|
5
|
+
# 功能(与旧版行为等价):
|
|
6
|
+
# - 静态服务指定目录(html/css/…),"/" 提供目录索引
|
|
7
|
+
# - *.js 请求按对应 *.rb 现场编译(Opal CLI),带失效缓存
|
|
8
|
+
# - 监听 lib/ 与目录下 .rb/.css 变更,经 SSE 通知浏览器整页刷新
|
|
9
|
+
# - 编译失败时返回错误浮层脚本(页面不再白屏)
|
|
10
|
+
# - 样式资产注入(Citrine.css → <link>;Citrine.css_text → <style>)
|
|
11
|
+
#
|
|
12
|
+
# 用法:bin/citrine dev [目录] [-p 端口] [-I 加载路径]
|
|
13
|
+
#
|
|
14
|
+
# 旧版自建五件事(socket 静态服务 / 轮询监听 / 缓存失效 / SSE / 浮层),
|
|
15
|
+
# 瘦身后 HTTP 交给 Rack + Puma、文件监听交给 Listen,只保留应用语义
|
|
16
|
+
# (现场编译、错误浮层、样式资产注入)。
|
|
17
|
+
require "rack"
|
|
18
|
+
require "puma"
|
|
19
|
+
require "puma/server"
|
|
20
|
+
require "listen"
|
|
21
|
+
require "json"
|
|
22
|
+
require "open3"
|
|
23
|
+
require "tmpdir"
|
|
24
|
+
require "tempfile"
|
|
25
|
+
|
|
26
|
+
require "citrine/theme" # 样式资产注册表(Citrine.css / css_text)
|
|
27
|
+
|
|
28
|
+
module Citrine
|
|
29
|
+
class DevServer
|
|
30
|
+
CONTENT_TYPES = {
|
|
31
|
+
"html" => "text/html; charset=utf-8",
|
|
32
|
+
"js" => "application/javascript",
|
|
33
|
+
"css" => "text/css",
|
|
34
|
+
"png" => "image/png",
|
|
35
|
+
"svg" => "image/svg+xml",
|
|
36
|
+
"map" => "application/json"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
CLIENT_JS = <<~'JS'
|
|
40
|
+
(function () {
|
|
41
|
+
var overlay = null;
|
|
42
|
+
window.__rvShowError = function (msg) {
|
|
43
|
+
if (!overlay) {
|
|
44
|
+
overlay = document.createElement("pre");
|
|
45
|
+
overlay.style.cssText =
|
|
46
|
+
"position:fixed;z-index:99999;top:0;left:0;right:0;margin:0;padding:12px;" +
|
|
47
|
+
"background:#b91c1c;color:#fff;font:13px/1.5 monospace;white-space:pre-wrap;" +
|
|
48
|
+
"max-height:60vh;overflow:auto;";
|
|
49
|
+
document.documentElement.appendChild(overlay);
|
|
50
|
+
}
|
|
51
|
+
overlay.textContent = "⚠ Citrine 编译错误(修正并保存即自动恢复)\n\n" + msg;
|
|
52
|
+
};
|
|
53
|
+
window.__rvClearError = function () {
|
|
54
|
+
if (overlay) { overlay.remove(); overlay = null; }
|
|
55
|
+
};
|
|
56
|
+
var es = new EventSource("/__rv_reload");
|
|
57
|
+
es.onmessage = function (e) {
|
|
58
|
+
if (e.data === "reload") { window.__rvClearError(); location.reload(); }
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
JS
|
|
62
|
+
|
|
63
|
+
# SSE ping 清扫周期(秒):僵尸连接(浏览器已关、但无消息可写)平时不会暴露,
|
|
64
|
+
# 定期 ping 让写动作发生,写失败即被清扫(T6)
|
|
65
|
+
SSE_PING_INTERVAL = 15
|
|
66
|
+
|
|
67
|
+
# 命令行解析(纯函数,便于单测):返回 [目录, 端口, 额外加载路径]
|
|
68
|
+
def self.parse_args(args)
|
|
69
|
+
dir = nil
|
|
70
|
+
port = 4402
|
|
71
|
+
extra_libs = []
|
|
72
|
+
i = 0
|
|
73
|
+
while i < args.length
|
|
74
|
+
arg = args[i]
|
|
75
|
+
if arg == "-p"
|
|
76
|
+
value = args[i + 1]
|
|
77
|
+
raise ArgumentError, "-p 需要一个端口参数(如 -p 4402)" if value.nil? || value.start_with?("-")
|
|
78
|
+
|
|
79
|
+
port = value.to_i
|
|
80
|
+
i += 2
|
|
81
|
+
elsif arg == "-I"
|
|
82
|
+
value = args[i + 1]
|
|
83
|
+
raise ArgumentError, "-I 需要一个加载路径参数(如 -I ../some-lib/lib)" if value.nil? || value.start_with?("-")
|
|
84
|
+
|
|
85
|
+
extra_libs << value
|
|
86
|
+
i += 2
|
|
87
|
+
elsif arg.start_with?("-I")
|
|
88
|
+
extra_libs << arg[2..]
|
|
89
|
+
i += 1
|
|
90
|
+
elsif arg !~ /\A-/
|
|
91
|
+
dir = arg
|
|
92
|
+
i += 1
|
|
93
|
+
else
|
|
94
|
+
warn "未知参数 #{arg},已忽略(用法:citrine dev [目录] [-p 端口] [-I 加载路径])"
|
|
95
|
+
i += 1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
[dir || "examples", port, extra_libs]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.run!(args)
|
|
102
|
+
dir, port, extra_libs = parse_args(args)
|
|
103
|
+
new(dir, port, extra_libs).start
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def initialize(dir, port, extra_libs = [])
|
|
107
|
+
@dir = File.expand_path(dir)
|
|
108
|
+
@root = File.expand_path("../..", __dir__) # 项目根(dev_server.rb 位于 lib/citrine/)
|
|
109
|
+
# 额外加载路径(-I 可重复):跨仓库示例(如组件库的 examples/)编译时
|
|
110
|
+
# 需要补上那个仓库的 lib;转绝对路径——编译 cwd 是源文件所在目录
|
|
111
|
+
@extra_libs = extra_libs.map { |p| File.expand_path(p) }
|
|
112
|
+
@port = port
|
|
113
|
+
@clients = [] # 每个 SSE 连接一个 Queue
|
|
114
|
+
@cache = {} # js 路径 => body(Listen 事件触发时整体清空)
|
|
115
|
+
@mutex = Mutex.new
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def start
|
|
119
|
+
raise "目录不存在: #{@dir}" unless File.directory?(@dir)
|
|
120
|
+
|
|
121
|
+
watch
|
|
122
|
+
start_ping_sweep
|
|
123
|
+
puma = Puma::Server.new(rack_app)
|
|
124
|
+
puma.add_tcp_listener "127.0.0.1", @port
|
|
125
|
+
puts "Citrine dev server → http://localhost:#{@port}/"
|
|
126
|
+
puts " 目录: #{@dir};监听 lib/**/*.rb 与该目录 **/*.{rb,css}"
|
|
127
|
+
puma.run
|
|
128
|
+
sleep # Puma 在后台线程受理连接,主线程挂起
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# ── 文件监听(Listen 替代 0.3s 轮询)──────────────────────
|
|
134
|
+
|
|
135
|
+
def watch
|
|
136
|
+
sources = [File.join(@root, "lib"), @dir].select { |p| File.directory?(p) }
|
|
137
|
+
listener = Listen.to(*sources, only: /\.(rb|css)$/) do |modified, added, _removed|
|
|
138
|
+
@mutex.synchronize { @cache.clear }
|
|
139
|
+
names = (modified + added).map { |f| f.delete_prefix("#{@root}/") }
|
|
140
|
+
puts "[citrine] 变更: #{names.join(', ')} → 通知刷新"
|
|
141
|
+
broadcast("reload")
|
|
142
|
+
end
|
|
143
|
+
listener.start
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def broadcast(message)
|
|
147
|
+
@mutex.synchronize { @clients.dup }.each { |queue| queue << message }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ── Rack 应用 ──────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def rack_app
|
|
153
|
+
@rack_app ||= ->(env) { route(env) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def route(env)
|
|
157
|
+
path = Rack::Request.new(env).path
|
|
158
|
+
return sse(env) if path == "/__rv_reload"
|
|
159
|
+
return respond(200, "application/javascript", CLIENT_JS) if path == "/__rv_client.js"
|
|
160
|
+
return index if path == "/"
|
|
161
|
+
|
|
162
|
+
route_file(path)
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
respond(500, "text/plain; charset=utf-8", "#{e.class}: #{e.message}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def route_file(path)
|
|
168
|
+
full = File.expand_path(File.join(@dir, path.delete_prefix("/")))
|
|
169
|
+
# A4:目录逃逸守卫——裸前缀匹配挡不住 ../(@dir 为 /x/app 时 /x/app-evil 也命中),
|
|
170
|
+
# 必须"等于目录本身,或以 目录+分隔符 开头";先确认是文件再比路径
|
|
171
|
+
unless File.file?(full) && (full == @dir || full.start_with?(@dir + File::SEPARATOR))
|
|
172
|
+
return respond(404, "text/plain; charset=utf-8", "not found: #{path}")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if path.end_with?(".js")
|
|
176
|
+
rb = full.sub(/\.js$/, ".rb")
|
|
177
|
+
return serve_compiled(path, rb) if File.exist?(rb)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
body = File.binread(full)
|
|
181
|
+
type = CONTENT_TYPES[full.split(".").last] || "application/octet-stream"
|
|
182
|
+
if full.end_with?(".html")
|
|
183
|
+
body = inject_client(body)
|
|
184
|
+
type = CONTENT_TYPES["html"]
|
|
185
|
+
end
|
|
186
|
+
respond(200, type, body)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def serve_compiled(path, rb_full)
|
|
190
|
+
cached = @cache[path]
|
|
191
|
+
return respond(200, "application/javascript", cached) if cached
|
|
192
|
+
|
|
193
|
+
# T6:临时产物用 Tempfile(进程退出兜底清理),不再 rand 拼名裸写 tmpdir
|
|
194
|
+
tmp = Tempfile.new(["rv_dev_#{Process.pid}", ".js"])
|
|
195
|
+
# 与手工编译完全一致的形态:cwd = 源文件所在目录,-I附着式传参
|
|
196
|
+
includes = ["-I#{File.join(@root, 'lib')}", "-I."] + @extra_libs.map { |p| "-I#{p}" }
|
|
197
|
+
out, err, status = Open3.capture3(
|
|
198
|
+
opal_executable, "-c", *includes,
|
|
199
|
+
"-o", tmp.path, File.basename(rb_full),
|
|
200
|
+
chdir: File.dirname(rb_full)
|
|
201
|
+
)
|
|
202
|
+
if status.success?
|
|
203
|
+
body = File.binread(tmp.path)
|
|
204
|
+
@mutex.synchronize { @cache[path] = body }
|
|
205
|
+
respond(200, "application/javascript", body)
|
|
206
|
+
else
|
|
207
|
+
message = (out + "\n" + err).strip.to_json
|
|
208
|
+
respond(200, "application/javascript",
|
|
209
|
+
"window.__rvShowError(#{message});")
|
|
210
|
+
end
|
|
211
|
+
rescue Errno::ENOENT
|
|
212
|
+
abort "找不到 opal 可执行文件:请先 bundle install,并用 bundle exec bin/citrine dev 启动"
|
|
213
|
+
ensure
|
|
214
|
+
tmp&.close!
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# A5:经 rubygems 解析 opal 的 binstub(bundler 环境下稳定指向 bundle 内的 opal,
|
|
218
|
+
# 不依赖 PATH 里有没有裸 "opal");解析不到退化为 PATH 查找——真缺失时由
|
|
219
|
+
# 上方 Errno::ENOENT 分支给出可操作的 abort 提示
|
|
220
|
+
def opal_executable
|
|
221
|
+
Gem.bin_path("opal", "opal")
|
|
222
|
+
rescue Gem::LoadError
|
|
223
|
+
"opal"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def index
|
|
227
|
+
pages = Dir.glob(File.join(@dir, "*.html")).map { |f| File.basename(f) }.sort
|
|
228
|
+
links = pages.map { |p| %(<li><a href="/#{p}">#{p}</a></li>) }.join("\n")
|
|
229
|
+
body = <<~HTML
|
|
230
|
+
<!DOCTYPE html>
|
|
231
|
+
<html lang="zh"><head><meta charset="utf-8"><title>Citrine dev</title></head>
|
|
232
|
+
<body style="font-family:sans-serif;padding:24px;line-height:1.8">
|
|
233
|
+
<h2>Citrine dev server</h2>
|
|
234
|
+
<p>修改 lib/ 或本目录下的 .rb / .css 文件并保存,浏览器将自动刷新。</p>
|
|
235
|
+
<ul>#{links}</ul>
|
|
236
|
+
</body></html>
|
|
237
|
+
HTML
|
|
238
|
+
respond(200, CONTENT_TYPES["html"], body)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def inject_client(html)
|
|
242
|
+
# S2-5:先把声明的样式资产(<link> / <style>)注入 <head>
|
|
243
|
+
html = inject_head_assets(html)
|
|
244
|
+
# CITRINE_DEV:开发模式标志(布局提醒等只在开发期输出;生产构建不注入)
|
|
245
|
+
script = %(<script>window.CITRINE_DEV = true;</script>\n<script src="/__rv_client.js"></script>)
|
|
246
|
+
return html.sub("</head>", "#{script}</head>") if html.include?("</head>")
|
|
247
|
+
|
|
248
|
+
html.sub("</body>", "#{script}</body>")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# S2-5:样式资产注入——Citrine.css 声明的样式表(<link>)与
|
|
252
|
+
# Citrine.css_text 自定义样式文本(<style>,媒体查询/伪类的逃生舱)。
|
|
253
|
+
# 纯函数(便于单测):只改传入的 html,不读文件系统。
|
|
254
|
+
def inject_head_assets(html)
|
|
255
|
+
assets = Citrine.css_files.map { |f| %(<link rel="stylesheet" href="/#{f}">) }
|
|
256
|
+
assets << "<style>#{Citrine.css_text}</style>" if Citrine.css_text && !Citrine.css_text.empty?
|
|
257
|
+
return html if assets.empty?
|
|
258
|
+
|
|
259
|
+
block = assets.join("\n")
|
|
260
|
+
return html.sub("</head>", "#{block}\n</head>") if html.include?("</head>")
|
|
261
|
+
|
|
262
|
+
html.sub("</body>", "#{block}\n</body>")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# ── SSE 热刷新(rack.hijack 接管连接)────────────────────
|
|
266
|
+
|
|
267
|
+
def sse(env)
|
|
268
|
+
io = env["rack.hijack"].call
|
|
269
|
+
io.write "HTTP/1.1 200 OK\r\n" \
|
|
270
|
+
"Content-Type: text/event-stream\r\n" \
|
|
271
|
+
"Cache-Control: no-cache\r\n" \
|
|
272
|
+
"Connection: keep-alive\r\n\r\n"
|
|
273
|
+
queue = Queue.new
|
|
274
|
+
@mutex.synchronize { @clients << queue }
|
|
275
|
+
Thread.new { pump_client(io, queue) }
|
|
276
|
+
[-1, {}, []] # 已劫持连接,Rack 不再处理响应
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# 每连接一个泵线程:从队列取消息写给浏览器;写失败(连接已死)即注销。
|
|
280
|
+
# ping 帧(:ping)只作心跳不占消息位——僵尸连接靠它暴露写失败并被清扫。
|
|
281
|
+
def pump_client(io, queue)
|
|
282
|
+
loop do
|
|
283
|
+
message = queue.pop
|
|
284
|
+
io.write(message == :ping ? ": ping\n\n" : "data: #{message}\n\n")
|
|
285
|
+
end
|
|
286
|
+
rescue StandardError
|
|
287
|
+
@mutex.synchronize { @clients.delete(queue) } # 连接断开时清理
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# T6:定期给所有 SSE 连接发 ping——平时无消息可写时,浏览器已关的僵尸连接
|
|
291
|
+
# 不会触发写失败,注册表只增不减;ping 让写动作周期性发生,死连接随之被清扫
|
|
292
|
+
def start_ping_sweep
|
|
293
|
+
Thread.new do
|
|
294
|
+
loop do
|
|
295
|
+
sleep SSE_PING_INTERVAL
|
|
296
|
+
@mutex.synchronize { @clients.dup }.each { |queue| queue << :ping }
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def respond(status, type, body)
|
|
302
|
+
[status, {"Content-Type" => type, "Content-Length" => body.bytesize.to_s}, [body]]
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|