ruflet_server 0.0.15 → 0.0.16
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 +4 -4
- data/README.md +15 -1
- data/lib/ruflet/server.rb +125 -22
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_server.rb +9 -2
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2720e5cd2f2bbe7ea1f8c6470c420fcb2b84bb8c6c3bab83ca12b5ea64633d5
|
|
4
|
+
data.tar.gz: 410a06961fed394deb2b0bdc194cccede9c407de205b5a8e977da1c165290e68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9d1f379990a194e710220e0cc872501c3ce9da07e7e9149b5eaf4377ab669f5d74cc814bb6c8cf37dec3f647ea767074c1aa5c49daab87a584da3f9909d9846
|
|
7
|
+
data.tar.gz: c59c37531ebbd43796a7117d843208680562f18e09d939aeb63ab05b5560faa79fe7fbc5014de7fd4dc413f2be80d949f4cb1a4d802aa421ed4e41aa1ca325fb
|
data/README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
# ruflet_server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`ruflet_server` runs server-driven Ruflet applications and connects their Ruby
|
|
4
|
+
UI code to Ruflet clients.
|
|
5
|
+
|
|
6
|
+
It is installed automatically in projects created with `ruflet new`. Start an
|
|
7
|
+
application through the Ruflet CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle exec ruflet run
|
|
11
|
+
bundle exec ruflet run --web
|
|
12
|
+
bundle exec ruflet run --desktop
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Application code uses the public `Ruflet.run` API supplied by `ruflet_core`.
|
|
16
|
+
Rails applications should use `ruflet_rails` instead of starting this server
|
|
17
|
+
directly.
|
data/lib/ruflet/server.rb
CHANGED
|
@@ -63,7 +63,7 @@ module Ruflet
|
|
|
63
63
|
@server_socket = TCPServer.new(@host, candidate)
|
|
64
64
|
@port = candidate
|
|
65
65
|
if @port != requested && ENV["RUFLET_SUPPRESS_SERVER_BANNER"] != "1"
|
|
66
|
-
warn "
|
|
66
|
+
warn "Port #{requested} is busy; using #{@port}."
|
|
67
67
|
end
|
|
68
68
|
publish_bound_port!
|
|
69
69
|
return
|
|
@@ -233,10 +233,24 @@ module Ruflet
|
|
|
233
233
|
warn e.backtrace.join("\n") if e.backtrace
|
|
234
234
|
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") }) if ws
|
|
235
235
|
ensure
|
|
236
|
-
|
|
236
|
+
if ws
|
|
237
|
+
close_connection(ws)
|
|
238
|
+
else
|
|
239
|
+
# Plain HTTP request: we answer with `Connection: close`, so we must
|
|
240
|
+
# actually close the socket. Leaving it open exhausts the browser's
|
|
241
|
+
# per-host connection pool and the later /ws upgrade never opens —
|
|
242
|
+
# the app then hangs on its "connecting" screen.
|
|
243
|
+
close_http_socket(socket)
|
|
244
|
+
end
|
|
237
245
|
end
|
|
238
246
|
end
|
|
239
247
|
|
|
248
|
+
def close_http_socket(socket)
|
|
249
|
+
socket.close if socket && !socket.closed?
|
|
250
|
+
rescue StandardError
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
|
|
240
254
|
def read_http_upgrade_request(socket)
|
|
241
255
|
request_line = socket.gets("\r\n")
|
|
242
256
|
return [nil, {}] if request_line.nil?
|
|
@@ -261,7 +275,7 @@ module Ruflet
|
|
|
261
275
|
end
|
|
262
276
|
|
|
263
277
|
def websocket_upgrade_request?(path, headers)
|
|
264
|
-
return false unless path == "/ws"
|
|
278
|
+
return false unless path.to_s.split("?", 2).first == "/ws"
|
|
265
279
|
return false unless headers["upgrade"]&.downcase == "websocket"
|
|
266
280
|
return false unless headers["connection"]&.downcase&.include?("upgrade")
|
|
267
281
|
return false if headers["sec-websocket-key"].to_s.empty?
|
|
@@ -270,21 +284,95 @@ module Ruflet
|
|
|
270
284
|
end
|
|
271
285
|
|
|
272
286
|
def handle_http_request(socket, path)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
287
|
+
clean = path.to_s.split("?", 2).first.split("#", 2).first
|
|
288
|
+
return write_http_response(socket, 200, "text/plain", "ok") if clean == "/health"
|
|
289
|
+
|
|
290
|
+
# In web mode the standalone backend also serves the Flutter web client,
|
|
291
|
+
# so the browser loads the app and opens its websocket on this same
|
|
292
|
+
# origin/port — no separate static server or proxy is needed.
|
|
293
|
+
return serve_web_client(socket, clean) if web_client_root
|
|
294
|
+
|
|
295
|
+
case clean
|
|
276
296
|
when "/"
|
|
277
297
|
write_http_response(socket, 200, "text/plain", "ruflet server")
|
|
278
298
|
else
|
|
279
|
-
if
|
|
280
|
-
serve_asset(socket,
|
|
299
|
+
if clean.start_with?("/assets/")
|
|
300
|
+
serve_asset(socket, clean)
|
|
281
301
|
else
|
|
282
302
|
write_http_response(socket, 404, "text/plain", "not found")
|
|
283
303
|
end
|
|
284
304
|
end
|
|
285
305
|
rescue StandardError => e
|
|
306
|
+
# The browser routinely cancels in-flight asset requests (preloads,
|
|
307
|
+
# duplicate connections); writing to a reset socket raises EPIPE/ECONNRESET
|
|
308
|
+
# and is expected, not an error.
|
|
309
|
+
return if disconnect_error?(e)
|
|
310
|
+
|
|
286
311
|
warn "http error: #{e.class}: #{e.message}"
|
|
287
|
-
|
|
312
|
+
begin
|
|
313
|
+
write_http_response(socket, 500, "text/plain", "server error")
|
|
314
|
+
rescue StandardError
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def web_client_root
|
|
320
|
+
dir = ENV["RUFLET_WEB_CLIENT_DIR"].to_s
|
|
321
|
+
return nil if dir.empty?
|
|
322
|
+
|
|
323
|
+
full = File.expand_path(dir)
|
|
324
|
+
File.directory?(full) ? full : nil
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def serve_web_client(socket, path)
|
|
328
|
+
root = web_client_root
|
|
329
|
+
|
|
330
|
+
# Neutralize the Flutter service worker: when this dev server hops between
|
|
331
|
+
# localhost ports a cached worker would otherwise keep a stale client
|
|
332
|
+
# alive that reconnects to the wrong backend. This unregisters it and
|
|
333
|
+
# clears caches so the browser always loads the current client.
|
|
334
|
+
if path == "/flutter_service_worker.js"
|
|
335
|
+
return write_http_response(socket, 200, "text/javascript", service_worker_reset_js, cache: false)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
relative = path == "/" ? "index.html" : path.sub(%r{\A/}, "")
|
|
339
|
+
full = File.expand_path(File.join(root, relative))
|
|
340
|
+
if (full == root || full.start_with?(root + File::SEPARATOR)) && File.file?(full)
|
|
341
|
+
return write_http_response(socket, 200, content_type_for(full), File.binread(full), binary: true, cache: false)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# App runtime assets (images referenced by the app) fall back to the
|
|
345
|
+
# configured assets directory when not part of the client bundle.
|
|
346
|
+
if path.start_with?("/assets/") && (asset = resolve_asset_path(path))
|
|
347
|
+
return write_http_response(socket, 200, content_type_for(asset), File.binread(asset), binary: true, cache: false)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# SPA fallback: serve index.html for extension-less route paths.
|
|
351
|
+
index = File.join(root, "index.html")
|
|
352
|
+
if File.extname(path).empty? && File.file?(index)
|
|
353
|
+
return write_http_response(socket, 200, "text/html", File.binread(index), binary: true, cache: false)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
write_http_response(socket, 404, "text/plain", "not found")
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# A no-op service worker: it registers cleanly (so Flutter's loader, which
|
|
360
|
+
# awaits navigator.serviceWorker.ready, never hangs), wipes any caches a
|
|
361
|
+
# previous run left behind, claims the page, and installs NO fetch handler —
|
|
362
|
+
# so every request (including the app shell) goes straight to the network and
|
|
363
|
+
# the client always loads fresh and connects to the current origin/port.
|
|
364
|
+
# (Self-unregistering here can leave serviceWorker.ready unresolved.)
|
|
365
|
+
def service_worker_reset_js
|
|
366
|
+
<<~JS
|
|
367
|
+
self.addEventListener('install', function (e) { self.skipWaiting(); });
|
|
368
|
+
self.addEventListener('activate', function (e) {
|
|
369
|
+
e.waitUntil((async function () {
|
|
370
|
+
var keys = await caches.keys();
|
|
371
|
+
await Promise.all(keys.map(function (k) { return caches.delete(k); }));
|
|
372
|
+
await self.clients.claim();
|
|
373
|
+
})());
|
|
374
|
+
});
|
|
375
|
+
JS
|
|
288
376
|
end
|
|
289
377
|
|
|
290
378
|
def serve_asset(socket, path)
|
|
@@ -315,28 +403,39 @@ module Ruflet
|
|
|
315
403
|
root = ENV["RUFLET_ASSETS_DIR"].to_s
|
|
316
404
|
return root unless root.empty?
|
|
317
405
|
|
|
406
|
+
embedded_root = defined?($__ruflet_app_root) ? $__ruflet_app_root.to_s : ""
|
|
407
|
+
unless embedded_root.empty?
|
|
408
|
+
embedded_assets = File.join(embedded_root, "assets")
|
|
409
|
+
return embedded_assets if File.directory?(embedded_assets)
|
|
410
|
+
end
|
|
411
|
+
|
|
318
412
|
default_root = File.join(Dir.pwd, "assets")
|
|
319
413
|
File.directory?(default_root) ? default_root : nil
|
|
320
414
|
end
|
|
321
415
|
|
|
322
416
|
def content_type_for(path)
|
|
323
417
|
case File.extname(path).downcase
|
|
324
|
-
when ".
|
|
325
|
-
|
|
326
|
-
when ".
|
|
327
|
-
|
|
328
|
-
when ".
|
|
329
|
-
|
|
330
|
-
when ".
|
|
331
|
-
|
|
332
|
-
when ".
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
418
|
+
when ".html", ".htm" then "text/html; charset=utf-8"
|
|
419
|
+
when ".js", ".mjs" then "text/javascript; charset=utf-8"
|
|
420
|
+
when ".json", ".map" then "application/json; charset=utf-8"
|
|
421
|
+
when ".css" then "text/css; charset=utf-8"
|
|
422
|
+
when ".wasm" then "application/wasm"
|
|
423
|
+
when ".png" then "image/png"
|
|
424
|
+
when ".jpg", ".jpeg" then "image/jpeg"
|
|
425
|
+
when ".gif" then "image/gif"
|
|
426
|
+
when ".webp" then "image/webp"
|
|
427
|
+
when ".svg" then "image/svg+xml"
|
|
428
|
+
when ".ico" then "image/x-icon"
|
|
429
|
+
when ".ttf" then "font/ttf"
|
|
430
|
+
when ".otf" then "font/otf"
|
|
431
|
+
when ".woff" then "font/woff"
|
|
432
|
+
when ".woff2" then "font/woff2"
|
|
433
|
+
when ".txt" then "text/plain; charset=utf-8"
|
|
434
|
+
else "application/octet-stream"
|
|
336
435
|
end
|
|
337
436
|
end
|
|
338
437
|
|
|
339
|
-
def write_http_response(socket, status, content_type, body, binary: false)
|
|
438
|
+
def write_http_response(socket, status, content_type, body, binary: false, cache: true)
|
|
340
439
|
reason = {
|
|
341
440
|
200 => "OK",
|
|
342
441
|
404 => "Not Found",
|
|
@@ -349,6 +448,10 @@ module Ruflet
|
|
|
349
448
|
socket.write("HTTP/1.1 #{status} #{reason}\r\n")
|
|
350
449
|
socket.write("Content-Type: #{content_type}\r\n")
|
|
351
450
|
socket.write("Content-Length: #{length}\r\n")
|
|
451
|
+
unless cache
|
|
452
|
+
socket.write("Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n")
|
|
453
|
+
socket.write("Pragma: no-cache\r\n")
|
|
454
|
+
end
|
|
352
455
|
socket.write("Connection: close\r\n")
|
|
353
456
|
socket.write("\r\n")
|
|
354
457
|
socket.write(body_str)
|
data/lib/ruflet/version.rb
CHANGED
data/lib/ruflet_server.rb
CHANGED
|
@@ -6,7 +6,8 @@ require_relative "ruflet/server"
|
|
|
6
6
|
module Ruflet
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
-
def run(entrypoint = nil, host: "0.0.0.0", port:
|
|
9
|
+
def run(entrypoint = nil, host: "0.0.0.0", port: nil, &block)
|
|
10
|
+
port = normalize_run_port(port || ENV["RUFLET_PORT"] || 8550)
|
|
10
11
|
callback = entrypoint || block
|
|
11
12
|
raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
|
|
12
13
|
|
|
@@ -27,7 +28,13 @@ module Ruflet
|
|
|
27
28
|
|
|
28
29
|
@run_interceptors_mutex.synchronize { @run_interceptors.last }
|
|
29
30
|
end
|
|
31
|
+
|
|
32
|
+
def normalize_run_port(value)
|
|
33
|
+
Integer(value)
|
|
34
|
+
rescue ArgumentError, TypeError
|
|
35
|
+
8550
|
|
36
|
+
end
|
|
30
37
|
class << self
|
|
31
|
-
private :run_interceptor
|
|
38
|
+
private :run_interceptor, :normalize_run_port
|
|
32
39
|
end
|
|
33
40
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet_server
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.16
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.16
|
|
26
26
|
description: Ruflet WebSocket server runtime compatible with Flet protocol.
|
|
27
27
|
email:
|
|
28
28
|
- adammusa2222@gmail.com
|
|
@@ -38,7 +38,8 @@ files:
|
|
|
38
38
|
- lib/ruflet/version.rb
|
|
39
39
|
- lib/ruflet_server.rb
|
|
40
40
|
homepage: https://github.com/AdamMusa/Ruflet
|
|
41
|
-
licenses:
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
42
43
|
metadata: {}
|
|
43
44
|
rdoc_options: []
|
|
44
45
|
require_paths:
|