tina4ruby 3.13.36 → 3.13.38

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.
@@ -351,9 +351,12 @@ module Tina4
351
351
  env["tina4.request"] = request # Store for session save after response
352
352
  response = Tina4::Response.new
353
353
 
354
- # Run global middleware (block-based + class-based before_* methods)
354
+ # Run global middleware (block-based + class-based before_* methods).
355
+ # M2 — AFTER-ON-4xx RULE: when a before_* short-circuits (4xx/skip) or
356
+ # throws (clean 500), the after-pass STILL runs so after_* can add
357
+ # headers/logging — consistent across all 4 frameworks.
355
358
  unless Tina4::Middleware.run_before(Tina4::Middleware.global_middleware, request, response)
356
- # Middleware halted the request -- return whatever response was set
359
+ Tina4::Middleware.run_after(Tina4::Middleware.global_middleware, request, response)
357
360
  return response.to_rack
358
361
  end
359
362
 
@@ -885,18 +888,23 @@ module Tina4
885
888
  # Wire the route handler into the WebSocket engine events
886
889
  handler = ws_route.handler
887
890
 
888
- # Create a dedicated WebSocket engine for this route so handlers stay isolated
891
+ # Create a dedicated WebSocket *event* engine for this route so each
892
+ # upgrade keeps its own isolated open/message/close handlers (the engine's
893
+ # on() appends handlers, so a single shared event engine would cross-wire
894
+ # routes).
889
895
  ws = Tina4::WebSocket.new
890
896
 
891
- # The dev-reload channel is held by a process-wide shared manager so a
892
- # broadcast from POST /__dev/api/reload reaches every browser, not just
893
- # the connections of this one isolated per-socket engine. Mirrors
894
- # Python's single _ws_manager keyed on the /__dev_reload path.
897
+ # The connection itself is OWNED by a process-wide shared manager so that
898
+ # broadcasts, rooms, the multi-instance backplane and the idle reaper span
899
+ # every route's connections not just this one per-socket event engine.
900
+ # Mirrors Python's single WebSocketManager. The dev-reload channel uses its
901
+ # own shared manager (Tina4::DevReload) so POST /__dev/api/reload reaches
902
+ # every browser.
895
903
  dev_reload = ws_route.path == "/__dev_reload"
904
+ manager = dev_reload ? Tina4::DevReload.manager : @websocket_engine
896
905
 
897
906
  ws.on(:open) do |connection|
898
907
  connection.params = ws_params
899
- Tina4::DevReload.add(connection) if dev_reload
900
908
  handler.call(connection, :open, nil)
901
909
  end
902
910
 
@@ -905,7 +913,6 @@ module Tina4
905
913
  end
906
914
 
907
915
  ws.on(:close) do |connection|
908
- Tina4::DevReload.remove(connection) if dev_reload
909
916
  handler.call(connection, :close, nil)
910
917
  end
911
918
 
@@ -913,7 +920,7 @@ module Tina4
913
920
  Tina4::Log.error("WebSocket error on #{ws_route.path}: #{error.message}")
914
921
  end
915
922
 
916
- ws.handle_upgrade(env, socket)
923
+ ws.handle_upgrade(env, socket, manager: manager)
917
924
 
918
925
  # Return async response (-1 signals Rack the response is handled via hijack)
919
926
  [-1, {}, []]
@@ -347,18 +347,38 @@ module Tina4
347
347
  gen = @_stream_generator
348
348
  blk = @_stream_block
349
349
  body = Enumerator.new do |yielder|
350
- if gen
351
- if gen.respond_to?(:each)
352
- # Enumerator / array / any Enumerable of string chunks
353
- gen.each { |chunk| yielder << chunk }
354
- elsif gen.respond_to?(:call)
355
- # Callable that receives the yielder, like the block form
356
- gen.call(yielder)
357
- else
358
- yielder << gen.to_s
350
+ # SSE hardening: a streaming source that raises mid-stream (a
351
+ # generator/block error, or the client disconnecting and the server
352
+ # tearing the body down) must NEVER crash the worker. We catch the
353
+ # error, log it, and end the stream cleanly — the chunks emitted
354
+ # before the failure are still delivered.
355
+ #
356
+ # A client disconnect surfaces in a hijack/Puma streaming body as a
357
+ # write-side IOError/Errno on the socket; that is propagated up as a
358
+ # normal stop and re-raised so Rack/Puma can close the connection,
359
+ # while a *source* error is swallowed after logging.
360
+ begin
361
+ if gen
362
+ if gen.respond_to?(:each)
363
+ # Enumerator / array / any Enumerable of string chunks
364
+ gen.each { |chunk| yielder << chunk }
365
+ elsif gen.respond_to?(:call)
366
+ # Callable that receives the yielder, like the block form
367
+ gen.call(yielder)
368
+ else
369
+ yielder << gen.to_s
370
+ end
371
+ elsif blk
372
+ blk.call(yielder)
359
373
  end
360
- elsif blk
361
- blk.call(yielder)
374
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET => e
375
+ # Client disconnected mid-stream — stop cleanly, do not crash, and
376
+ # do not log loudly (a normal browser closing an SSE stream).
377
+ Tina4::Log.debug("SSE/stream client disconnected: #{e.class}: #{e.message}") if defined?(Tina4::Log)
378
+ rescue StandardError => e
379
+ # The source (generator/block) itself raised mid-stream. Log it and
380
+ # end the stream cleanly rather than crashing the handler/worker.
381
+ Tina4::Log.error("SSE/stream source error: #{e.class}: #{e.message}") if defined?(Tina4::Log)
362
382
  end
363
383
  end
364
384
  return [@status_code, final_headers, body]