Class: Tina4::RackApp

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/rack_app.rb

Constant Summary collapse

STATIC_DIRS =
%w[public src/public src/assets assets].freeze
FRAMEWORK_PUBLIC_DIR =

Framework’s own public directory (bundled static assets like the logo)

File.expand_path("public", __dir__).freeze

Instance Method Summary collapse

Constructor Details

#initialize(root_dir: Dir.pwd) ⇒ RackApp

Returns a new instance of RackApp.



28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/tina4/rack_app.rb', line 28

def initialize(root_dir: Dir.pwd)
  @root_dir = root_dir
  # Pre-compute static roots at boot (not per-request)
  # Project dirs are checked first; framework's bundled public dir is the fallback
  project_roots = STATIC_DIRS.map { |d| File.join(root_dir, d) }
                             .select { |d| Dir.exist?(d) }
  fallback = Dir.exist?(FRAMEWORK_PUBLIC_DIR) ? [FRAMEWORK_PUBLIC_DIR] : []
  @static_roots = (project_roots + fallback).freeze

  # Shared WebSocket engine for route-based WS handling
  @websocket_engine = Tina4::WebSocket.new
end

Instance Method Details

#call(env) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/tina4/rack_app.rb', line 41

def call(env)
  method = env["REQUEST_METHOD"]
  path = env["PATH_INFO"] || "/"
  request_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  # Fast-path: OPTIONS preflight
  return Tina4::CorsMiddleware.preflight_response(env) if method == "OPTIONS"

  # WebSocket upgrade — match against registered ws_routes
  if websocket_upgrade?(env)
    ws_result = Tina4::Router.find_ws_route(path)
    if ws_result
      ws_route, ws_params = ws_result
      return handle_websocket_upgrade(env, ws_route, ws_params)
    end
  end

  # Dev dashboard routes (handled before anything else)
  if path.start_with?("/__dev")
    # Block live-reload endpoint on the AI port — AI tools must get stable responses
    if path == "/__dev_reload" && env["tina4.ai_port"]
      return [404, { "content-type" => "text/plain" }, ["Not available on AI port"]]
    end
    dev_response = Tina4::DevAdmin.handle_request(env)
    return dev_response if dev_response
  end

  # Fast-path: API routes skip static file + swagger checks entirely
  unless path.start_with?("/api/")
    # Swagger
    if path == "/swagger" || path == "/swagger/"
      return serve_swagger_ui
    end
    if path == "/swagger/openapi.json"
      return serve_openapi_json
    end

    # Static files (only for non-API paths)
    static_response = try_static(path)
    return static_response if static_response
  end

  # Route matching
  result = Tina4::Router.match(method, path)
  if result
    route, path_params = result
    rack_response = handle_route(env, route, path_params)
    matched_pattern = route.path
  else
    rack_response = handle_404(path)
    matched_pattern = nil
  end

  # Capture request for dev inspector
  if dev_mode? && !path.start_with?("/__dev")
    duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start) * 1000).round(3)
    Tina4::DevAdmin.request_inspector.capture(
      method: method,
      path: path,
      status: rack_response[0],
      duration: duration_ms
    )
  end

  # Inject dev overlay button for HTML responses in dev mode
  if dev_mode? && !path.start_with?("/__dev")
    status, headers, body_parts = rack_response
    content_type = headers["content-type"] || ""
    if content_type.include?("text/html")
      request_info = {
        method: method,
        path: path,
        matched_pattern: matched_pattern || "(no match)",
      }
      joined = body_parts.join
      overlay = inject_dev_overlay(joined, request_info, ai_port: env["tina4.ai_port"])
      rack_response = [status, headers, [overlay]]
    end
  end

  # Save session and set cookie if session was used
  if result && defined?(rack_response)
    status, headers, body_parts = rack_response
    request_obj = env["tina4.request"]
    if request_obj&.instance_variable_get(:@session)
      sess = request_obj.session
      sess.save

      # Probabilistic garbage collection (~1% of requests)
      if rand(1..100) == 1
        begin
          sess.gc
        rescue StandardError
          # GC failure is non-critical — silently ignore
        end
      end

      sid = sess.id
      cookie_val = (env["HTTP_COOKIE"] || "")[/tina4_session=([^;]+)/, 1]
      if sid && sid != cookie_val
        ttl = Integer(ENV.fetch("TINA4_SESSION_TTL", 3600))
        headers["set-cookie"] = "tina4_session=#{sid}; Path=/; HttpOnly; SameSite=Lax; Max-Age=#{ttl}"
      end
      rack_response = [status, headers, body_parts]
    end
  end

  rack_response
rescue => e
  handle_500(e, env)
end

#handle(request) ⇒ Object

Dispatch a pre-built Request through the Rack app and return the Rack response triple. Useful for testing and embedding without starting an HTTP server.



155
156
157
158
159
# File 'lib/tina4/rack_app.rb', line 155

def handle(request)
  env = request.env
  env["rack.input"].rewind if env["rack.input"].respond_to?(:rewind)
  call(env)
end