tina4ruby 3.13.93 → 3.13.96
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 +883 -0
- data/README.md +1 -1
- data/lib/tina4/auth.rb +166 -87
- data/lib/tina4/auto_crud.rb +29 -32
- data/lib/tina4/cache_backends/base_backend.rb +19 -0
- data/lib/tina4/cache_backends/database_backend.rb +29 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +124 -13
- data/lib/tina4/cache_backends/memory_backend.rb +15 -0
- data/lib/tina4/cache_backends/redis_backend.rb +173 -52
- data/lib/tina4/cache_backends.rb +10 -1
- data/lib/tina4/cli.rb +35 -43
- data/lib/tina4/cors.rb +186 -30
- data/lib/tina4/database/sqlite3_adapter.rb +4 -1
- data/lib/tina4/database.rb +458 -48
- data/lib/tina4/database_adapter.rb +178 -0
- data/lib/tina4/database_result.rb +63 -17
- data/lib/tina4/database_url.rb +363 -0
- data/lib/tina4/dev.rb +0 -1
- data/lib/tina4/dev_admin.rb +118 -20
- data/lib/tina4/dev_mailbox.rb +5 -1
- data/lib/tina4/dispatch_pipeline.rb +605 -0
- data/lib/tina4/docstore.rb +274 -60
- data/lib/tina4/drivers/firebird_driver.rb +118 -4
- data/lib/tina4/drivers/mongodb_driver.rb +19 -4
- data/lib/tina4/drivers/mssql_driver.rb +73 -10
- data/lib/tina4/drivers/mysql_driver.rb +71 -4
- data/lib/tina4/drivers/odbc_driver.rb +40 -4
- data/lib/tina4/drivers/postgres_driver.rb +97 -10
- data/lib/tina4/drivers/sqlite_driver.rb +25 -3
- data/lib/tina4/env.rb +176 -34
- data/lib/tina4/field_types.rb +12 -0
- data/lib/tina4/frond.rb +102 -10
- data/lib/tina4/health.rb +30 -14
- data/lib/tina4/job.rb +15 -5
- data/lib/tina4/log.rb +236 -32
- data/lib/tina4/mcp.rb +11 -5
- data/lib/tina4/messenger.rb +317 -82
- data/lib/tina4/metrics.rb +179 -891
- data/lib/tina4/middleware.rb +191 -56
- data/lib/tina4/migration.rb +17 -1
- data/lib/tina4/orm.rb +114 -17
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/queue.rb +154 -9
- data/lib/tina4/queue_backends/kafka_backend.rb +191 -2
- data/lib/tina4/queue_backends/lite_backend.rb +121 -25
- data/lib/tina4/queue_backends/mongo_backend.rb +146 -10
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +194 -1
- data/lib/tina4/rack_app.rb +94 -316
- data/lib/tina4/request.rb +48 -8
- data/lib/tina4/response.rb +42 -1
- data/lib/tina4/response_cache.rb +142 -24
- data/lib/tina4/router.rb +141 -12
- data/lib/tina4/session.rb +243 -29
- data/lib/tina4/session_handlers/database_handler.rb +185 -20
- data/lib/tina4/session_handlers/file_handler.rb +113 -21
- data/lib/tina4/session_handlers/memcached_handler.rb +183 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +232 -15
- data/lib/tina4/session_handlers/mongo_wire_client.rb +300 -0
- data/lib/tina4/session_handlers/redis_handler.rb +20 -6
- data/lib/tina4/session_handlers/valkey_handler.rb +18 -4
- data/lib/tina4/shutdown.rb +180 -30
- data/lib/tina4/sql_translator.rb +110 -0
- data/lib/tina4/swagger.rb +50 -18
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -6
- data/lib/tina4.rb +301 -38
- metadata +35 -17
- data/lib/tina4/scss_compiler.rb +0 -349
data/lib/tina4.rb
CHANGED
|
@@ -9,7 +9,10 @@ require_relative "tina4/env"
|
|
|
9
9
|
require_relative "tina4/router"
|
|
10
10
|
require_relative "tina4/request"
|
|
11
11
|
require_relative "tina4/response"
|
|
12
|
+
require_relative "tina4/dispatch_pipeline"
|
|
12
13
|
require_relative "tina4/rack_app"
|
|
14
|
+
require_relative "tina4/database_adapter"
|
|
15
|
+
require_relative "tina4/database_url"
|
|
13
16
|
require_relative "tina4/database"
|
|
14
17
|
require_relative "tina4/database_result"
|
|
15
18
|
require_relative "tina4/field_types"
|
|
@@ -53,6 +56,51 @@ require_relative "tina4/mcp"
|
|
|
53
56
|
require_relative "tina4/realtime"
|
|
54
57
|
|
|
55
58
|
module Tina4
|
|
59
|
+
# Bind address, with the framework name winning over the bare one.
|
|
60
|
+
#
|
|
61
|
+
# TINA4_HOST > HOST (deprecated) > default. See resolve_bind_port.
|
|
62
|
+
def self.resolve_bind_host(default = "0.0.0.0")
|
|
63
|
+
tina4_host = ENV["TINA4_HOST"]
|
|
64
|
+
return tina4_host if tina4_host && !tina4_host.empty?
|
|
65
|
+
|
|
66
|
+
legacy = ENV["HOST"]
|
|
67
|
+
if legacy && !legacy.empty?
|
|
68
|
+
warn_deprecated_bind_var("HOST", "TINA4_HOST", legacy)
|
|
69
|
+
return legacy
|
|
70
|
+
end
|
|
71
|
+
default
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Bind port, with the framework name winning over the bare one.
|
|
75
|
+
#
|
|
76
|
+
# TINA4_PORT > PORT (deprecated) > default. Bare PORT is a name anything can
|
|
77
|
+
# set - a shared CI runner, a PaaS, another tool - and it should never
|
|
78
|
+
# outrank the framework's own variable. It is still honoured so no deployment
|
|
79
|
+
# breaks; the warning is what drives the move. Removal is 3.14.
|
|
80
|
+
def self.resolve_bind_port(default = 7147)
|
|
81
|
+
tina4_port = ENV["TINA4_PORT"]
|
|
82
|
+
return tina4_port.to_i if tina4_port && tina4_port.match?(/\A\d+\z/)
|
|
83
|
+
|
|
84
|
+
legacy = ENV["PORT"]
|
|
85
|
+
if legacy && legacy.match?(/\A\d+\z/)
|
|
86
|
+
warn_deprecated_bind_var("PORT", "TINA4_PORT", legacy)
|
|
87
|
+
return legacy.to_i
|
|
88
|
+
end
|
|
89
|
+
default
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Warn ONCE per variable. Repeated on every call it is noise people filter.
|
|
93
|
+
def self.warn_deprecated_bind_var(old_name, new_name, value)
|
|
94
|
+
@warned_bind_vars ||= {}
|
|
95
|
+
return if @warned_bind_vars[old_name]
|
|
96
|
+
|
|
97
|
+
@warned_bind_vars[old_name] = true
|
|
98
|
+
Tina4::Log.warning(
|
|
99
|
+
"#{old_name} is deprecated and will be removed in 3.14 - use " \
|
|
100
|
+
"#{new_name} instead (using #{value} from #{old_name})"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
56
104
|
# ── Lazy-loaded: database drivers ─────────────────────────────────────
|
|
57
105
|
module Drivers
|
|
58
106
|
autoload :SchemaSplit, File.expand_path("tina4/drivers/schema_split", __dir__)
|
|
@@ -71,6 +119,7 @@ module Tina4
|
|
|
71
119
|
autoload :RedisHandler, File.expand_path("tina4/session_handlers/redis_handler", __dir__)
|
|
72
120
|
autoload :MongoHandler, File.expand_path("tina4/session_handlers/mongo_handler", __dir__)
|
|
73
121
|
autoload :ValkeyHandler, File.expand_path("tina4/session_handlers/valkey_handler", __dir__)
|
|
122
|
+
autoload :MemcachedHandler, File.expand_path("tina4/session_handlers/memcached_handler", __dir__)
|
|
74
123
|
autoload :DatabaseHandler, File.expand_path("tina4/session_handlers/database_handler", __dir__)
|
|
75
124
|
end
|
|
76
125
|
|
|
@@ -127,10 +176,148 @@ module Tina4
|
|
|
127
176
|
autoload :Messenger, File.expand_path("tina4/messenger", __dir__)
|
|
128
177
|
autoload :MessengerError, File.expand_path("tina4/messenger", __dir__)
|
|
129
178
|
autoload :MessengerConnectionError, File.expand_path("tina4/messenger", __dir__)
|
|
130
|
-
|
|
179
|
+
|
|
180
|
+
# Factory for a Messenger configured from the environment.
|
|
181
|
+
#
|
|
182
|
+
# Defined HERE, eagerly, and not in messenger.rb: `autoload` only fires on a
|
|
183
|
+
# CONSTANT reference, and a module function is not a constant. A cold
|
|
184
|
+
# `require "tina4"; Tina4.create_messenger` therefore raised NoMethodError until
|
|
185
|
+
# something else happened to touch Tina4::Messenger first -- the documented entry
|
|
186
|
+
# point was unreachable on a fresh process. Naming the constant below is what
|
|
187
|
+
# triggers the autoload.
|
|
188
|
+
def self.create_messenger(**options)
|
|
189
|
+
Tina4::Messenger.create_messenger(**options)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# ── dotenv surface, at the top level ──────────────────────────────
|
|
193
|
+
#
|
|
194
|
+
# Python, PHP and Node all expose these as plain module/namespace functions.
|
|
195
|
+
# Ruby had them only as Tina4::Env.*, so the obvious cross-framework call
|
|
196
|
+
# (Tina4.load_env) raised NoMethodError - a parity defect by the audit's rule
|
|
197
|
+
# that a concept carries one name in all four, differing only in casing.
|
|
198
|
+
#
|
|
199
|
+
# These are thin delegators, defined EAGERLY for the same reason
|
|
200
|
+
# create_messenger above is: `autoload` fires on a CONSTANT reference, and a
|
|
201
|
+
# module function is not a constant, so a cold `require "tina4"` would
|
|
202
|
+
# otherwise leave the documented entry point unreachable until something else
|
|
203
|
+
# happened to touch Tina4::Env first. Naming Tina4::Env inside each body is
|
|
204
|
+
# what triggers the autoload.
|
|
205
|
+
#
|
|
206
|
+
# Tina4::Env.* stays as the implementation namespace (PHP keeps Env:: the same
|
|
207
|
+
# way). Neither is an alias of the other - one is the surface, one is the home.
|
|
208
|
+
|
|
209
|
+
# Load .env.local then .env from a ROOT DIRECTORY (canonical in all four).
|
|
210
|
+
def self.load_env(root = nil)
|
|
211
|
+
root.nil? ? Tina4::Env.load_env : Tina4::Env.load_env(root)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.get_env(key, default = nil)
|
|
215
|
+
Tina4::Env.get_env(key, default)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Raises KeyError naming EVERY missing variable; returns the requested map.
|
|
219
|
+
def self.require_env(*keys)
|
|
220
|
+
Tina4::Env.require_env(*keys)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def self.has_env?(key)
|
|
224
|
+
Tina4::Env.has_env?(key)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.all_env
|
|
228
|
+
Tina4::Env.all_env
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.reset_env
|
|
232
|
+
Tina4::Env.reset_env
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def self.truthy?(value)
|
|
236
|
+
Tina4::Env.is_truthy(value)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Parsed TINA4_TRUSTED_PROXIES, cached on the raw env string so a change is
|
|
240
|
+
# picked up but the parse does not run per request. [raw_value, networks]
|
|
241
|
+
@trusted_proxy_cache = [nil, [].freeze]
|
|
242
|
+
|
|
243
|
+
# The configured trusted-proxy networks, from TINA4_TRUSTED_PROXIES.
|
|
244
|
+
#
|
|
245
|
+
# Comma-separated exact addresses and/or CIDR ranges, IPv4 and IPv6:
|
|
246
|
+
# "10.0.0.0/8, 192.168.1.5, ::1, fd00::/8". Empty or unset means trust
|
|
247
|
+
# NOTHING, which is the secure default: X-Forwarded-For is then ignored
|
|
248
|
+
# entirely and the raw socket peer identifies the client. See ADR-0019.
|
|
249
|
+
def self.trusted_proxy_networks
|
|
250
|
+
raw = ENV["TINA4_TRUSTED_PROXIES"].to_s
|
|
251
|
+
cached_raw, cached_networks = @trusted_proxy_cache
|
|
252
|
+
return cached_networks if raw == cached_raw
|
|
253
|
+
|
|
254
|
+
networks = []
|
|
255
|
+
raw.split(",").each do |entry|
|
|
256
|
+
entry = normalise_ip(entry)
|
|
257
|
+
next if entry.empty?
|
|
258
|
+
|
|
259
|
+
begin
|
|
260
|
+
networks << IPAddr.new(entry)
|
|
261
|
+
rescue IPAddr::Error, ArgumentError
|
|
262
|
+
# Loud, and exactly once per distinct config value (the cache below
|
|
263
|
+
# means this parse runs once). A silently-skipped entry would leave a
|
|
264
|
+
# real proxy untrusted, which looks like the app over-limiting every
|
|
265
|
+
# client - a very expensive typo to debug.
|
|
266
|
+
Tina4::Log.error(
|
|
267
|
+
"TINA4_TRUSTED_PROXIES: ignoring invalid entry '#{entry}' - " \
|
|
268
|
+
"expected an IP address or CIDR range, e.g. 10.0.0.0/8 or 192.168.1.5"
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
@trusted_proxy_cache = [raw, networks.freeze]
|
|
273
|
+
@trusted_proxy_cache[1]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Is this address a configured trusted proxy?
|
|
277
|
+
def self.trusted_proxy?(address)
|
|
278
|
+
networks = trusted_proxy_networks
|
|
279
|
+
return false if networks.empty?
|
|
280
|
+
|
|
281
|
+
address = normalise_ip(address)
|
|
282
|
+
return false if address.empty?
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
parsed = IPAddr.new(address)
|
|
286
|
+
rescue IPAddr::Error, ArgumentError
|
|
287
|
+
return false
|
|
288
|
+
end
|
|
289
|
+
# A peer arriving as ::ffff:10.0.0.1 must match an allow-list entry of
|
|
290
|
+
# 10.0.0.0/8 - dual-stack listeners hand out the mapped form routinely.
|
|
291
|
+
parsed = parsed.native if parsed.ipv4_mapped?
|
|
292
|
+
networks.any? { |network| network.include?(parsed) }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Strip the decorations a peer address can arrive with: the "[::1]" bracket
|
|
296
|
+
# form and an IPv6 zone id ("fe80::1%eth0").
|
|
297
|
+
def self.normalise_ip(value)
|
|
298
|
+
value = value.to_s.strip
|
|
299
|
+
value = value[1...value.index("]")].to_s if value.start_with?("[") && value.include?("]")
|
|
300
|
+
value = value.split("%", 2).first.to_s if value.include?("%")
|
|
301
|
+
value
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def self.env_bool(name, default: false)
|
|
305
|
+
Tina4::Env.bool(name, default: default)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.env_int(name, default: 0)
|
|
309
|
+
Tina4::Env.int(name, default: default)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def self.env_float(name, default: 0.0)
|
|
313
|
+
Tina4::Env.float(name, default: default)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def self.env_str(name, default: "")
|
|
317
|
+
Tina4::Env.str(name, default: default)
|
|
318
|
+
end
|
|
131
319
|
autoload :IMAP_CONNECTION_ERRORS, File.expand_path("tina4/messenger", __dir__)
|
|
132
320
|
autoload :DocStore, File.expand_path("tina4/docstore", __dir__)
|
|
133
|
-
autoload :ScssCompiler, File.expand_path("tina4/scss_compiler", __dir__)
|
|
134
321
|
autoload :FakeData, File.expand_path("tina4/seeder", __dir__)
|
|
135
322
|
autoload :WSDL, File.expand_path("tina4/wsdl", __dir__)
|
|
136
323
|
# Validator is a counted framework feature and belongs on the main
|
|
@@ -245,8 +432,19 @@ module Tina4
|
|
|
245
432
|
# (.env.local loads first, both first-wins, so a real env var always wins).
|
|
246
433
|
Tina4::Env.load_env(root_dir)
|
|
247
434
|
|
|
248
|
-
# Setup debug logging
|
|
249
|
-
|
|
435
|
+
# Setup debug logging.
|
|
436
|
+
#
|
|
437
|
+
# No argument here, deliberately (ADR-0041). This used to pass `root_dir`,
|
|
438
|
+
# and since Ruby correctly lets an explicit argument beat the environment,
|
|
439
|
+
# that had two measured consequences in every booted Ruby app:
|
|
440
|
+
# TINA4_LOG_DIR was ENTIRELY DEAD -- the documented variable could not
|
|
441
|
+
# move the logs anywhere -- and because root_dir is an existing directory
|
|
442
|
+
# it became the log directory itself, so tina4.log and error.log were
|
|
443
|
+
# written into the PROJECT ROOT rather than the documented logs/.
|
|
444
|
+
# root_dir is the framework's default, not a user instruction, so it
|
|
445
|
+
# belongs below the environment. Resolution is now TINA4_LOG_DIR, then
|
|
446
|
+
# logs/, matching Python and Node.
|
|
447
|
+
Tina4::Log.configure
|
|
250
448
|
Tina4::Log.info("Tina4 Ruby v#{VERSION} initializing...")
|
|
251
449
|
|
|
252
450
|
# Fail-safe dev secret: in dev (and NOT CI/prod) mint a per-machine
|
|
@@ -267,6 +465,13 @@ module Tina4
|
|
|
267
465
|
# Connect database if configured
|
|
268
466
|
setup_database
|
|
269
467
|
|
|
468
|
+
# Framework routes FIRST, then the app's. Routes resolve in registration
|
|
469
|
+
# order, so anything registered later cannot shadow these - and an app
|
|
470
|
+
# with an ordinary catch-all (`any("/{slug}")`) would otherwise swallow
|
|
471
|
+
# /__health, which is exactly what a container health check probes.
|
|
472
|
+
# Python registers its built-ins first for the same reason.
|
|
473
|
+
register_builtin_routes!
|
|
474
|
+
|
|
270
475
|
# Auto-discover routes
|
|
271
476
|
auto_discover(root_dir)
|
|
272
477
|
|
|
@@ -333,16 +538,30 @@ module Tina4
|
|
|
333
538
|
|
|
334
539
|
initialize!(root_dir) unless @root_dir
|
|
335
540
|
|
|
336
|
-
# Built-in routes
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
541
|
+
# Built-in routes now register inside initialize!, BEFORE route discovery,
|
|
542
|
+
# so they cannot be shadowed by an app catch-all. Kept here as a safety net
|
|
543
|
+
# for a caller that reaches run! without initialize! having run (the guard
|
|
544
|
+
# above skips it when @root_dir is already set). register! is idempotent
|
|
545
|
+
# and Router.add replaces in place, so a second call is a no-op.
|
|
546
|
+
#
|
|
547
|
+
# They used to be registered ONLY by the CLI's cmd_start, so an app booted
|
|
548
|
+
# the documented way -- `Tina4.run!` from app.rb, which is what
|
|
549
|
+
# `tina4 init ruby` scaffolds -- served 404 on /health while the identical
|
|
550
|
+
# code under `tina4ruby serve` served 200.
|
|
342
551
|
register_builtin_routes!
|
|
343
552
|
|
|
344
|
-
|
|
345
|
-
|
|
553
|
+
# TINA4_HOST/TINA4_PORT BEFORE the bare names, not after.
|
|
554
|
+
#
|
|
555
|
+
# This had both the wrong way round: ENV.fetch("PORT", ...) returns PORT
|
|
556
|
+
# whenever it is set, so a stray OS-level HOST or PORT outranked the
|
|
557
|
+
# framework's own variable - the one the CLI documents and prefers, and
|
|
558
|
+
# the one Tina4::WebServer a few files away already reads first. Two code
|
|
559
|
+
# paths in one framework disagreeing about the same variable.
|
|
560
|
+
#
|
|
561
|
+
# Bare HOST/PORT stay honoured so nothing breaks, and warn so the
|
|
562
|
+
# migration happens. Removal is 3.14.
|
|
563
|
+
host = Tina4.resolve_bind_host
|
|
564
|
+
port = Tina4.resolve_bind_port(7147)
|
|
346
565
|
|
|
347
566
|
actual_port = find_available_port(port)
|
|
348
567
|
if actual_port != port
|
|
@@ -357,32 +576,10 @@ module Tina4
|
|
|
357
576
|
is_debug = Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
|
|
358
577
|
|
|
359
578
|
# Try Puma first (production-grade), fall back to WEBrick
|
|
360
|
-
if !is_debug
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
require "puma/launcher"
|
|
365
|
-
|
|
366
|
-
config = Puma::Configuration.new do |user_config|
|
|
367
|
-
user_config.bind "tcp://#{host}:#{port}"
|
|
368
|
-
user_config.app app
|
|
369
|
-
user_config.threads 0, 16
|
|
370
|
-
user_config.workers 0
|
|
371
|
-
user_config.environment "production"
|
|
372
|
-
user_config.log_requests false
|
|
373
|
-
user_config.quiet
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
Tina4::Log.info("Production server: puma")
|
|
377
|
-
Tina4::Shutdown.setup
|
|
378
|
-
|
|
379
|
-
open_browser(url)
|
|
380
|
-
launcher = Puma::Launcher.new(config)
|
|
381
|
-
launcher.run
|
|
382
|
-
return
|
|
383
|
-
rescue LoadError
|
|
384
|
-
# Puma not installed, fall through to WEBrick
|
|
385
|
-
end
|
|
579
|
+
if !is_debug && !builtin_webserver_pinned? && puma_available?
|
|
580
|
+
open_browser(url)
|
|
581
|
+
start_puma_server(app, host: host, port: port)
|
|
582
|
+
return
|
|
386
583
|
end
|
|
387
584
|
|
|
388
585
|
Tina4::Log.info("Development server: WEBrick")
|
|
@@ -391,6 +588,72 @@ module Tina4
|
|
|
391
588
|
server.start
|
|
392
589
|
end
|
|
393
590
|
|
|
591
|
+
# TINA4_DEFAULT_WEBSERVER=TRUE pins Tina4's BUILT-IN server (WEBrick) even
|
|
592
|
+
# in production, where Puma would otherwise be chosen. Unset/FALSE keeps
|
|
593
|
+
# today's behaviour, so this is non-breaking.
|
|
594
|
+
#
|
|
595
|
+
# This is NOT the remedy for a production shutdown problem - an operator
|
|
596
|
+
# must never have to give up Puma to get their database connections closed.
|
|
597
|
+
# It exists so CI can pin the built-in server deterministically without
|
|
598
|
+
# having to switch TINA4_DEBUG on to get there.
|
|
599
|
+
def builtin_webserver_pinned?
|
|
600
|
+
Tina4::Env.is_truthy(ENV["TINA4_DEFAULT_WEBSERVER"])
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Is Puma loadable? Separate from start_puma_server so the caller can fall
|
|
604
|
+
# back to WEBrick BEFORE any side effect (opening a browser) happens.
|
|
605
|
+
def puma_available?
|
|
606
|
+
require "puma"
|
|
607
|
+
require "puma/configuration"
|
|
608
|
+
require "puma/launcher"
|
|
609
|
+
true
|
|
610
|
+
rescue LoadError
|
|
611
|
+
false
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Boot the production server (Puma) with Tina4's shutdown contract wired in.
|
|
615
|
+
#
|
|
616
|
+
# Feature 9's OUTCOMES are the framework's contract whichever server owns
|
|
617
|
+
# the socket; the MECHANISM differs per server. Puma already stops
|
|
618
|
+
# accepting and drains in-flight requests properly, so that is CONFIGURED,
|
|
619
|
+
# not reimplemented:
|
|
620
|
+
#
|
|
621
|
+
# * TINA4_SHUTDOWN_TIMEOUT -> Puma's force_shutdown_after. Puma's default
|
|
622
|
+
# is :forever, so before this the documented env var meant NOTHING on
|
|
623
|
+
# the path operators actually run.
|
|
624
|
+
# * raise_exception_on_sigterm false -> Puma's default is true, which
|
|
625
|
+
# re-raises SignalException after the graceful stop and terminates the
|
|
626
|
+
# process BY the signal. The contract is a clean exit 0.
|
|
627
|
+
#
|
|
628
|
+
# What Puma cannot know about is ours, and runs in the ensure: live
|
|
629
|
+
# WebSocket peers get RFC 6455 1001, background tasks stop, and every
|
|
630
|
+
# database connection is closed. Nothing but Tina4 knows those connections
|
|
631
|
+
# exist, so without this they leaked on every production shutdown.
|
|
632
|
+
def start_puma_server(app, host:, port:)
|
|
633
|
+
# Puma owns INT/TERM here, so Tina4 installs no handlers of its own.
|
|
634
|
+
Tina4::Shutdown.setup(trap_signals: false)
|
|
635
|
+
shutdown_timeout = Tina4::Shutdown.timeout
|
|
636
|
+
|
|
637
|
+
config = Puma::Configuration.new do |user_config|
|
|
638
|
+
user_config.bind "tcp://#{host}:#{port}"
|
|
639
|
+
user_config.app app
|
|
640
|
+
user_config.threads 0, 16
|
|
641
|
+
user_config.workers 0
|
|
642
|
+
user_config.environment "production"
|
|
643
|
+
user_config.log_requests false
|
|
644
|
+
user_config.quiet
|
|
645
|
+
user_config.force_shutdown_after shutdown_timeout
|
|
646
|
+
user_config.raise_exception_on_sigterm false
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
Tina4::Log.info("Production server: puma (TINA4_SHUTDOWN_TIMEOUT=#{shutdown_timeout}s)")
|
|
650
|
+
begin
|
|
651
|
+
Puma::Launcher.new(config).run
|
|
652
|
+
ensure
|
|
653
|
+
Tina4::Shutdown.release_resources
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
394
657
|
# DSL methods for route registration
|
|
395
658
|
# GET is public by default (matching tina4_python behavior)
|
|
396
659
|
# POST/PUT/PATCH/DELETE are secured by default — use auth: false to make public
|
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.96
|
|
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-07
|
|
11
|
+
date: 2026-08-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -52,20 +52,6 @@ dependencies:
|
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '6.0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: jwt
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - "~>"
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '2.7'
|
|
62
|
-
type: :runtime
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - "~>"
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '2.7'
|
|
69
55
|
- !ruby/object:Gem::Dependency
|
|
70
56
|
name: net-smtp
|
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -206,6 +192,20 @@ dependencies:
|
|
|
206
192
|
- - "~>"
|
|
207
193
|
- !ruby/object:Gem::Version
|
|
208
194
|
version: '2.19'
|
|
195
|
+
- !ruby/object:Gem::Dependency
|
|
196
|
+
name: bigdecimal
|
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
|
198
|
+
requirements:
|
|
199
|
+
- - "~>"
|
|
200
|
+
- !ruby/object:Gem::Version
|
|
201
|
+
version: '4.0'
|
|
202
|
+
type: :development
|
|
203
|
+
prerelease: false
|
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
205
|
+
requirements:
|
|
206
|
+
- - "~>"
|
|
207
|
+
- !ruby/object:Gem::Version
|
|
208
|
+
version: '4.0'
|
|
209
209
|
- !ruby/object:Gem::Dependency
|
|
210
210
|
name: pg
|
|
211
211
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -290,6 +290,20 @@ dependencies:
|
|
|
290
290
|
- - "~>"
|
|
291
291
|
- !ruby/object:Gem::Version
|
|
292
292
|
version: '1.50'
|
|
293
|
+
- !ruby/object:Gem::Dependency
|
|
294
|
+
name: openapi3_parser
|
|
295
|
+
requirement: !ruby/object:Gem::Requirement
|
|
296
|
+
requirements:
|
|
297
|
+
- - "~>"
|
|
298
|
+
- !ruby/object:Gem::Version
|
|
299
|
+
version: '0.10'
|
|
300
|
+
type: :development
|
|
301
|
+
prerelease: false
|
|
302
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
303
|
+
requirements:
|
|
304
|
+
- - "~>"
|
|
305
|
+
- !ruby/object:Gem::Version
|
|
306
|
+
version: '0.10'
|
|
293
307
|
description: 'TINA4: The Intelligent Native Application 4ramework for Ruby — Simple.
|
|
294
308
|
Fast. Human. Built for AI. Built for you. 54 built-in features, minimal dependencies
|
|
295
309
|
(Rack-based; the runtime gems are production-deployment essentials, not framework
|
|
@@ -330,11 +344,14 @@ files:
|
|
|
330
344
|
- lib/tina4/crud.rb
|
|
331
345
|
- lib/tina4/database.rb
|
|
332
346
|
- lib/tina4/database/sqlite3_adapter.rb
|
|
347
|
+
- lib/tina4/database_adapter.rb
|
|
333
348
|
- lib/tina4/database_result.rb
|
|
349
|
+
- lib/tina4/database_url.rb
|
|
334
350
|
- lib/tina4/debug.rb
|
|
335
351
|
- lib/tina4/dev.rb
|
|
336
352
|
- lib/tina4/dev_admin.rb
|
|
337
353
|
- lib/tina4/dev_mailbox.rb
|
|
354
|
+
- lib/tina4/dispatch_pipeline.rb
|
|
338
355
|
- lib/tina4/docs.rb
|
|
339
356
|
- lib/tina4/docstore.rb
|
|
340
357
|
- lib/tina4/drivers/firebird_driver.rb
|
|
@@ -434,14 +451,15 @@ files:
|
|
|
434
451
|
- lib/tina4/scss/tina4css/base.scss
|
|
435
452
|
- lib/tina4/scss/tina4css/colors.scss
|
|
436
453
|
- lib/tina4/scss/tina4css/tina4.scss
|
|
437
|
-
- lib/tina4/scss_compiler.rb
|
|
438
454
|
- lib/tina4/seeder.rb
|
|
439
455
|
- lib/tina4/service.rb
|
|
440
456
|
- lib/tina4/service_runner.rb
|
|
441
457
|
- lib/tina4/session.rb
|
|
442
458
|
- lib/tina4/session_handlers/database_handler.rb
|
|
443
459
|
- lib/tina4/session_handlers/file_handler.rb
|
|
460
|
+
- lib/tina4/session_handlers/memcached_handler.rb
|
|
444
461
|
- lib/tina4/session_handlers/mongo_handler.rb
|
|
462
|
+
- lib/tina4/session_handlers/mongo_wire_client.rb
|
|
445
463
|
- lib/tina4/session_handlers/redis_handler.rb
|
|
446
464
|
- lib/tina4/session_handlers/resp_client.rb
|
|
447
465
|
- lib/tina4/session_handlers/valkey_handler.rb
|