tina4ruby 3.13.141 → 3.13.143
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 +22 -0
- data/lib/tina4/cli.rb +7 -2
- data/lib/tina4/crud.rb +61 -20
- data/lib/tina4/csp.rb +119 -0
- data/lib/tina4/error_overlay.rb +79 -43
- data/lib/tina4/frond.rb +7 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +25 -11
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +30 -10
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +34 -15
- data/lib/tina4/graphql.rb +9 -3
- data/lib/tina4/job.rb +17 -0
- data/lib/tina4/middleware.rb +16 -6
- data/lib/tina4/migration.rb +49 -41
- data/lib/tina4/queue_backends/kafka_backend.rb +12 -31
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +15 -37
- data/lib/tina4/queue_backends/republish_retry_policy.rb +35 -0
- data/lib/tina4/rack_app.rb +70 -23
- data/lib/tina4/response.rb +6 -1
- data/lib/tina4/session.rb +145 -10
- data/lib/tina4/session_handlers/redis_handler.rb +7 -51
- data/lib/tina4/session_handlers/resp_session_handler.rb +75 -0
- data/lib/tina4/session_handlers/valkey_handler.rb +6 -51
- data/lib/tina4/template.rb +11 -3
- data/lib/tina4/templates/errors/302.twig +1 -1
- data/lib/tina4/templates/errors/401.twig +2 -2
- data/lib/tina4/templates/errors/403.twig +1 -1
- data/lib/tina4/templates/errors/404.twig +1 -1
- data/lib/tina4/templates/errors/500.twig +1 -1
- data/lib/tina4/templates/errors/502.twig +2 -2
- data/lib/tina4/templates/errors/503.twig +2 -2
- data/lib/tina4/templates/errors/base.twig +4 -1
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +30 -14
- data/lib/tina4/websocket.rb +26 -2
- data/lib/tina4/websocket_backplane.rb +16 -13
- data/lib/tina4.rb +1 -0
- metadata +5 -2
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
require "json"
|
|
9
9
|
require_relative "resp_client"
|
|
10
|
+
require_relative "resp_session_handler"
|
|
10
11
|
|
|
11
12
|
module Tina4
|
|
12
13
|
module SessionHandlers
|
|
13
14
|
# Valkey-backed session handler. Valkey speaks the RESP protocol, so it works
|
|
14
15
|
# through the `redis` gem when installed (parity with Python/Node); otherwise
|
|
15
16
|
# it speaks raw RESP over a TCP socket via RespClient — zero dependencies.
|
|
16
|
-
# Same as RedisHandler
|
|
17
|
+
# Same behaviour as RedisHandler (read/write/destroy/cleanup and the
|
|
18
|
+
# gem-client build come from the shared RespSessionHandler) but reads
|
|
19
|
+
# VALKEY-prefixed configuration variables.
|
|
17
20
|
class ValkeyHandler
|
|
21
|
+
include RespSessionHandler
|
|
22
|
+
|
|
18
23
|
def initialize(options = {})
|
|
19
24
|
@prefix = options[:prefix] || ENV["TINA4_SESSION_VALKEY_PREFIX"] || "tina4:session:"
|
|
20
25
|
# TINA4_SESSION_VALKEY_TTL stays as the valkey-specific override, but it
|
|
@@ -29,56 +34,6 @@ module Tina4
|
|
|
29
34
|
@redis = build_gem_client
|
|
30
35
|
@resp = @redis ? nil : RespClient.new(host: @host, port: @port, password: @password, db: @db)
|
|
31
36
|
end
|
|
32
|
-
|
|
33
|
-
def read(session_id)
|
|
34
|
-
key = "#{@prefix}#{session_id}"
|
|
35
|
-
data = @redis ? @redis.get(key) : @resp.get(key)
|
|
36
|
-
return nil unless data
|
|
37
|
-
JSON.parse(data)
|
|
38
|
-
rescue JSON::ParserError
|
|
39
|
-
nil
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Write session data. A per-call +ttl+ WINS over the handler default, so
|
|
43
|
-
# asking for a 60s session really gets 60s; 0 uses the default. Every
|
|
44
|
-
# handler in every Tina4 framework takes this third argument -- Session#write
|
|
45
|
-
# passes it, and a handler that did not accept it raised ArgumentError,
|
|
46
|
-
# which safe_write swallowed into a silent STALE write.
|
|
47
|
-
#
|
|
48
|
-
# @param session_id [String] the session id
|
|
49
|
-
# @param data [Hash] the payload to store
|
|
50
|
-
# @param ttl [Integer] per-call lifetime in seconds; 0 uses the handler default
|
|
51
|
-
def write(session_id, data, ttl = 0)
|
|
52
|
-
key = "#{@prefix}#{session_id}"
|
|
53
|
-
payload = JSON.generate(data)
|
|
54
|
-
effective_ttl = ttl.to_i.positive? ? ttl.to_i : @ttl
|
|
55
|
-
if @redis
|
|
56
|
-
@redis.setex(key, effective_ttl, payload)
|
|
57
|
-
else
|
|
58
|
-
@resp.setex(key, effective_ttl, payload)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def destroy(session_id)
|
|
63
|
-
key = "#{@prefix}#{session_id}"
|
|
64
|
-
@redis ? @redis.del(key) : @resp.del(key)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def cleanup
|
|
68
|
-
# Valkey handles TTL automatically (same as Redis)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
# Use the official `redis` gem (RESP-compatible with Valkey) only when the
|
|
74
|
-
# REAL gem is loadable; otherwise nil so the caller falls back to raw RESP.
|
|
75
|
-
def build_gem_client
|
|
76
|
-
require "redis"
|
|
77
|
-
return nil unless defined?(::Redis::VERSION)
|
|
78
|
-
Redis.new(host: @host, port: @port, db: @db, password: @password)
|
|
79
|
-
rescue LoadError
|
|
80
|
-
nil
|
|
81
|
-
end
|
|
82
37
|
end
|
|
83
38
|
end
|
|
84
39
|
end
|
data/lib/tina4/template.rb
CHANGED
|
@@ -56,7 +56,11 @@ module Tina4
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def globals
|
|
59
|
-
|
|
59
|
+
# csp_nonce() is a built-in template global (ADR-0088): a template can
|
|
60
|
+
# stamp the current response's CSP nonce on an inline <style>/<script>
|
|
61
|
+
# — <style nonce="{{ csp_nonce() }}">. Matches the Frond engine global
|
|
62
|
+
# and the value the security middleware names in the CSP header.
|
|
63
|
+
@globals ||= { "csp_nonce" => -> { Tina4::Csp.current_nonce } }
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
def add_global(key, value)
|
|
@@ -87,7 +91,11 @@ module Tina4
|
|
|
87
91
|
error_dirs = TEMPLATE_DIRS.map { |d| File.join(Dir.pwd, d, "errors") }
|
|
88
92
|
error_dirs << File.join(File.dirname(__FILE__), "templates", "errors")
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
# csp_nonce() (ADR-0088) so the error twigs can carry a nonce on their
|
|
95
|
+
# inline <style>; render_error builds its own context and does not merge
|
|
96
|
+
# globals, so inject it here explicitly.
|
|
97
|
+
context = { "code" => code, "csp_nonce" => -> { Tina4::Csp.current_nonce } }
|
|
98
|
+
.merge(data.transform_keys(&:to_s))
|
|
91
99
|
|
|
92
100
|
error_dirs.each do |dir|
|
|
93
101
|
%w[.twig .html .erb].each do |ext|
|
|
@@ -152,7 +160,7 @@ module Tina4
|
|
|
152
160
|
|
|
153
161
|
def error_overlay_css(color)
|
|
154
162
|
<<~CSS
|
|
155
|
-
<style>
|
|
163
|
+
<style nonce="#{Tina4::Csp.current_nonce}">
|
|
156
164
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
157
165
|
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
158
166
|
.error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
<div class="spinner"></div>
|
|
10
10
|
<div class="error-title">Redirecting…</div>
|
|
11
11
|
<div class="error-msg">You are being redirected to a new location.</div>
|
|
12
|
-
<div class="error-path
|
|
12
|
+
<div class="error-path c-primary">{{ redirect_url }}</div>
|
|
13
13
|
<a href="{{ redirect_url }}" class="error-home">Click here if not redirected</a>
|
|
14
14
|
{% endblock %}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{% extends "errors/base.twig" %}
|
|
2
2
|
{% block title %}401 — Unauthorized{% endblock %}
|
|
3
3
|
{% block content %}
|
|
4
|
-
<div class="error-code
|
|
4
|
+
<div class="error-code c-danger">401</div>
|
|
5
5
|
<div class="error-title">Unauthorized</div>
|
|
6
6
|
<div class="error-msg">You need to sign in to access this resource.</div>
|
|
7
|
-
<div class="error-path
|
|
7
|
+
<div class="error-path c-danger">{{ path }}</div>
|
|
8
8
|
<a href="/" class="error-home">Go Home</a>
|
|
9
9
|
{% endblock %}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>403 — Forbidden</title>
|
|
7
|
-
<style>
|
|
7
|
+
<style nonce="{{ csp_nonce() }}">
|
|
8
8
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
9
|
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
10
10
|
.error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>404 - Not Found</title>
|
|
7
|
-
<style>
|
|
7
|
+
<style nonce="{{ csp_nonce() }}">
|
|
8
8
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
9
|
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
10
10
|
.error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>500 - Server Error</title>
|
|
7
|
-
<style>
|
|
7
|
+
<style nonce="{{ csp_nonce() }}">
|
|
8
8
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
9
|
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
10
10
|
.error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: left; max-width: 700px; width: 90%; }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{% extends "errors/base.twig" %}
|
|
2
2
|
{% block title %}502 — Bad Gateway{% endblock %}
|
|
3
3
|
{% block content %}
|
|
4
|
-
<div class="error-code
|
|
4
|
+
<div class="error-code c-danger">502</div>
|
|
5
5
|
<div class="error-title">Bad Gateway</div>
|
|
6
6
|
<div class="error-msg">The upstream server returned an invalid response.</div>
|
|
7
|
-
<div class="error-path
|
|
7
|
+
<div class="error-path c-danger">{{ path }}</div>
|
|
8
8
|
<a href="/" class="error-home">Go Home</a>
|
|
9
9
|
{% endblock %}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
.error-code { color: var(--warn); }
|
|
5
5
|
{% endblock %}
|
|
6
6
|
{% block content %}
|
|
7
|
-
<div class="error-code
|
|
7
|
+
<div class="error-code c-warn">503</div>
|
|
8
8
|
<div class="error-title">Service Unavailable</div>
|
|
9
9
|
<div class="error-msg">The server is temporarily unavailable. Please try again shortly.</div>
|
|
10
|
-
<div class="error-path
|
|
10
|
+
<div class="error-path c-warn">{{ path }}</div>
|
|
11
11
|
<a href="/" class="error-home">Go Home</a>
|
|
12
12
|
{% endblock %}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
{% block meta %}{% endblock %}
|
|
7
7
|
<title>{% block title %}Error{% endblock %}</title>
|
|
8
|
-
<style>
|
|
8
|
+
<style nonce="{{ csp_nonce() }}">
|
|
9
9
|
:root {
|
|
10
10
|
--bg: #0f172a; --surface: #1e293b; --border: #334155;
|
|
11
11
|
--text: #e2e8f0; --muted: #94a3b8; --primary: #3b82f6;
|
|
@@ -23,6 +23,9 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); min-
|
|
|
23
23
|
.error-home { display: inline-block; margin-top: 1.5rem; padding: 0.5rem 1.5rem; background: var(--primary); color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.85rem; }
|
|
24
24
|
.error-home:hover { opacity: 0.9; }
|
|
25
25
|
.logo { width: 3rem; height: 3rem; margin-bottom: 1rem; opacity: 0.5; }
|
|
26
|
+
.c-danger { color: var(--danger); }
|
|
27
|
+
.c-warn { color: var(--warn); }
|
|
28
|
+
.c-primary { color: var(--primary); }
|
|
26
29
|
{% block extra_styles %}{% endblock %}
|
|
27
30
|
</style>
|
|
28
31
|
</head>
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/webserver.rb
CHANGED
|
@@ -126,20 +126,9 @@ module Tina4
|
|
|
126
126
|
# drains in-flight requests (bounded by TINA4_SHUTDOWN_TIMEOUT).
|
|
127
127
|
Tina4::Shutdown.setup(server: @server)
|
|
128
128
|
|
|
129
|
-
# Test port (port + 1000) — stable, no-browser
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if is_debug && !no_ai_port
|
|
134
|
-
ai_port = @port + 1000
|
|
135
|
-
begin
|
|
136
|
-
# Tagged so the pipeline suppresses live reload on this port.
|
|
137
|
-
@server.listen(@host, ai_port, Tina4::AiPortRackApp.new(@app))
|
|
138
|
-
puts " Test Port: http://localhost:#{ai_port} (stable — no hot-reload)"
|
|
139
|
-
rescue Errno::EADDRINUSE
|
|
140
|
-
puts " Test Port: SKIPPED (port #{ai_port} in use)"
|
|
141
|
-
end
|
|
142
|
-
end
|
|
129
|
+
# Test port (port + 1000) — stable, no-browser. Extracted so its bind
|
|
130
|
+
# rescue stays out of #start's cyclomatic complexity.
|
|
131
|
+
bind_auxiliary_ai_port
|
|
143
132
|
|
|
144
133
|
@server.start
|
|
145
134
|
|
|
@@ -156,6 +145,33 @@ module Tina4
|
|
|
156
145
|
Tina4::PortTakeover.remove_pidfile(@port)
|
|
157
146
|
end
|
|
158
147
|
|
|
148
|
+
# Bind the auxiliary AI/test port (main port + 1000), a debug-only, stable,
|
|
149
|
+
# no-hot-reload sibling of the main port. It is a CONVENIENCE, never
|
|
150
|
+
# load-bearing: any bind failure here must DEGRADE to no-aux-port and leave
|
|
151
|
+
# the main server the operator asked for untouched. The rescue used to catch
|
|
152
|
+
# only Errno::EADDRINUSE, so any OTHER bind failure propagated out of #start
|
|
153
|
+
# and crashed the whole boot: a transient getaddrinfo/Socket::ResolutionError
|
|
154
|
+
# on a fresh host under process churn (a measured macOS artifact),
|
|
155
|
+
# Errno::EADDRNOTAVAIL, Errno::EACCES. Catch the broad bind-failure set
|
|
156
|
+
# (SocketError covers Socket::ResolutionError; SystemCallError covers every
|
|
157
|
+
# Errno::*), log LOUD so it is never silent, and carry on main-port only.
|
|
158
|
+
# Parity with Python (`except OSError`) and PHP (`\Throwable`).
|
|
159
|
+
def bind_auxiliary_ai_port
|
|
160
|
+
no_ai_port = %w[true 1 yes].include?(ENV.fetch("TINA4_NO_AI_PORT", "").downcase)
|
|
161
|
+
is_debug = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "").downcase)
|
|
162
|
+
return unless is_debug && !no_ai_port
|
|
163
|
+
|
|
164
|
+
ai_port = @port + 1000
|
|
165
|
+
# Tagged so the pipeline suppresses live reload on this port.
|
|
166
|
+
@server.listen(@host, ai_port, Tina4::AiPortRackApp.new(@app))
|
|
167
|
+
puts " Test Port: http://localhost:#{ai_port} (stable — no hot-reload)"
|
|
168
|
+
rescue Errno::EADDRINUSE
|
|
169
|
+
puts " Test Port: SKIPPED (port #{ai_port} in use)"
|
|
170
|
+
rescue SocketError, SystemCallError => e
|
|
171
|
+
Tina4::Log.error("Test Port: SKIPPED — auxiliary port #{ai_port} bind failed (#{e.class}: #{e.message})")
|
|
172
|
+
puts " Test Port: SKIPPED (#{e.class})"
|
|
173
|
+
end
|
|
174
|
+
|
|
159
175
|
# Dispatch a Rack-style env through the Tina4 app and return [status, headers, body].
|
|
160
176
|
#
|
|
161
177
|
# Useful for testing and embedding — does not require a running server.
|
data/lib/tina4/websocket.rb
CHANGED
|
@@ -691,6 +691,11 @@ module Tina4
|
|
|
691
691
|
|
|
692
692
|
Thread.new do
|
|
693
693
|
begin
|
|
694
|
+
# A large message (a pasted image in a chat, base64 inside JSON) arrives as a first frame with FIN clear,
|
|
695
|
+
# then CONTINUATION frames (RFC 6455 section 5.4); control frames may come in between. The first piece used
|
|
696
|
+
# to be emitted as the whole message and the rest ignored. Same design as the Python master (_handle_frame).
|
|
697
|
+
fragments = nil
|
|
698
|
+
fragment_opcode = nil
|
|
694
699
|
loop do
|
|
695
700
|
frame = connection.read_frame
|
|
696
701
|
break unless frame
|
|
@@ -698,8 +703,26 @@ module Tina4
|
|
|
698
703
|
connection.touch # mark activity for the idle reaper
|
|
699
704
|
|
|
700
705
|
case frame[:opcode]
|
|
706
|
+
when 0x0 # Continuation - a stray one (no message started) is dropped
|
|
707
|
+
next unless fragments
|
|
708
|
+
fragments << frame[:data]
|
|
709
|
+
if frame[:fin]
|
|
710
|
+
emit(:message, connection, fragments) if fragment_opcode == 0x1
|
|
711
|
+
fragments = nil
|
|
712
|
+
fragment_opcode = nil
|
|
713
|
+
end
|
|
701
714
|
when 0x1 # Text
|
|
702
|
-
|
|
715
|
+
if frame[:fin]
|
|
716
|
+
emit(:message, connection, frame[:data])
|
|
717
|
+
else
|
|
718
|
+
fragments = frame[:data].dup
|
|
719
|
+
fragment_opcode = 0x1
|
|
720
|
+
end
|
|
721
|
+
when 0x2 # Binary - not delivered (as before), but its pieces are one message, never text
|
|
722
|
+
unless frame[:fin]
|
|
723
|
+
fragments = frame[:data].dup
|
|
724
|
+
fragment_opcode = 0x2
|
|
725
|
+
end
|
|
703
726
|
when 0x8 # Close
|
|
704
727
|
break
|
|
705
728
|
when 0x9 # Ping
|
|
@@ -867,6 +890,7 @@ module Tina4
|
|
|
867
890
|
first_byte = @socket.getbyte
|
|
868
891
|
return nil unless first_byte
|
|
869
892
|
|
|
893
|
+
fin = (first_byte & 0x80) != 0
|
|
870
894
|
opcode = first_byte & 0x0F
|
|
871
895
|
second_byte = @socket.getbyte
|
|
872
896
|
return nil unless second_byte
|
|
@@ -887,7 +911,7 @@ module Tina4
|
|
|
887
911
|
data = data.bytes.each_with_index.map { |b, i| b ^ mask_key[i % 4] }.pack("C*")
|
|
888
912
|
end
|
|
889
913
|
|
|
890
|
-
{ opcode: opcode, data: data }
|
|
914
|
+
{ fin: fin, opcode: opcode, data: data }
|
|
891
915
|
rescue IOError, EOFError
|
|
892
916
|
nil
|
|
893
917
|
end
|
|
@@ -175,25 +175,28 @@ module Tina4
|
|
|
175
175
|
|
|
176
176
|
def subscribe(channel, &block)
|
|
177
177
|
@mutex.synchronize do
|
|
178
|
-
|
|
178
|
+
# NATS::Client#subscribe returns a NATS::Subscription. The client's own
|
|
179
|
+
# #unsubscribe is private in nats-pure 2.x, so we tear a subscription
|
|
180
|
+
# down through the subscription object, not the client (see #unsubscribe).
|
|
181
|
+
subscription = @nats.subscribe(channel) do |msg|
|
|
179
182
|
block.call(msg.data) if @running
|
|
180
183
|
end
|
|
181
|
-
@subs[channel] =
|
|
182
|
-
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
@subs[channel] = subscription
|
|
185
|
+
|
|
186
|
+
# The NATS client dispatches subscription callbacks on its own reader
|
|
187
|
+
# thread, so this keeper thread only has to stay alive until unsubscribe
|
|
188
|
+
# or close kills it. It blocks on an empty queue - nothing is ever
|
|
189
|
+
# pushed - so it waits at zero CPU. Before, it woke a hundred times a
|
|
190
|
+
# second on `sleep 0.01` just to re-check @running, burning energy to do
|
|
191
|
+
# nothing (Carbonah E004).
|
|
192
|
+
@threads[channel] ||= Thread.new { Thread::Queue.new.pop }
|
|
190
193
|
end
|
|
191
194
|
end
|
|
192
195
|
|
|
193
196
|
def unsubscribe(channel)
|
|
194
197
|
@mutex.synchronize do
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
subscription = @subs.delete(channel)
|
|
199
|
+
subscription&.unsubscribe
|
|
197
200
|
thread = @threads.delete(channel)
|
|
198
201
|
thread&.kill
|
|
199
202
|
end
|
|
@@ -202,7 +205,7 @@ module Tina4
|
|
|
202
205
|
def close
|
|
203
206
|
@running = false
|
|
204
207
|
@mutex.synchronize do
|
|
205
|
-
@subs.each_value { |
|
|
208
|
+
@subs.each_value { |subscription| subscription.unsubscribe rescue nil }
|
|
206
209
|
@subs.clear
|
|
207
210
|
@threads.each_value { |t| t.kill }
|
|
208
211
|
@threads.clear
|
data/lib/tina4.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "tina4/constants"
|
|
|
12
12
|
require_relative "tina4/log"
|
|
13
13
|
require_relative "tina4/debug" # backward compat alias
|
|
14
14
|
require_relative "tina4/env"
|
|
15
|
+
require_relative "tina4/csp" # per-response CSP nonce (ADR-0088) — before response
|
|
15
16
|
require_relative "tina4/sqlite_path_resolver"
|
|
16
17
|
require_relative "tina4/row_symbolizer"
|
|
17
18
|
require_relative "tina4/router"
|
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.143
|
|
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-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sqlite3
|
|
@@ -212,6 +212,7 @@ files:
|
|
|
212
212
|
- lib/tina4/context/chunker.rb
|
|
213
213
|
- lib/tina4/cors.rb
|
|
214
214
|
- lib/tina4/crud.rb
|
|
215
|
+
- lib/tina4/csp.rb
|
|
215
216
|
- lib/tina4/database.rb
|
|
216
217
|
- lib/tina4/database/sqlite3_adapter.rb
|
|
217
218
|
- lib/tina4/database_adapter.rb
|
|
@@ -311,6 +312,7 @@ files:
|
|
|
311
312
|
- lib/tina4/queue_backends/lite_backend.rb
|
|
312
313
|
- lib/tina4/queue_backends/mongo_backend.rb
|
|
313
314
|
- lib/tina4/queue_backends/rabbitmq_backend.rb
|
|
315
|
+
- lib/tina4/queue_backends/republish_retry_policy.rb
|
|
314
316
|
- lib/tina4/rack_app.rb
|
|
315
317
|
- lib/tina4/rate_limiter.rb
|
|
316
318
|
- lib/tina4/realtime.rb
|
|
@@ -340,6 +342,7 @@ files:
|
|
|
340
342
|
- lib/tina4/session_handlers/mongo_wire_client.rb
|
|
341
343
|
- lib/tina4/session_handlers/redis_handler.rb
|
|
342
344
|
- lib/tina4/session_handlers/resp_client.rb
|
|
345
|
+
- lib/tina4/session_handlers/resp_session_handler.rb
|
|
343
346
|
- lib/tina4/session_handlers/valkey_handler.rb
|
|
344
347
|
- lib/tina4/shutdown.rb
|
|
345
348
|
- lib/tina4/smtp_client.rb
|