tina4ruby 3.13.131 → 3.13.133
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/CHANGELOG.md +90 -0
- data/lib/tina4/dev_admin.rb +255 -363
- data/lib/tina4/frond.rb +37 -75
- data/lib/tina4/model_collection.rb +87 -0
- data/lib/tina4/orm.rb +39 -20
- data/lib/tina4/public/js/tina4-dev-admin.min.js +148 -512
- data/lib/tina4/rack_app.rb +4 -1
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +120 -138
- data/lib/tina4.rb +1 -0
- metadata +3 -2
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -941,7 +941,10 @@ module Tina4
|
|
|
941
941
|
# data-reload gates the live reloader in toolbar.js: "1" on the human dev
|
|
942
942
|
# port, "0" on the AI/stable port (the JS early-returns before starting the
|
|
943
943
|
# reloader when it is not "1"). Matches Python's render_dev_toolbar.
|
|
944
|
-
|
|
944
|
+
# Also suppress on the dev-admin dashboard (any /__dev page): its SPA reloads
|
|
945
|
+
# itself gently, so the toolbar's full-page reloader must not fire there --
|
|
946
|
+
# otherwise saving from the editor reloads the whole dashboard.
|
|
947
|
+
reload = (ai_port || request_info[:path].to_s.start_with?("/__dev")) ? "0" : "1"
|
|
945
948
|
ai_badge = ai_port ? '<span class="t4-ai-badge">AI PORT</span>' : ""
|
|
946
949
|
|
|
947
950
|
toolbar = <<~HTML.strip
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/webserver.rb
CHANGED
|
@@ -97,6 +97,26 @@ module Tina4
|
|
|
97
97
|
AccessLog: []
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
+
# Dual-stack loopback: ALSO listen on the sibling loopback family on the
|
|
101
|
+
# MAIN port, so `localhost` reaches this server whether the OS resolves it
|
|
102
|
+
# to IPv4 (127.0.0.1) or IPv6 (::1). On Windows `localhost` resolves to
|
|
103
|
+
# ::1 first, so a server bound only to 127.0.0.1 -- or to 0.0.0.0, the
|
|
104
|
+
# IPv4 wildcard, which does NOT cover IPv6 -- refuses the browser with
|
|
105
|
+
# ERR_CONNECTION_REFUSED even though it is serving. The primary bind above
|
|
106
|
+
# is unchanged (keeps its fail-closed throw + port-takeover); each sibling
|
|
107
|
+
# is BEST-EFFORT via WEBrick::GenericServer#listen, which appends to the
|
|
108
|
+
# listener set the accept loop already services. A family that is
|
|
109
|
+
# unavailable, or that the primary bind already answers, raises and is
|
|
110
|
+
# skipped -- a sibling failure NEVER fails the boot. Main port only
|
|
111
|
+
# (mirrors tina4-php PR #206); the AI/debug port is left alone.
|
|
112
|
+
self.class.loopback_bind_hosts(@host).each do |sibling_host|
|
|
113
|
+
begin
|
|
114
|
+
@server.listen(sibling_host, @port)
|
|
115
|
+
rescue Errno::EADDRINUSE, Errno::EADDRNOTAVAIL, SocketError, Errno::EAFNOSUPPORT => e
|
|
116
|
+
Tina4::Log.debug("Dual-stack loopback: skipped #{sibling_host}:#{@port} (#{e.class})")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
100
120
|
# Record THIS process as the Tina4 dev server on the main port, so a later
|
|
101
121
|
# `tina4 serve` can identify it as reclaimable (TAKEOVER-DEC-01).
|
|
102
122
|
Tina4::PortTakeover.write_pidfile(@port)
|
|
@@ -106,6 +126,103 @@ module Tina4
|
|
|
106
126
|
|
|
107
127
|
# Use a custom servlet that passes ALL methods (including OPTIONS) to Rack
|
|
108
128
|
rack_app = @app
|
|
129
|
+
servlet = build_rack_servlet(@host, @port.to_s)
|
|
130
|
+
|
|
131
|
+
@server.mount("/", servlet, rack_app)
|
|
132
|
+
|
|
133
|
+
# Test port (port + 1000) — stable, no-browser
|
|
134
|
+
@ai_server = nil
|
|
135
|
+
@ai_thread = nil
|
|
136
|
+
no_ai_port = %w[true 1 yes].include?(ENV.fetch("TINA4_NO_AI_PORT", "").downcase)
|
|
137
|
+
is_debug = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "").downcase)
|
|
138
|
+
|
|
139
|
+
if is_debug && !no_ai_port
|
|
140
|
+
ai_port = @port + 1000
|
|
141
|
+
begin
|
|
142
|
+
test = TCPServer.new("0.0.0.0", ai_port)
|
|
143
|
+
test.close
|
|
144
|
+
|
|
145
|
+
@ai_server = WEBrick::HTTPServer.new(
|
|
146
|
+
BindAddress: @host,
|
|
147
|
+
Port: ai_port,
|
|
148
|
+
Logger: WEBrick::Log.new(File::NULL),
|
|
149
|
+
AccessLog: []
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Wrap the rack app so AI-port requests are tagged
|
|
153
|
+
ai_rack_app = Tina4::AiPortRackApp.new(@app)
|
|
154
|
+
|
|
155
|
+
# Same servlet as the main port, bound to the AI port's host/port.
|
|
156
|
+
ai_servlet = build_rack_servlet(@host, ai_port.to_s)
|
|
157
|
+
|
|
158
|
+
@ai_server.mount("/", ai_servlet, ai_rack_app)
|
|
159
|
+
@ai_thread = Thread.new { @ai_server.start }
|
|
160
|
+
puts " Test Port: http://localhost:#{ai_port} (stable — no hot-reload)"
|
|
161
|
+
rescue Errno::EADDRINUSE
|
|
162
|
+
puts " Test Port: SKIPPED (port #{ai_port} in use)"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
@server.start
|
|
167
|
+
|
|
168
|
+
# Shutdown closes the listener FIRST, so #start returns as soon as the
|
|
169
|
+
# in-flight workers are joined - potentially while the signal handler's
|
|
170
|
+
# thread is still stopping background tasks and closing the database.
|
|
171
|
+
# Wait for that teardown instead of exiting out from under it.
|
|
172
|
+
Tina4::Shutdown.wait_for_completion
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def stop
|
|
176
|
+
@ai_server&.shutdown
|
|
177
|
+
@ai_thread&.join(5)
|
|
178
|
+
@server&.shutdown
|
|
179
|
+
# Drop our identity marker so a later takeover does not match a dead PID.
|
|
180
|
+
Tina4::PortTakeover.remove_pidfile(@port)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Dispatch a Rack-style env through the Tina4 app and return [status, headers, body].
|
|
184
|
+
#
|
|
185
|
+
# Useful for testing and embedding — does not require a running server.
|
|
186
|
+
# Cross-framework parity with Python and Node.js.
|
|
187
|
+
#
|
|
188
|
+
# @param env [Hash] A Rack environment hash
|
|
189
|
+
# @return [Array] Rack-style response triple [status, headers, body]
|
|
190
|
+
def handle(env)
|
|
191
|
+
@app.call(env)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Sibling loopback addresses to ALSO listen on, so `localhost` reaches this
|
|
195
|
+
# server whether the OS resolves it to IPv4 (127.0.0.1) or IPv6 (::1).
|
|
196
|
+
#
|
|
197
|
+
# Returns only the families a direct bind of *host* does not already cover;
|
|
198
|
+
# a host that is neither loopback nor a wildcard yields an empty list -- an
|
|
199
|
+
# explicit LAN address is bound exactly as asked, with no sibling. The host
|
|
200
|
+
# is normalised (downcased, surrounding whitespace and brackets stripped) so
|
|
201
|
+
# "[::1]", " ::1 " and "::1" all resolve alike.
|
|
202
|
+
#
|
|
203
|
+
# Mirrors the tina4-php Server::loopbackBindHosts mapping exactly. WEBrick
|
|
204
|
+
# binds "::1" WITHOUT brackets (PHP's stream URL needed "[::1]"; the brackets
|
|
205
|
+
# are not carried into Ruby).
|
|
206
|
+
#
|
|
207
|
+
# @param host [String] the host the main socket binds
|
|
208
|
+
# @return [Array<String>] extra bind addresses (possibly empty)
|
|
209
|
+
def self.loopback_bind_hosts(host)
|
|
210
|
+
normalized = host.to_s.strip.downcase.gsub(/\A\[+|\]+\z/, "")
|
|
211
|
+
case normalized
|
|
212
|
+
when "localhost" then ["127.0.0.1", "::1"]
|
|
213
|
+
when "127.0.0.1", "0.0.0.0" then ["::1"]
|
|
214
|
+
when "::1", "::" then ["127.0.0.1"]
|
|
215
|
+
else []
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
# Build the Rack->WEBrick servlet class bound to a specific host/port.
|
|
222
|
+
# The main port and the debug AI port mount an identical servlet; the ONLY
|
|
223
|
+
# difference is the host/port reported in the Rack env, bound below via
|
|
224
|
+
# define_method. Extracted from two verbatim-identical copies.
|
|
225
|
+
def build_rack_servlet(host, port)
|
|
109
226
|
servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
|
110
227
|
define_method(:initialize) do |server, app|
|
|
111
228
|
super(server)
|
|
@@ -186,146 +303,11 @@ module Tina4
|
|
|
186
303
|
end
|
|
187
304
|
end
|
|
188
305
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
port = @port.to_s
|
|
306
|
+
# Bind the per-server host/port the Rack env reports (the ONLY thing that
|
|
307
|
+
# differs between the main port and the AI port).
|
|
192
308
|
servlet.define_method(:webrick_req_host) { host }
|
|
193
309
|
servlet.define_method(:webrick_req_port) { port }
|
|
194
|
-
|
|
195
|
-
@server.mount("/", servlet, rack_app)
|
|
196
|
-
|
|
197
|
-
# Test port (port + 1000) — stable, no-browser
|
|
198
|
-
@ai_server = nil
|
|
199
|
-
@ai_thread = nil
|
|
200
|
-
no_ai_port = %w[true 1 yes].include?(ENV.fetch("TINA4_NO_AI_PORT", "").downcase)
|
|
201
|
-
is_debug = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "").downcase)
|
|
202
|
-
|
|
203
|
-
if is_debug && !no_ai_port
|
|
204
|
-
ai_port = @port + 1000
|
|
205
|
-
begin
|
|
206
|
-
test = TCPServer.new("0.0.0.0", ai_port)
|
|
207
|
-
test.close
|
|
208
|
-
|
|
209
|
-
@ai_server = WEBrick::HTTPServer.new(
|
|
210
|
-
BindAddress: @host,
|
|
211
|
-
Port: ai_port,
|
|
212
|
-
Logger: WEBrick::Log.new(File::NULL),
|
|
213
|
-
AccessLog: []
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
# Wrap the rack app so AI-port requests are tagged
|
|
217
|
-
ai_rack_app = Tina4::AiPortRackApp.new(@app)
|
|
218
|
-
|
|
219
|
-
# Build a servlet identical to the main one but bound to the AI port host/port
|
|
220
|
-
ai_host = @host
|
|
221
|
-
ai_port_str = ai_port.to_s
|
|
222
|
-
ai_servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
|
223
|
-
define_method(:initialize) do |server, app|
|
|
224
|
-
super(server)
|
|
225
|
-
@app = app
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
%w[GET POST PUT DELETE PATCH HEAD OPTIONS].each do |http_method|
|
|
229
|
-
define_method("do_#{http_method}") do |webrick_req, webrick_res|
|
|
230
|
-
handle_request(webrick_req, webrick_res)
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
define_method(:handle_request) do |webrick_req, webrick_res|
|
|
235
|
-
# Same accepted-a-moment-before-the-listener-closed race as the
|
|
236
|
-
# main servlet above - see the comment there.
|
|
237
|
-
if Tina4::Shutdown.shutting_down?
|
|
238
|
-
webrick_res.status = 503
|
|
239
|
-
webrick_res.body = '{"error":"Service shutting down"}'
|
|
240
|
-
webrick_res["content-type"] = "application/json"
|
|
241
|
-
return
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
Tina4::Shutdown.track_request do
|
|
245
|
-
env = build_rack_env(webrick_req)
|
|
246
|
-
status, headers, body = @app.call(env)
|
|
247
|
-
|
|
248
|
-
webrick_res.status = status
|
|
249
|
-
headers.each do |key, value|
|
|
250
|
-
if key.downcase == "set-cookie"
|
|
251
|
-
Array(value.split("\n")).each { |c| webrick_res.cookies << WEBrick::Cookie.parse_set_cookie(c) }
|
|
252
|
-
else
|
|
253
|
-
webrick_res[key] = value
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
response_body = ""
|
|
258
|
-
body.each { |chunk| response_body += chunk }
|
|
259
|
-
webrick_res.body = response_body
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
define_method(:build_rack_env) do |req|
|
|
264
|
-
input = StringIO.new(req.body || "")
|
|
265
|
-
env = {
|
|
266
|
-
"REQUEST_METHOD" => req.request_method,
|
|
267
|
-
"PATH_INFO" => req.path,
|
|
268
|
-
"QUERY_STRING" => req.query_string || "",
|
|
269
|
-
"SERVER_NAME" => webrick_req_host,
|
|
270
|
-
"SERVER_PORT" => webrick_req_port,
|
|
271
|
-
"CONTENT_TYPE" => req.content_type || "",
|
|
272
|
-
"CONTENT_LENGTH" => (req.content_length rescue 0).to_s,
|
|
273
|
-
"REMOTE_ADDR" => req.peeraddr&.last || "127.0.0.1",
|
|
274
|
-
"rack.input" => input,
|
|
275
|
-
"rack.errors" => $stderr,
|
|
276
|
-
"rack.url_scheme" => "http",
|
|
277
|
-
"rack.version" => [1, 3],
|
|
278
|
-
"rack.multithread" => true,
|
|
279
|
-
"rack.multiprocess" => false,
|
|
280
|
-
"rack.run_once" => false
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
req.header.each do |key, values|
|
|
284
|
-
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
285
|
-
env[env_key] = values.join(", ")
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
env
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
ai_servlet.define_method(:webrick_req_host) { ai_host }
|
|
293
|
-
ai_servlet.define_method(:webrick_req_port) { ai_port_str }
|
|
294
|
-
|
|
295
|
-
@ai_server.mount("/", ai_servlet, ai_rack_app)
|
|
296
|
-
@ai_thread = Thread.new { @ai_server.start }
|
|
297
|
-
puts " Test Port: http://localhost:#{ai_port} (stable — no hot-reload)"
|
|
298
|
-
rescue Errno::EADDRINUSE
|
|
299
|
-
puts " Test Port: SKIPPED (port #{ai_port} in use)"
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
@server.start
|
|
304
|
-
|
|
305
|
-
# Shutdown closes the listener FIRST, so #start returns as soon as the
|
|
306
|
-
# in-flight workers are joined - potentially while the signal handler's
|
|
307
|
-
# thread is still stopping background tasks and closing the database.
|
|
308
|
-
# Wait for that teardown instead of exiting out from under it.
|
|
309
|
-
Tina4::Shutdown.wait_for_completion
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def stop
|
|
313
|
-
@ai_server&.shutdown
|
|
314
|
-
@ai_thread&.join(5)
|
|
315
|
-
@server&.shutdown
|
|
316
|
-
# Drop our identity marker so a later takeover does not match a dead PID.
|
|
317
|
-
Tina4::PortTakeover.remove_pidfile(@port)
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
# Dispatch a Rack-style env through the Tina4 app and return [status, headers, body].
|
|
321
|
-
#
|
|
322
|
-
# Useful for testing and embedding — does not require a running server.
|
|
323
|
-
# Cross-framework parity with Python and Node.js.
|
|
324
|
-
#
|
|
325
|
-
# @param env [Hash] A Rack environment hash
|
|
326
|
-
# @return [Array] Rack-style response triple [status, headers, body]
|
|
327
|
-
def handle(env)
|
|
328
|
-
@app.call(env)
|
|
310
|
+
servlet
|
|
329
311
|
end
|
|
330
312
|
end
|
|
331
313
|
end
|
data/lib/tina4.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "tina4/database_result"
|
|
|
18
18
|
require_relative "tina4/graph"
|
|
19
19
|
require_relative "tina4/point"
|
|
20
20
|
require_relative "tina4/field_types"
|
|
21
|
+
require_relative "tina4/model_collection"
|
|
21
22
|
require_relative "tina4/orm"
|
|
22
23
|
require_relative "tina4/query_builder"
|
|
23
24
|
require_relative "tina4/migration"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.133
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -384,6 +384,7 @@ files:
|
|
|
384
384
|
- lib/tina4/metrics.rb
|
|
385
385
|
- lib/tina4/middleware.rb
|
|
386
386
|
- lib/tina4/migration.rb
|
|
387
|
+
- lib/tina4/model_collection.rb
|
|
387
388
|
- lib/tina4/mqtt.rb
|
|
388
389
|
- lib/tina4/mqtt_message.rb
|
|
389
390
|
- lib/tina4/orm.rb
|