tina4ruby 3.10.11 → 3.10.13

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: 4966bc1955a5ee02eba12ebc9d43a72ba8f1572a2953dc285857f1f6d0f42a02
4
- data.tar.gz: eabb1a3be1213f34353a0adf4bca93fba38f7bb7e15574ee76888e02d163f29a
3
+ metadata.gz: c6d46cd66e041da43c47eef3869b8fd66455270fe1937d4c86ee839836dd6419
4
+ data.tar.gz: bf4853aa8715cc865c25fa2693b10d50168d1fd48bc9a9371e8368a963e142bc
5
5
  SHA512:
6
- metadata.gz: de40973a6d1f36c8dc4b18a6a360e9647d59aa5b94803890d32a9f39bd0bd9350d208683337082f49a4df6dbde066eacdd63aaa4bb8a070dc68feddf9d0a5612
7
- data.tar.gz: 80bbc66c2c78d07177bcee66f21fd9fb0f5ef4b4a7148284dd65b6cb4c5344f2132c9bbe4a1dae2839ab0803865f78660dad318099354625454b26b4c4cf61b6
6
+ metadata.gz: 21b443f74d40f46d610be7d039be8158b6a3a8be044476730ece168baaa2f8bb9b65a1b6e82f8a8c2d1cf50b9aafa614d99c3229b0c75b64b696973853cd1a21
7
+ data.tar.gz: 93a487f54bcbf48995dc61915326b8826cac1a7a4250275ab0ec25fd3a47af55b39da60186dbeac86eb87db2c2d2b6b5f2a92f73f6ce46bf44db9c724cec8fb8
data/lib/tina4/orm.rb CHANGED
@@ -315,6 +315,7 @@ module Tina4
315
315
 
316
316
  sql = "CREATE TABLE IF NOT EXISTS #{table_name} (#{col_defs.join(', ')})"
317
317
  db.execute(sql)
318
+ db.commit
318
319
  true
319
320
  end
320
321
 
@@ -401,6 +402,7 @@ module Tina4
401
402
  end
402
403
  @persisted = true
403
404
  end
405
+ self.class.db.commit
404
406
  true
405
407
  rescue => e
406
408
  @errors << e.message
@@ -422,6 +424,7 @@ module Tina4
422
424
  else
423
425
  self.class.db.delete(self.class.table_name, { pk => pk_value })
424
426
  end
427
+ self.class.db.commit
425
428
  @persisted = false
426
429
  true
427
430
  end
@@ -432,6 +435,7 @@ module Tina4
432
435
  raise "Cannot delete: no primary key value" unless pk_value
433
436
 
434
437
  self.class.db.delete(self.class.table_name, { pk => pk_value })
438
+ self.class.db.commit
435
439
  @persisted = false
436
440
  true
437
441
  end
@@ -448,6 +452,7 @@ module Tina4
448
452
  { self.class.soft_delete_field => 0 },
449
453
  { pk => pk_value }
450
454
  )
455
+ self.class.db.commit
451
456
  __send__("#{self.class.soft_delete_field}=", 0) if respond_to?("#{self.class.soft_delete_field}=")
452
457
  true
453
458
  end
@@ -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)
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.11"
4
+ VERSION = "3.10.13"
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,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.10.11
4
+ version: 3.10.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-03-28 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rack
@@ -399,6 +400,7 @@ licenses:
399
400
  - MIT
400
401
  metadata:
401
402
  homepage_uri: https://tina4.com
403
+ post_install_message:
402
404
  rdoc_options: []
403
405
  require_paths:
404
406
  - lib
@@ -413,7 +415,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
413
415
  - !ruby/object:Gem::Version
414
416
  version: '0'
415
417
  requirements: []
416
- rubygems_version: 4.0.3
418
+ rubygems_version: 3.4.19
419
+ signing_key:
417
420
  specification_version: 4
418
421
  summary: Simple. Fast. Human. This is not a framework.
419
422
  test_files: []