tina4ruby 3.10.10 → 3.10.12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3001c964ea4a136f0dcc9af6766c2de0f98f323f8b9ff93deaab922477967c28
4
- data.tar.gz: 2abc2dfa785de1cb2a503902cf009501df413af82e9054fc6d044cb4d689525a
3
+ metadata.gz: 58885d2a44c72f086b6359780fe7f01f88870bf3491973d9f1068934bb8780e1
4
+ data.tar.gz: c567b4d7a4a027d460da4bd528204ffd7ba3ce4aa6e0b3e5d303c748d00f12fe
5
5
  SHA512:
6
- metadata.gz: d86f3ba17e99a972bdeee8d25a8afec34f12a89ab68ae9be21a5f1ed9bae6e3fa58b69083a8db25ee1ce17bfd1dc47790439f54196f848f6e95df0332d343eb1
7
- data.tar.gz: '08ded077a54c8ed7c044878ccce8ba7aa099c855be82a3dcc93568ee7ab7eb32f429ae2877872e97754ead753cc2cb4ccc8a990b94703e77787163e20b30e4db'
6
+ metadata.gz: 409514dda53214f3f02d36dacd58b845cd9c282e63e814590be34c13e4cac839e8c8f6c6dc571575c434d0736a055109f12e3b56f76087ceee2d4b046930b1e8
7
+ data.tar.gz: caaa5dc05a96d5061bc3e70af18371d5e3fb7e8c10bd8e03cc84ca47be1863527814d478ec5788654b417d13ee754cc84f575ccb3ec43a5c16826fff798a5a52
@@ -107,6 +107,16 @@ module Tina4
107
107
  if request_obj&.instance_variable_get(:@session)
108
108
  sess = request_obj.session
109
109
  sess.save
110
+
111
+ # Probabilistic garbage collection (~1% of requests)
112
+ if rand(1..100) == 1
113
+ begin
114
+ sess.gc
115
+ rescue StandardError
116
+ # GC failure is non-critical — silently ignore
117
+ end
118
+ end
119
+
110
120
  sid = sess.id
111
121
  cookie_val = (env["HTTP_COOKIE"] || "")[/tina4_session=([^;]+)/, 1]
112
122
  if sid && sid != cookie_val
@@ -56,6 +56,12 @@ module Tina4
56
56
  @db.execute("DELETE FROM #{TABLE_NAME} WHERE expires_at > 0 AND expires_at < ?", [Time.now.to_f])
57
57
  end
58
58
 
59
+ # Garbage-collect expired sessions. Matches the Python interface.
60
+ # @param max_age [Integer] maximum session age in seconds (unused — expiry is absolute)
61
+ def gc(max_age)
62
+ @db.execute("DELETE FROM #{TABLE_NAME} WHERE expires_at > 0 AND expires_at < ?", [Time.now.to_f])
63
+ end
64
+
59
65
  private
60
66
 
61
67
  def ensure_table
@@ -44,6 +44,18 @@ module Tina4
44
44
  end
45
45
  end
46
46
 
47
+ # Garbage-collect expired sessions. Matches the Python interface.
48
+ # @param max_age [Integer] maximum session age in seconds
49
+ def gc(max_age)
50
+ return unless Dir.exist?(@dir)
51
+ now = Time.now
52
+ Dir.glob(File.join(@dir, "sess_*")).each do |file|
53
+ File.delete(file) if File.mtime(file) + max_age < now
54
+ rescue StandardError
55
+ # Corrupt or locked file — skip
56
+ end
57
+ end
58
+
47
59
  private
48
60
 
49
61
  def session_path(session_id)
@@ -537,13 +537,19 @@ module Tina4
537
537
  end
538
538
  elsif part =~ /\A(\w+)\[(.+?)\]\z/
539
539
  base = Regexp.last_match(1)
540
- index = Regexp.last_match(2)
540
+ index = Regexp.last_match(2).strip
541
541
  value = access_value(value, base)
