tina4ruby 0.5.2 → 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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +434 -544
  4. data/exe/{tina4 → tina4ruby} +1 -0
  5. data/lib/tina4/ai.rb +312 -0
  6. data/lib/tina4/auth.rb +44 -3
  7. data/lib/tina4/auto_crud.rb +163 -0
  8. data/lib/tina4/cli.rb +389 -97
  9. data/lib/tina4/constants.rb +46 -0
  10. data/lib/tina4/cors.rb +74 -0
  11. data/lib/tina4/database/sqlite3_adapter.rb +139 -0
  12. data/lib/tina4/database.rb +144 -7
  13. data/lib/tina4/debug.rb +4 -79
  14. data/lib/tina4/dev_admin.rb +1162 -0
  15. data/lib/tina4/dev_mailbox.rb +191 -0
  16. data/lib/tina4/dev_reload.rb +9 -9
  17. data/lib/tina4/drivers/firebird_driver.rb +19 -3
  18. data/lib/tina4/drivers/mssql_driver.rb +3 -3
  19. data/lib/tina4/drivers/mysql_driver.rb +4 -4
  20. data/lib/tina4/drivers/postgres_driver.rb +9 -2
  21. data/lib/tina4/drivers/sqlite_driver.rb +1 -1
  22. data/lib/tina4/env.rb +42 -2
  23. data/lib/tina4/error_overlay.rb +252 -0
  24. data/lib/tina4/events.rb +90 -0
  25. data/lib/tina4/field_types.rb +4 -0
  26. data/lib/tina4/frond.rb +1497 -0
  27. data/lib/tina4/gallery/auth/meta.json +1 -0
  28. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
  29. data/lib/tina4/gallery/database/meta.json +1 -0
  30. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
  31. data/lib/tina4/gallery/error-overlay/meta.json +1 -0
  32. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
  33. data/lib/tina4/gallery/orm/meta.json +1 -0
  34. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
  35. data/lib/tina4/gallery/queue/meta.json +1 -0
  36. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -0
  37. data/lib/tina4/gallery/rest-api/meta.json +1 -0
  38. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
  39. data/lib/tina4/gallery/templates/meta.json +1 -0
  40. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
  41. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
  42. data/lib/tina4/health.rb +39 -0
  43. data/lib/tina4/html_element.rb +148 -0
  44. data/lib/tina4/localization.rb +2 -2
  45. data/lib/tina4/log.rb +203 -0
  46. data/lib/tina4/messenger.rb +562 -0
  47. data/lib/tina4/migration.rb +132 -29
  48. data/lib/tina4/orm.rb +463 -35
  49. data/lib/tina4/public/css/tina4.css +178 -1
  50. data/lib/tina4/public/css/tina4.min.css +1 -2
  51. data/lib/tina4/public/favicon.ico +0 -0
  52. data/lib/tina4/public/images/logo.svg +5 -0
  53. data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
  54. data/lib/tina4/public/js/frond.min.js +420 -0
  55. data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
  56. data/lib/tina4/public/js/tina4.min.js +93 -0
  57. data/lib/tina4/public/swagger/index.html +90 -0
  58. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
  59. data/lib/tina4/queue.rb +162 -6
  60. data/lib/tina4/queue_backends/lite_backend.rb +88 -0
  61. data/lib/tina4/rack_app.rb +331 -27
  62. data/lib/tina4/rate_limiter.rb +123 -0
  63. data/lib/tina4/request.rb +61 -15
  64. data/lib/tina4/response.rb +54 -24
  65. data/lib/tina4/response_cache.rb +551 -0
  66. data/lib/tina4/router.rb +90 -15
  67. data/lib/tina4/scss_compiler.rb +2 -2
  68. data/lib/tina4/seeder.rb +56 -61
  69. data/lib/tina4/service_runner.rb +303 -0
  70. data/lib/tina4/session.rb +85 -0
  71. data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
  72. data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
  73. data/lib/tina4/shutdown.rb +84 -0
  74. data/lib/tina4/sql_translation.rb +295 -0
  75. data/lib/tina4/template.rb +36 -6
  76. data/lib/tina4/templates/base.twig +2 -2
  77. data/lib/tina4/templates/errors/302.twig +14 -0
  78. data/lib/tina4/templates/errors/401.twig +9 -0
  79. data/lib/tina4/templates/errors/403.twig +22 -15
  80. data/lib/tina4/templates/errors/404.twig +22 -15
  81. data/lib/tina4/templates/errors/500.twig +31 -15
  82. data/lib/tina4/templates/errors/502.twig +9 -0
  83. data/lib/tina4/templates/errors/503.twig +12 -0
  84. data/lib/tina4/templates/errors/base.twig +37 -0
  85. data/lib/tina4/version.rb +1 -1
  86. data/lib/tina4/webserver.rb +28 -18
  87. data/lib/tina4.rb +118 -21
  88. metadata +68 -8
  89. data/lib/tina4/public/js/tina4.js +0 -134
  90. data/lib/tina4/public/js/tina4helper.js +0 -387
@@ -0,0 +1,551 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ # Multi-backend response cache for GET requests.
5
+ #
6
+ # Backends are selected via the TINA4_CACHE_BACKEND env var:
7
+ # memory — in-process LRU cache (default, zero deps)
8
+ # redis — Redis / Valkey (uses `redis` gem or raw RESP over TCP)
9
+ # file — JSON files in data/cache/
10
+ #
11
+ # Environment:
12
+ # TINA4_CACHE_BACKEND — memory | redis | file (default: memory)
13
+ # TINA4_CACHE_URL — redis://localhost:6379 (redis only)
14
+ # TINA4_CACHE_TTL — default TTL in seconds (default: 0 = disabled)
15
+ # TINA4_CACHE_MAX_ENTRIES — maximum cache entries (default: 1000)
16
+ #
17
+ # Usage:
18
+ # cache = Tina4::ResponseCache.new(ttl: 60, max_entries: 1000)
19
+ # cache.cache_response("GET", "/api/users", 200, "application/json", '{"users":[]}')
20
+ # hit = cache.get("GET", "/api/users")
21
+ #
22
+ # # Direct API (same across all 4 languages)
23
+ # cache.cache_set("key", {"data" => "value"}, ttl: 120)
24
+ # value = cache.cache_get("key")
25
+ # cache.cache_delete("key")
26
+ #
27
+ class ResponseCache
28
+ CacheEntry = Struct.new(:body, :content_type, :status_code, :expires_at)
29
+
30
+ # @param ttl [Integer] default TTL in seconds (0 = disabled)
31
+ # @param max_entries [Integer] maximum cache entries
32
+ # @param status_codes [Array<Integer>] only cache these status codes
33
+ # @param backend [String, nil] cache backend: memory|redis|file
34
+ # @param cache_url [String, nil] Redis URL
35
+ # @param cache_dir [String, nil] File cache directory
36
+ def initialize(ttl: nil, max_entries: nil, status_codes: [200],
37
+ backend: nil, cache_url: nil, cache_dir: nil)
38
+ @ttl = ttl || (ENV["TINA4_CACHE_TTL"] ? ENV["TINA4_CACHE_TTL"].to_i : 0)
39
+ @max_entries = max_entries || (ENV["TINA4_CACHE_MAX_ENTRIES"] ? ENV["TINA4_CACHE_MAX_ENTRIES"].to_i : 1000)
40
+ @status_codes = status_codes
41
+ @backend_name = backend || ENV.fetch("TINA4_CACHE_BACKEND", "memory").downcase.strip
42
+ @cache_url = cache_url || ENV.fetch("TINA4_CACHE_URL", "redis://localhost:6379")
43
+ @cache_dir = cache_dir || ENV.fetch("TINA4_CACHE_DIR", "data/cache")
44
+ @store = {}
45
+ @mutex = Mutex.new
46
+ @hits = 0
47
+ @misses = 0
48
+
49
+ # Initialize backend
50
+ init_backend
51
+ end
52
+
53
+ # Check if caching is enabled.
54
+ #
55
+ # @return [Boolean]
56
+ def enabled?
57
+ @ttl > 0
58
+ end
59
+
60
+ # Build a cache key from method and URL.
61
+ #
62
+ # @param method [String]
63
+ # @param url [String]
64
+ # @return [String]
65
+ def cache_key(method, url)
66
+ "#{method}:#{url}"
67
+ end
68
+
69
+ # Retrieve a cached response. Returns nil on miss or expired entry.
70
+ #
71
+ # @param method [String]
72
+ # @param url [String]
73
+ # @return [CacheEntry, nil]
74
+ def get(method, url)
75
+ return nil unless enabled?
76
+ return nil unless method == "GET"
77
+
78
+ key = cache_key(method, url)
79
+ entry = backend_get(key)
80
+
81
+ if entry.nil?
82
+ @mutex.synchronize { @misses += 1 }
83
+ return nil
84
+ end
85
+
86
+ # For memory backend, entry is a CacheEntry; for others, reconstruct
87
+ if entry.is_a?(CacheEntry)
88
+ if Time.now.to_f > entry.expires_at
89
+ backend_delete(key)
90
+ @mutex.synchronize { @misses += 1 }
91
+ return nil
92
+ end
93
+ @mutex.synchronize { @hits += 1 }
94
+ entry
95
+ elsif entry.is_a?(Hash)
96
+ expires_at = entry["expires_at"] || entry[:expires_at] || 0
97
+ if Time.now.to_f > expires_at
98
+ backend_delete(key)
99
+ @mutex.synchronize { @misses += 1 }
100
+ return nil
101
+ end
102
+ @mutex.synchronize { @hits += 1 }
103
+ CacheEntry.new(
104
+ entry["body"] || entry[:body],
105
+ entry["content_type"] || entry[:content_type],
106
+ entry["status_code"] || entry[:status_code],
107
+ expires_at
108
+ )
109
+ end
110
+ end
111
+
112
+ # Store a response in the cache.
113
+ #
114
+ # @param method [String]
115
+ # @param url [String]
116
+ # @param status_code [Integer]
117
+ # @param content_type [String]
118
+ # @param body [String]
119
+ # @param ttl [Integer, nil] override default TTL
120
+ def cache_response(method, url, status_code, content_type, body, ttl: nil)
121
+ return unless enabled?
122
+ return unless method == "GET"
123
+ return unless @status_codes.include?(status_code)
124
+
125
+ effective_ttl = ttl || @ttl
126
+ key = cache_key(method, url)
127
+ expires_at = Time.now.to_f + effective_ttl
128
+
129
+ entry_data = {
130
+ "body" => body,
131
+ "content_type" => content_type,
132
+ "status_code" => status_code,
133
+ "expires_at" => expires_at
134
+ }
135
+
136
+ case @backend_name
137
+ when "memory"
138
+ @mutex.synchronize do
139
+ if @store.size >= @max_entries && !@store.key?(key)
140
+ oldest_key = @store.keys.first
141
+ @store.delete(oldest_key)
142
+ end
143
+ @store[key] = CacheEntry.new(body, content_type, status_code, expires_at)
144
+ end
145
+ else
146
+ backend_set(key, entry_data, effective_ttl)
147
+ end
148
+ end
149
+
150
+ # ── Direct Cache API (same across all 4 languages) ──────────
151
+
152
+ # Get a value from the cache by key.
153
+ #
154
+ # @param key [String]
155
+ # @return [Object, nil]
156
+ def cache_get(key)
157
+ full_key = "direct:#{key}"
158
+ raw = backend_get(full_key)
159
+
160
+ if raw.nil?
161
+ @mutex.synchronize { @misses += 1 }
162
+ return nil
163
+ end
164
+
165
+ if raw.is_a?(Hash)
166
+ expires_at = raw["expires_at"] || raw[:expires_at] || 0
167
+ if expires_at > 0 && Time.now.to_f > expires_at
168
+ backend_delete(full_key)
169
+ @mutex.synchronize { @misses += 1 }
170
+ return nil
171
+ end
172
+ @mutex.synchronize { @hits += 1 }
173
+ raw["value"] || raw[:value]
174
+ else
175
+ @mutex.synchronize { @hits += 1 }
176
+ raw
177
+ end
178
+ end
179
+
180
+ # Store a value in the cache with optional TTL.
181
+ #
182
+ # @param key [String]
183
+ # @param value [Object]
184
+ # @param ttl [Integer] TTL in seconds (0 uses default)
185
+ def cache_set(key, value, ttl: 0)
186
+ effective_ttl = ttl > 0 ? ttl : @ttl
187
+ effective_ttl = 60 if effective_ttl <= 0 # fallback for direct API
188
+ full_key = "direct:#{key}"
189
+ entry = {
190
+ "value" => value,
191
+ "expires_at" => Time.now.to_f + effective_ttl
192
+ }
193
+ backend_set(full_key, entry, effective_ttl)
194
+ end
195
+
196
+ # Delete a key from the cache.
197
+ #
198
+ # @param key [String]
199
+ # @return [Boolean]
200
+ def cache_delete(key)
201
+ backend_delete("direct:#{key}")
202
+ end
203
+
204
+ # Get cache statistics.
205
+ #
206
+ # @return [Hash] with :hits, :misses, :size, :backend, :keys
207
+ def cache_stats
208
+ @mutex.synchronize do
209
+ case @backend_name
210
+ when "memory"
211
+ now = Time.now.to_f
212
+ @store.reject! { |_k, v| v.is_a?(CacheEntry) && now > v.expires_at }
213
+ { hits: @hits, misses: @misses, size: @store.size, backend: @backend_name, keys: @store.keys.dup }
214
+ when "file"
215
+ sweep
216
+ files = Dir.glob(File.join(@cache_dir, "*.json"))
217
+ { hits: @hits, misses: @misses, size: files.size, backend: @backend_name, keys: [] }
218
+ when "redis"
219
+ size = 0
220
+ if @redis_client
221
+ begin
222
+ keys = @redis_client.keys("tina4:cache:*")
223
+ size = keys.size
224
+ rescue StandardError
225
+ end
226
+ end
227
+ { hits: @hits, misses: @misses, size: size, backend: @backend_name, keys: [] }
228
+ else
229
+ { hits: @hits, misses: @misses, size: @store.size, backend: @backend_name, keys: @store.keys.dup }
230
+ end
231
+ end
232
+ end
233
+
234
+ # Clear all cached responses.
235
+ def clear_cache
236
+ @mutex.synchronize do
237
+ @hits = 0
238
+ @misses = 0
239
+
240
+ case @backend_name
241
+ when "memory"
242
+ @store.clear
243
+ when "file"
244
+ Dir.glob(File.join(@cache_dir, "*.json")).each { |f| File.delete(f) rescue nil }
245
+ when "redis"
246
+ if @redis_client
247
+ begin
248
+ keys = @redis_client.keys("tina4:cache:*")
249
+ @redis_client.del(*keys) unless keys.empty?
250
+ rescue StandardError
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ # Remove expired entries.
258
+ #
259
+ # @return [Integer] number of entries removed
260
+ def sweep
261
+ case @backend_name
262
+ when "memory"
263
+ @mutex.synchronize do
264
+ now = Time.now.to_f
265
+ keys_to_remove = @store.select { |_k, v| v.is_a?(CacheEntry) && now > v.expires_at }.keys
266
+ keys_to_remove += @store.select { |_k, v| v.is_a?(Hash) && (v["expires_at"] || 0) > 0 && now > (v["expires_at"] || 0) }.keys
267
+ keys_to_remove.each { |k| @store.delete(k) }
268
+ keys_to_remove.size
269
+ end
270
+ when "file"
271
+ removed = 0
272
+ now = Time.now.to_f
273
+ Dir.glob(File.join(@cache_dir, "*.json")).each do |f|
274
+ begin
275
+ data = JSON.parse(File.read(f))
276
+ if data["expires_at"] && now > data["expires_at"]
277
+ File.delete(f)
278
+ removed += 1
279
+ end
280
+ rescue StandardError
281
+ end
282
+ end
283
+ removed
284
+ else
285
+ 0
286
+ end
287
+ end
288
+
289
+ # Current TTL setting.
290
+ attr_reader :ttl
291
+
292
+ # Maximum entries setting.
293
+ attr_reader :max_entries
294
+
295
+ # Active backend name.
296
+ def backend_name
297
+ @backend_name
298
+ end
299
+
300
+ private
301
+
302
+ # ── Backend initialization ─────────────────────────────────
303
+
304
+ def init_backend
305
+ case @backend_name
306
+ when "redis"
307
+ init_redis
308
+ when "file"
309
+ init_file_dir
310
+ end
311
+ end
312
+
313
+ def init_redis
314
+ @redis_client = nil
315
+ begin
316
+ require "redis"
317
+ parsed = parse_redis_url(@cache_url)
318
+ @redis_client = Redis.new(host: parsed[:host], port: parsed[:port], db: parsed[:db], timeout: 5)
319
+ @redis_client.ping
320
+ rescue LoadError, StandardError
321
+ @redis_client = nil
322
+ end
323
+ end
324
+
325
+ def parse_redis_url(url)
326
+ cleaned = url.sub(%r{^redis://}, "")
327
+ parts = cleaned.split(":")
328
+ host = parts[0].empty? ? "localhost" : parts[0]
329
+ port_and_db = parts[1] ? parts[1].split("/") : ["6379"]
330
+ port = port_and_db[0].to_i
331
+ port = 6379 if port == 0
332
+ db = port_and_db[1] ? port_and_db[1].to_i : 0
333
+ { host: host, port: port, db: db }
334
+ end
335
+
336
+ def init_file_dir
337
+ require "json"
338
+ require "fileutils"
339
+ FileUtils.mkdir_p(@cache_dir)
340
+ end
341
+
342
+ # ── Backend operations ─────────────────────────────────────
343
+
344
+ def backend_get(key)
345
+ case @backend_name
346
+ when "memory"
347
+ @mutex.synchronize { @store[key] }
348
+ when "redis"
349
+ redis_get(key)
350
+ when "file"
351
+ file_get(key)
352
+ else
353
+ @mutex.synchronize { @store[key] }
354
+ end
355
+ end
356
+
357
+ def backend_set(key, entry, ttl)
358
+ case @backend_name
359
+ when "memory"
360
+ @mutex.synchronize do
361
+ if @store.size >= @max_entries && !@store.key?(key)
362
+ oldest_key = @store.keys.first
363
+ @store.delete(oldest_key)
364
+ end
365
+ @store[key] = entry
366
+ end
367
+ when "redis"
368
+ redis_set(key, entry, ttl)
369
+ when "file"
370
+ file_set(key, entry)
371
+ end
372
+ end
373
+
374
+ def backend_delete(key)
375
+ case @backend_name
376
+ when "memory"
377
+ @mutex.synchronize do
378
+ !@store.delete(key).nil?
379
+ end
380
+ when "redis"
381
+ redis_delete(key)
382
+ when "file"
383
+ file_delete(key)
384
+ end
385
+ end
386
+
387
+ # ── Redis operations ───────────────────────────────────────
388
+
389
+ def redis_get(key)
390
+ full_key = "tina4:cache:#{key}"
391
+ if @redis_client
392
+ begin
393
+ raw = @redis_client.get(full_key)
394
+ return nil if raw.nil?
395
+ JSON.parse(raw)
396
+ rescue StandardError
397
+ nil
398
+ end
399
+ else
400
+ resp_get(full_key)
401
+ end
402
+ end
403
+
404
+ def redis_set(key, entry, ttl)
405
+ full_key = "tina4:cache:#{key}"
406
+ serialized = JSON.generate(entry)
407
+ if @redis_client
408
+ begin
409
+ if ttl > 0
410
+ @redis_client.setex(full_key, ttl, serialized)
411
+ else
412
+ @redis_client.set(full_key, serialized)
413
+ end
414
+ rescue StandardError
415
+ end
416
+ else
417
+ if ttl > 0
418
+ resp_command("SETEX", full_key, ttl.to_s, serialized)
419
+ else
420
+ resp_command("SET", full_key, serialized)
421
+ end
422
+ end
423
+ end
424
+
425
+ def redis_delete(key)
426
+ full_key = "tina4:cache:#{key}"
427
+ if @redis_client
428
+ begin
429
+ @redis_client.del(full_key) > 0
430
+ rescue StandardError
431
+ false
432
+ end
433
+ else
434
+ result = resp_command("DEL", full_key)
435
+ result == "1"
436
+ end
437
+ end
438
+
439
+ def resp_get(key)
440
+ result = resp_command("GET", key)
441
+ return nil if result.nil?
442
+ JSON.parse(result) rescue nil
443
+ end
444
+
445
+ def resp_command(*args)
446
+ parsed = parse_redis_url(@cache_url)
447
+ cmd = "*#{args.size}\r\n"
448
+ args.each { |arg| s = arg.to_s; cmd += "$#{s.bytesize}\r\n#{s}\r\n" }
449
+
450
+ sock = TCPSocket.new(parsed[:host], parsed[:port])
451
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [5, 0].pack("l_2"))
452
+ if parsed[:db] > 0
453
+ select_cmd = "*2\r\n$6\r\nSELECT\r\n$#{parsed[:db].to_s.bytesize}\r\n#{parsed[:db]}\r\n"
454
+ sock.write(select_cmd)
455
+ sock.recv(1024)
456
+ end
457
+ sock.write(cmd)
458
+ response = sock.recv(65536)
459
+ sock.close
460
+
461
+ if response.start_with?("+")
462
+ response[1..].strip
463
+ elsif response.start_with?("$-1")
464
+ nil
465
+ elsif response.start_with?("$")
466
+ lines = response.split("\r\n")
467
+ lines[1]
468
+ elsif response.start_with?(":")
469
+ response[1..].strip
470
+ else
471
+ nil
472
+ end
473
+ rescue StandardError
474
+ nil
475
+ end
476
+
477
+ # ── File operations ────────────────────────────────────────
478
+
479
+ def file_key_path(key)
480
+ require "digest"
481
+ safe = Digest::SHA256.hexdigest(key)
482
+ File.join(@cache_dir, "#{safe}.json")
483
+ end
484
+
485
+ def file_get(key)
486
+ path = file_key_path(key)
487
+ return nil unless File.exist?(path)
488
+ begin
489
+ data = JSON.parse(File.read(path))
490
+ if data["expires_at"] && Time.now.to_f > data["expires_at"]
491
+ File.delete(path) rescue nil
492
+ return nil
493
+ end
494
+ data
495
+ rescue StandardError
496
+ nil
497
+ end
498
+ end
499
+
500
+ def file_set(key, entry)
501
+ init_file_dir
502
+ files = Dir.glob(File.join(@cache_dir, "*.json")).sort_by { |f| File.mtime(f) }
503
+ while files.size >= @max_entries
504
+ File.delete(files.shift) rescue nil
505
+ end
506
+ path = file_key_path(key)
507
+ File.write(path, JSON.generate(entry))
508
+ rescue StandardError
509
+ end
510
+
511
+ def file_delete(key)
512
+ path = file_key_path(key)
513
+ if File.exist?(path)
514
+ File.delete(path) rescue nil
515
+ true
516
+ else
517
+ false
518
+ end
519
+ end
520
+ end
521
+
522
+ # ── Module-level convenience (singleton) ───────────────────────
523
+
524
+ @default_cache = nil
525
+
526
+ class << self
527
+ def cache_instance
528
+ @default_cache ||= ResponseCache.new(ttl: ENV["TINA4_CACHE_TTL"] ? ENV["TINA4_CACHE_TTL"].to_i : 60)
529
+ end
530
+
531
+ def cache_get(key)
532
+ cache_instance.cache_get(key)
533
+ end
534
+
535
+ def cache_set(key, value, ttl: 0)
536
+ cache_instance.cache_set(key, value, ttl: ttl)
537
+ end
538
+
539
+ def cache_delete(key)
540
+ cache_instance.cache_delete(key)
541
+ end
542
+
543
+ def cache_clear
544
+ cache_instance.clear_cache
545
+ end
546
+
547
+ def cache_stats
548
+ cache_instance.cache_stats
549
+ end
550
+ end
551
+ end