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.
@@ -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
- reload = ai_port ? "0" : "1"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.131"
4
+ VERSION = "3.13.133"
5
5
  end
@@ -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
- # Store host/port for the servlet's build_rack_env
190
- host = @host
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.131
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-02 00:00:00.000000000 Z
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