542
- if index =~ /\A\d+\z/
542
+ if index =~ /\A["'](.*)["']\z/
543
+ # Quoted string literal — use as-is
544
+ index = Regexp.last_match(1)
545
+ value = access_value(value, index)
546
+ elsif index =~ /\A\d+\z/
543
547
  value = value[index.to_i] if value.respond_to?(:[])
544
548
  else
545
- index = index.gsub(/["']/, "")
546
- value = access_value(value, index)
549
+ # Resolve as a variable from context
550
+ resolved = resolve_variable(index)
551
+ value = access_value(value, resolved.to_s) unless resolved.nil?
552
+ value = nil if resolved.nil?
547
553
  end
548
554
  else
549
555
  value = access_value(value, part)
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.10.10"
4
+ VERSION = "3.10.12"
5
5
  end
@@ -56,7 +56,7 @@ module Tina4
56
56
  when "redis"
57
57
  RedisBackplane.new(url: url)
58
58
  when "nats"
59
- raise NotImplementedError, "NATS backplane is on the roadmap but not yet implemented."
59
+ NATSBackplane.new(url: url)
60
60
  when ""
61
61
  nil
62
62
  else
@@ -115,4 +115,76 @@ module Tina4
115
115
  @redis.close
116
116
  end
117
117
  end
118
+
119
+ # NATS pub/sub backplane.
120
+ #
121
+ # Requires the +nats-pure+ gem (+gem install nats-pure+). The require is
122
+ # deferred so the rest of Tina4 works fine without it installed — an error
123
+ # is raised only when this class is actually instantiated.
124
+ #
125
+ # NATS is async-native, so we run a background thread with an event
126
+ # machine for the subscription listener.
127
+ class NATSBackplane < WebSocketBackplane
128
+ def initialize(url: nil)
129
+ begin
130
+ require "nats/client"
131
+ rescue LoadError
132
+ raise LoadError,
133
+ "The 'nats-pure' gem is required for NATSBackplane. " \
134
+ "Install it with: gem install nats-pure"
135
+ end
136
+
137
+ @url = url || ENV.fetch("TINA4_WS_BACKPLANE_URL", "nats://localhost:4222")
138
+ @subs = {}
139
+ @threads = {}
140
+ @running = true
141
+ @mutex = Mutex.new
142
+
143
+ # Connect to NATS in a background thread with its own event loop
144
+ @nats = NATS::IO::Client.new
145
+ @nats.connect(@url)
146
+ end
147
+
148
+ def publish(channel, message)
149
+ @nats.publish(channel, message)
150
+ @nats.flush
151
+ end
152
+
153
+ def subscribe(channel, &block)
154
+ @mutex.synchronize do
155
+ sid = @nats.subscribe(channel) do |msg|
156
+ block.call(msg.data) if @running
157
+ end
158
+ @subs[channel] = sid
159
+
160
+ # Run NATS event processing in a background thread
161
+ @threads[channel] ||= Thread.new do
162
+ loop do
163
+ break unless @running
164
+ sleep 0.01
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ def unsubscribe(channel)
171
+ @mutex.synchronize do
172
+ sid = @subs.delete(channel)
173
+ @nats.unsubscribe(sid) if sid
174
+ thread = @threads.delete(channel)
175
+ thread&.kill
176
+ end
177
+ end
178
+
179
+ def close
180
+ @running = false
181
+ @mutex.synchronize do
182
+ @subs.each_value { |sid| @nats.unsubscribe(sid) rescue nil }
183
+ @subs.clear
184
+ @threads.each_value { |t| t.kill }
185
+ @threads.clear
186
+ end
187
+ @nats.close
188
+ end
189
+ end
118
190
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.10.10
4
+ version: 3.10.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-03-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rack
@@ -400,7 +399,6 @@ licenses:
400
399
  - MIT
401
400
  metadata:
402
401
  homepage_uri: https://tina4.com
403
- post_install_message:
404
402
  rdoc_options: []
405
403
  require_paths:
406
404
  - lib
@@ -415,8 +413,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
415
413
  - !ruby/object:Gem::Version
416
414
  version: '0'
417
415
  requirements: []
418
- rubygems_version: 3.4.19
419
- signing_key:
416
+ rubygems_version: 4.0.3
420
417
  specification_version: 4
421
418
  summary: Simple. Fast. Human. This is not a framework.
422
419
  test_files: []