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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2681e7067d0b0bd1d3d998e352b89a926cc6c8d1c129a632bb325fb5d2dde8c6
4
- data.tar.gz: d4562b20a7f17a82c29cca1020b1956cc18c22ff8aa8531a7c5ebd5230db6704
3
+ metadata.gz: b2720e5cd2f2bbe7ea1f8c6470c420fcb2b84bb8c6c3bab83ca12b5ea64633d5
4
+ data.tar.gz: 410a06961fed394deb2b0bdc194cccede9c407de205b5a8e977da1c165290e68
5
5
  SHA512:
6
- metadata.gz: a249874ec8efe3f1ac1c683ee69043077e73051c97eb7cfaa45b491e68df57b7cd22636f665918e33cfeac87913bca791a45ccacb36d16a815f8044ccb219717
7
- data.tar.gz: 8b5aca6dde6daf9e51d8700de9609879873110b7793db145153140c813095bd294b9683a2b0a29ffdfd111f7ad43373ce830e9c0103cd3ec86b7d098e5474ff0
6
+ metadata.gz: e9d1f379990a194e710220e0cc872501c3ce9da07e7e9149b5eaf4377ab669f5d74cc814bb6c8cf37dec3f647ea767074c1aa5c49daab87a584da3f9909d9846
7
+ data.tar.gz: c59c37531ebbd43796a7117d843208680562f18e09d939aeb63ab05b5560faa79fe7fbc5014de7fd4dc413f2be80d949f4cb1a4d802aa421ed4e41aa1ca325fb
data/README.md CHANGED
@@ -1,3 +1,17 @@
1
1
  # ruflet_server
2
2
 
3
- Part of Ruflet monorepo.
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 "Requested port #{requested} is busy; bound to #{@port}"
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
- close_connection(ws)
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
- case path
274
- when "/health"
275
- write_http_response(socket, 200, "text/plain", "ok")
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 path.start_with?("/assets/")
280
- serve_asset(socket, path)
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
- write_http_response(socket, 500, "text/plain", "server error")
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 ".png"
325
- "image/png"
326
- when ".jpg", ".jpeg"
327
- "image/jpeg"
328
- when ".gif"
329
- "image/gif"
330
- when ".webp"
331
- "image/webp"
332
- when ".svg"
333
- "image/svg+xml"
334
- else
335
- "application/octet-stream"
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.15" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.16" unless const_defined?(:VERSION)
5
5
  end
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: 8550, &block)
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.15
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.15
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.15
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: