tina4ruby 3.0.0 → 3.2.1

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: 47c53ad8cda483d0cde2eaec4072b5620de88dbd172d45fe9ab0df52a865c71e
4
- data.tar.gz: 438bbf58dc14e8b7f91007cde01c53bc4326b4b088c8d39fac845189da9a2cf9
3
+ metadata.gz: c9e0a35ede3d5672d8d4b1c4197129db19443f60638e0596b1e94f367f9a40aa
4
+ data.tar.gz: 37d5d5d98b0511ac523eeec07d8b2ada21d5a47553bab176ccbce9a01a600389
5
5
  SHA512:
6
- metadata.gz: ae5246539beea74da1618b9e498d6f6f1ea3949e26663ec862d79934c3acdb6ec7ba59cb49778444badcab81aaa5a42de3e8e2c02ed4717d3ba368f0701045a7
7
- data.tar.gz: ddecbcd770270106b44733563e9c83f58e5b3084a0b190f3b5ea8041066f310cfec6017ee5e0fa03895c048a7a61100f9a8bca222a23825794726fdc7c1bebad
6
+ metadata.gz: 9f9f9b1240111c24ba20e461439c55c043792206f5b2150811f2c8d97b1e0e7e937d644777d4c2a06c59888b994f01aa8d54ebcffdad5dffcbdca159f3868fab
7
+ data.tar.gz: 581ac0b6d4b0493f0d787d53846d9c1811e9dab6515797ad693ecceec8e5b7401e92d08319b434c3729df0321b3399b8b73019e2b701d39664fa2f69c8baa20a
data/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  </p>
19
19
 
20
20
  <p align="center">
21
- <img src="https://img.shields.io/badge/tests-1334%20passing-brightgreen" alt="Tests">
21
+ <img src="https://img.shields.io/badge/tests-1577%20passing-brightgreen" alt="Tests">
22
22
  <img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
23
23
  <img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
24
24
  <img src="https://img.shields.io/badge/ruby-3.1%2B-blue" alt="Ruby 3.1+">
@@ -27,17 +27,53 @@
27
27
 
28
28
  ---
29
29
 
30
- ## Quickstart
30
+ ## Quick Start
31
31
 
32
32
  ```bash
33
- gem install tina4ruby
34
- tina4ruby init my-app
35
- cd my-app
36
- tina4ruby serve
37
- # -> http://localhost:7147
33
+ # Install the Tina4 CLI
34
+ cargo install tina4 # or download binary from https://github.com/tina4stack/tina4/releases
35
+
36
+ # Create a project
37
+ tina4 init ruby ./my-app
38
+
39
+ # Run it
40
+ cd my-app && tina4 serve
41
+ ```
42
+
43
+ Open http://localhost:7147 — your app is running.
44
+
45
+ <details>
46
+ <summary><strong>Without the Tina4 CLI</strong></summary>
47
+
48
+ ```bash
49
+ # 1. Create project
50
+ mkdir my-app && cd my-app
51
+ echo 'source "https://rubygems.org"' > Gemfile
52
+ echo 'gem "tina4-ruby", "~> 3.0"' >> Gemfile
53
+ bundle install
54
+
55
+ # 2. Create entry point
56
+ cat > app.rb << 'EOF'
57
+ require "tina4"
58
+ Tina4.initialize!(__dir__)
59
+ app = Tina4::RackApp.new
60
+ Tina4::WebServer.new(app, host: "0.0.0.0", port: 7147).start
61
+ EOF
62
+
63
+ # 3. Create .env
64
+ echo 'TINA4_DEBUG=true' > .env
65
+ echo 'TINA4_LOG_LEVEL=ALL' >> .env
66
+
67
+ # 4. Create route directory
68
+ mkdir -p src/routes
69
+
70
+ # 5. Run
71
+ bundle exec ruby app.rb
38
72
  ```
39
73
 
40
- That's it. Zero configuration, zero classes, zero boilerplate.
74
+ Open http://localhost:7147
75
+
76
+ </details>
41
77
 
42
78
  ---
43
79
 
@@ -50,7 +86,7 @@ Every feature is built from scratch -- no gem install, no node_modules, no third
50
86
  | **HTTP** | Rack 3 server, block routing, path params (`{id:int}`, `{p:path}`), middleware pipeline, CORS, rate limiting, graceful shutdown |
51
87
  | **Templates** | Frond engine (Twig-compatible), inheritance, partials, 35+ filters, macros, fragment caching, sandboxing |
52
88
  | **ORM** | Active Record, typed fields with validation, soft delete, relationships (`has_one`/`has_many`/`belongs_to`), scopes, result caching, multi-database |
53
- | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface |
89
+ | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
54
90
  | **Auth** | Zero-dep JWT (RS256), sessions (file, Redis, MongoDB), password hashing, form tokens |
55
91
  | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE |
56
92
  | **Background** | DB-backed queue with priority, delayed jobs, retry, batch processing, multi-queue |
@@ -58,9 +94,9 @@ Every feature is built from scratch -- no gem install, no node_modules, no third
58
94
  | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
59
95
  | **DX** | Dev admin dashboard, error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
60
96
  | **Data** | Migrations with rollback, 50+ fake data generators, ORM and table seeders |
61
- | **Other** | REST client, localization (6 languages), in-memory cache (TTL/tags/LRU), event system, inline testing, configurable error pages |
97
+ | **Other** | REST client, localization (6 languages), cache (memory/Redis/file), event system, inline testing, messenger (.env driven), configurable error pages |
62
98
 
63
- **676 tests across 28 modules. All Carbonah benchmarks rated A+.**
99
+ **1,577 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
64
100
 
65
101
  For full documentation visit **[tina4.com](https://tina4.com)**.
66
102
 
@@ -345,9 +381,9 @@ end
345
381
  ### JWT Authentication
346
382
 
347
383
  ```ruby
348
- auth = Tina4::Auth.new
349
- token = auth.get_token({ user_id: 42 })
350
- payload = auth.get_payload(token)
384
+ token = Tina4::Auth.create_token({ user_id: 42 })
385
+ result = Tina4::Auth.validate_token(token)
386
+ payload = Tina4::Auth.get_payload(token)
351
387
  ```
352
388
 
353
389
  POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `auth: false` to make public, `secure_get` to protect GET routes.
@@ -486,7 +522,7 @@ cache.tag("users").flush
486
522
 
487
523
  ## Dev Mode
488
524
 
489
- Set `TINA4_DEBUG_LEVEL=DEBUG` in `.env` to enable:
525
+ Set `TINA4_DEBUG=true` in `.env` to enable:
490
526
 
491
527
  - **Live reload** -- browser auto-refreshes on code changes
492
528
  - **CSS hot-reload** -- SCSS changes apply without page refresh
@@ -500,9 +536,14 @@ Set `TINA4_DEBUG_LEVEL=DEBUG` in `.env` to enable:
500
536
  ```bash
501
537
  tina4ruby init [dir] # Scaffold a new project
502
538
  tina4ruby serve [port] # Start dev server (default: 7147)
539
+ tina4ruby serve --production # Auto-install and use Puma production server
503
540
  tina4ruby migrate # Run pending migrations
504
541
  tina4ruby migrate --create <desc># Create a migration file
505
542
  tina4ruby migrate --rollback # Rollback last batch
543
+ tina4ruby generate model <name> # Generate ORM model scaffold
544
+ tina4ruby generate route <name> # Generate route scaffold
545
+ tina4ruby generate migration <d> # Generate migration file
546
+ tina4ruby generate middleware <n># Generate middleware scaffold
506
547
  tina4ruby seed # Run seeders from src/seeds/
507
548
  tina4ruby routes # List all registered routes
508
549
  tina4ruby test # Run test suite
@@ -510,6 +551,53 @@ tina4ruby build # Build distributable gem
510
551
  tina4ruby ai [--all] # Detect AI tools and install context
511
552
  ```
512
553
 
554
+ ### Production Server Auto-Detection
555
+
556
+ `tina4 serve` automatically detects and uses the best available production server:
557
+
558
+ - **Ruby**: Puma (if installed), otherwise WEBrick -- Puma gives 2.8x improvement
559
+ - Use `tina4ruby serve --production` to auto-install Puma
560
+
561
+ ### Scaffolding with `tina4 generate`
562
+
563
+ Quickly scaffold new components:
564
+
565
+ ```bash
566
+ tina4ruby generate model User # Creates src/orm/user.rb with field stubs
567
+ tina4ruby generate route users # Creates src/routes/users.rb with CRUD stubs
568
+ tina4ruby generate migration "add age" # Creates migration SQL file
569
+ tina4ruby generate middleware AuthLog # Creates middleware class
570
+ ```
571
+
572
+ ### ORM Relationships & Eager Loading
573
+
574
+ ```ruby
575
+ # Relationships
576
+ orders = user.has_many("Order", "user_id")
577
+ profile = user.has_one("Profile", "user_id")
578
+ customer = order.belongs_to("Customer", "customer_id")
579
+
580
+ # Eager loading with include:
581
+ users = User.all(include: ["orders", "profile"])
582
+ ```
583
+
584
+ ### DB Query Caching
585
+
586
+ Enable query caching for up to 4x speedup on read-heavy workloads:
587
+
588
+ ```bash
589
+ # .env
590
+ TINA4_DB_CACHE=true
591
+ ```
592
+
593
+ ### Frond Pre-Compilation
594
+
595
+ Templates are pre-compiled for 2.8x faster rendering.
596
+
597
+ ### Gallery
598
+
599
+ 7 interactive examples with **Try It** deploy.
600
+
513
601
  ## Environment
514
602
 
515
603
  ```bash
@@ -517,7 +605,8 @@ SECRET=your-jwt-secret
517
605
  DATABASE_URL=sqlite://data/app.db
518
606
  DATABASE_USERNAME=
519
607
  DATABASE_PASSWORD=
520
- TINA4_DEBUG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR, ALL
608
+ TINA4_DEBUG=true # Enable dev toolbar, error overlay
609
+ TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
521
610
  TINA4_LANGUAGE=en # en, fr, af, zh, ja, es
522
611
  TINA4_SESSION_HANDLER=SessionFileHandler
523
612
  SWAGGER_TITLE=My API
data/lib/tina4/cli.rb CHANGED
@@ -5,7 +5,7 @@ require "fileutils"
5
5
 
6
6
  module Tina4
7
7
  class CLI
8
- COMMANDS = %w[init start migrate seed seed:create test version routes console ai help].freeze
8
+ COMMANDS = %w[init start migrate seed seed:create test version routes console generate ai help].freeze
9
9
 
10
10
  def self.start(argv)
11
11
  new.run(argv)
@@ -23,6 +23,7 @@ module Tina4
23
23
  when "version" then cmd_version
24
24
  when "routes" then cmd_routes
25
25
  when "console" then cmd_console
26
+ when "generate" then cmd_generate(argv)
26
27
  when "ai" then cmd_ai(argv)
27
28
  when "help", "-h", "--help" then cmd_help
28
29
  else
@@ -97,37 +98,45 @@ module Tina4
97
98
 
98
99
  app = Tina4::RackApp.new(root_dir: root_dir)
99
100
 
101
+ is_debug = Tina4::Env.truthy?(ENV["TINA4_DEBUG"])
102
+
100
103
  # Try Puma first (production-grade), fall back to WEBrick
101
- begin
102
- require "puma"
103
- require "puma/configuration"
104
- require "puma/launcher"
105
-
106
- puma_host = options[:host]
107
- puma_port = options[:port]
108
-
109
- config = Puma::Configuration.new do |user_config|
110
- user_config.bind "tcp://#{puma_host}:#{puma_port}"
111
- user_config.app app
112
- user_config.threads 0, 16
113
- user_config.workers 0
114
- user_config.environment "development"
115
- user_config.log_requests false
116
- user_config.quiet
117
- end
104
+ # In debug mode, always use WEBrick for dev toolbar/reload support
105
+ if !is_debug
106
+ begin
107
+ require "puma"
108
+ require "puma/configuration"
109
+ require "puma/launcher"
110
+
111
+ puma_host = options[:host]
112
+ puma_port = options[:port]
113
+
114
+ config = Puma::Configuration.new do |user_config|
115
+ user_config.bind "tcp://#{puma_host}:#{puma_port}"
116
+ user_config.app app
117
+ user_config.threads 0, 16
118
+ user_config.workers 0
119
+ user_config.environment "production"
120
+ user_config.log_requests false
121
+ user_config.quiet
122
+ end
118
123
 
119
- Tina4::Log.info("Starting Puma server on http://#{puma_host}:#{puma_port}")
124
+ Tina4::Log.info("Production server: puma")
120
125
 
121
- # Setup graceful shutdown (Puma manages its own signals, but we handle DB cleanup)
122
- Tina4::Shutdown.setup
126
+ # Setup graceful shutdown (Puma manages its own signals, but we handle DB cleanup)
127
+ Tina4::Shutdown.setup
123
128
 
124
- launcher = Puma::Launcher.new(config)
125
- launcher.run
126
- rescue LoadError
127
- Tina4::Log.info("Puma not found, falling back to WEBrick")
128
- server = Tina4::WebServer.new(app, host: options[:host], port: options[:port])
129
- server.start
129
+ launcher = Puma::Launcher.new(config)
130
+ launcher.run
131
+ return
132
+ rescue LoadError
133
+ # Puma not installed, fall through to WEBrick
134
+ end
130
135
  end
136
+
137
+ Tina4::Log.info("Development server: WEBrick")
138
+ server = Tina4::WebServer.new(app, host: options[:host], port: options[:port])
139
+ server.start
131
140
  end
132
141
 
133
142
  # ── migrate ───────────────────────────────────────────────────────────
@@ -310,6 +319,123 @@ module Tina4
310
319
 
311
320
  # ── help ──────────────────────────────────────────────────────────────
312
321
 
322
+ # ── generate ────────────────────────────────────────────────────────
323
+
324
+ def cmd_generate(argv)
325
+ what = argv.shift
326
+ name = argv.shift
327
+ unless what && name
328
+ puts "Usage: tina4ruby generate <what> <name>"
329
+ puts " Generators: model, route, migration, middleware"
330
+ exit 1
331
+ end
332
+
333
+ case what
334
+ when "model" then generate_model(name)
335
+ when "route" then generate_route(name)
336
+ when "migration" then generate_migration(name)
337
+ when "middleware" then generate_middleware(name)
338
+ else
339
+ puts "Unknown generator: #{what}"
340
+ puts " Available: model, route, migration, middleware"
341
+ exit 1
342
+ end
343
+ end
344
+
345
+ def generate_model(name)
346
+ dir = "src/orm"
347
+ FileUtils.mkdir_p(dir)
348
+ snake = name.gsub(/([A-Z])/) { |m| ($~.begin(0) > 0 ? "_" : "") + m.downcase }
349
+ path = File.join(dir, "#{snake}.rb")
350
+ abort " File already exists: #{path}" if File.exist?(path)
351
+ File.write(path, <<~RUBY)
352
+ class #{name} < Tina4::ORM
353
+ integer_field :id, primary_key: true, auto_increment: true
354
+ string_field :name
355
+ string_field :email
356
+ end
357
+ RUBY
358
+ puts " Created #{path}"
359
+ end
360
+
361
+ def generate_route(name)
362
+ route_path = name.sub(%r{^/}, "")
363
+ dir = "src/routes/#{route_path}"
364
+ FileUtils.mkdir_p(dir)
365
+ path = dir.chomp("/") + ".rb"
366
+ abort " File already exists: #{path}" if File.exist?(path)
367
+ File.write(path, <<~RUBY)
368
+ Tina4.get "/#{route_path}" do |request, response|
369
+ response.json(data: [])
370
+ end
371
+
372
+ Tina4.get "/#{route_path}/:id" do |request, response|
373
+ response.json(data: {})
374
+ end
375
+
376
+ Tina4.post "/#{route_path}" do |request, response|
377
+ response.json({ message: "created" }, 201)
378
+ end
379
+
380
+ Tina4.put "/#{route_path}/:id" do |request, response|
381
+ response.json(message: "updated")
382
+ end
383
+
384
+ Tina4.delete "/#{route_path}/:id" do |request, response|
385
+ response.json(message: "deleted")
386
+ end
387
+ RUBY
388
+ puts " Created #{path}"
389
+ end
390
+
391
+ def generate_migration(name)
392
+ dir = "migrations"
393
+ FileUtils.mkdir_p(dir)
394
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
395
+ table = name.sub(/^create_/, "")
396
+ table = if table.end_with?("s")
397
+ table
398
+ elsif table.end_with?("y")
399
+ table[0..-2] + "ies"
400
+ else
401
+ table + "s"
402
+ end
403
+ filename = "#{timestamp}_#{name}.sql"
404
+ path = File.join(dir, filename)
405
+ now = Time.now.strftime("%Y-%m-%d %H:%M:%S")
406
+ File.write(path, <<~SQL)
407
+ -- Migration: #{name}
408
+ -- Created: #{now}
409
+
410
+ CREATE TABLE #{table} (
411
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
412
+ name TEXT NOT NULL,
413
+ email TEXT NOT NULL,
414
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
415
+ );
416
+ SQL
417
+ puts " Created #{path}"
418
+ end
419
+
420
+ def generate_middleware(name)
421
+ dir = "src/middleware"
422
+ FileUtils.mkdir_p(dir)
423
+ snake = name.gsub(/([A-Z])/) { |m| ($~.begin(0) > 0 ? "_" : "") + m.downcase }
424
+ path = File.join(dir, "#{snake}.rb")
425
+ abort " File already exists: #{path}" if File.exist?(path)
426
+ File.write(path, <<~RUBY)
427
+ class #{name} < Tina4::Middleware
428
+ def process(request, response)
429
+ auth = request.headers["Authorization"]
430
+ return response.json({ error: "Unauthorized" }, 401) unless auth
431
+
432
+ nil
433
+ end
434
+ end
435
+ RUBY
436
+ puts " Created #{path}"
437
+ end
438
+
313
439
  def cmd_help
314
440
  puts <<~HELP
315
441
  Tina4 Ruby CLI
@@ -326,6 +452,7 @@ module Tina4
326
452
  version Show Tina4 version
327
453
  routes List all registered routes
328
454
  console Start an interactive console
455
+ generate <what> <name> Generate scaffolding (model, route, migration, middleware)
329
456
  ai Detect AI tools and install context files
330
457
  help Show this help message
331
458
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
3
  require "uri"
4
+ require "digest"
4
5
 
5
6
  module Tina4
6
7
  class Database
@@ -24,6 +25,15 @@ module Tina4
24
25
  @driver_name = driver_name || detect_driver(@connection_string)
25
26
  @driver = create_driver
26
27
  @connected = false
28
+
29
+ # Query cache — off by default, opt-in via TINA4_DB_CACHE=true
30
+ @cache_enabled = truthy?(ENV["TINA4_DB_CACHE"])
31
+ @cache_ttl = (ENV["TINA4_DB_CACHE_TTL"] || "30").to_i
32
+ @query_cache = {} # key => { expires_at:, value: }
33
+ @cache_hits = 0
34
+ @cache_misses = 0
35
+ @cache_mutex = Mutex.new
36
+
27
37
  connect
28
38
  end
29
39
 
@@ -41,21 +51,74 @@ module Tina4
41
51
  @connected = false
42
52
  end
43
53
 
54
+ # ── Query Cache ──────────────────────────────────────────────
55
+
56
+ def cache_stats
57
+ @cache_mutex.synchronize do
58
+ {
59
+ enabled: @cache_enabled,
60
+ hits: @cache_hits,
61
+ misses: @cache_misses,
62
+ size: @query_cache.size,
63
+ ttl: @cache_ttl
64
+ }
65
+ end
66
+ end
67
+
68
+ def cache_clear
69
+ @cache_mutex.synchronize do
70
+ @query_cache.clear
71
+ @cache_hits = 0
72
+ @cache_misses = 0
73
+ end
74
+ end
75
+
44
76
  def fetch(sql, params = [], limit: nil, skip: nil)
45
77
  effective_sql = sql
46
78
  if limit
47
79
  effective_sql = @driver.apply_limit(effective_sql, limit, skip || 0)
48
80
  end
81
+
82
+ if @cache_enabled
83
+ key = cache_key(effective_sql, params)
84
+ cached = cache_get(key)
85
+ if cached
86
+ @cache_mutex.synchronize { @cache_hits += 1 }
87
+ return cached
88
+ end
89
+ result = @driver.execute_query(effective_sql, params)
90
+ result = Tina4::DatabaseResult.new(result, sql: effective_sql)
91
+ cache_set(key, result)
92
+ @cache_mutex.synchronize { @cache_misses += 1 }
93
+ return result
94
+ end
95
+
49
96
  rows = @driver.execute_query(effective_sql, params)
50
97
  Tina4::DatabaseResult.new(rows, sql: effective_sql)
51
98
  end
52
99
 
53
100
  def fetch_one(sql, params = [])
101
+ if @cache_enabled
102
+ key = cache_key(sql + ":ONE", params)
103
+ cached = cache_get(key)
104
+ if cached
105
+ @cache_mutex.synchronize { @cache_hits += 1 }
106
+ return cached
107
+ end
108
+ result = fetch(sql, params, limit: 1)
109
+ value = result.first
110
+ cache_set(key, value)
111
+ @cache_mutex.synchronize { @cache_misses += 1 }
112
+ return value
113
+ end
114
+
54
115
  result = fetch(sql, params, limit: 1)
55
116
  result.first
56
117
  end
57
118
 
58
119
  def insert(table, data)
120
+ cache_invalidate if @cache_enabled
121
+
59
122
  # List of hashes — batch insert
60
123
  if data.is_a?(Array)
61
124
  return { success: true, affected_rows: 0 } if data.empty?
@@ -74,6 +137,8 @@ module Tina4
74
137
  end
75
138
 
76
139
  def update(table, data, filter = {})
140
+ cache_invalidate if @cache_enabled
141
+
77
142
  set_parts = data.keys.map { |k| "#{k} = #{@driver.placeholder}" }
78
143
  where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
79
144
  sql = "UPDATE #{table} SET #{set_parts.join(', ')}"
@@ -84,6 +149,8 @@ module Tina4
84
149
  end
85
150
 
86
151
  def delete(table, filter = {})
152
+ cache_invalidate if @cache_enabled
153
+
87
154
  # List of hashes — delete each row
88
155
  if filter.is_a?(Array)
89
156
  filter.each { |row| delete(table, row) }
@@ -107,6 +174,7 @@ module Tina4
107
174
  end
108
175
 
109
176
  def execute(sql, params = [])
177
+ cache_invalidate if @cache_enabled
110
178
  @driver.execute(sql, params)
111
179
  end
112
180
 
@@ -142,6 +210,39 @@ module Tina4
142
210
 
143
211
  private
144
212
 
213
+ def truthy?(val)
214
+ %w[true 1 yes on].include?((val || "").to_s.strip.downcase)
215
+ end
216
+
217
+ def cache_key(sql, params)
218
+ Digest::SHA256.hexdigest(sql + params.to_s)
219
+ end
220
+
221
+ def cache_get(key)
222
+ @cache_mutex.synchronize do
223
+ entry = @query_cache[key]
224
+ return nil unless entry
225
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > entry[:expires_at]
226
+ @query_cache.delete(key)
227
+ return nil
228
+ end
229
+ entry[:value]
230
+ end
231
+ end
232
+
233
+ def cache_set(key, value)
234
+ @cache_mutex.synchronize do
235
+ @query_cache[key] = {
236
+ expires_at: Process.clock_gettime(Process::CLOCK_MONOTONIC) + @cache_ttl,
237
+ value: value
238
+ }
239
+ end
240
+ end
241
+
242
+ def cache_invalidate
243
+ @cache_mutex.synchronize { @query_cache.clear }
244
+ end
245
+
145
246
  def detect_driver(conn)
146
247
  case conn.to_s.downcase
147
248
  when /\.db$/, /\.sqlite/, /sqlite/
@@ -34,7 +34,7 @@ module Tina4
34
34
  attachments: store_attachments(msg_id, attachments),
35
35
  read: false,
36
36
  folder: "outbox",
37
- created_at: timestamp.iso8601,
37
+ created_at: timestamp.strftime("%Y-%m-%dT%H:%M:%S.%6N%:z"),
38
38
  updated_at: timestamp.iso8601
39
39
  }
40
40