tina4ruby 3.10.90 → 3.10.91
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/lib/tina4/api.rb +50 -13
- data/lib/tina4/auth.rb +11 -3
- data/lib/tina4/auto_crud.rb +5 -5
- data/lib/tina4/cache.rb +154 -0
- data/lib/tina4/cli.rb +1 -1
- data/lib/tina4/container.rb +6 -6
- data/lib/tina4/crud.rb +3 -3
- data/lib/tina4/database.rb +90 -4
- data/lib/tina4/drivers/sqlite_driver.rb +4 -0
- data/lib/tina4/frond.rb +8 -0
- data/lib/tina4/graphql.rb +27 -24
- data/lib/tina4/health.rb +1 -1
- data/lib/tina4/job.rb +8 -4
- data/lib/tina4/localization.rb +21 -0
- data/lib/tina4/middleware.rb +18 -6
- data/lib/tina4/migration.rb +76 -25
- data/lib/tina4/orm.rb +23 -4
- data/lib/tina4/queue.rb +96 -21
- data/lib/tina4/queue_backends/lite_backend.rb +42 -1
- data/lib/tina4/rack_app.rb +3 -3
- data/lib/tina4/router.rb +34 -15
- data/lib/tina4/seeder.rb +33 -1
- data/lib/tina4/session.rb +59 -5
- data/lib/tina4/sql_translation.rb +1 -138
- data/lib/tina4/test_client.rb +1 -1
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +65 -39
- data/lib/tina4.rb +15 -14
- metadata +3 -2
|
@@ -63,6 +63,45 @@ module Tina4
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
def dequeue_batch(topic, count)
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
dir = topic_path(topic)
|
|
69
|
+
return [] unless Dir.exist?(dir)
|
|
70
|
+
|
|
71
|
+
now = Time.now
|
|
72
|
+
candidates = []
|
|
73
|
+
|
|
74
|
+
Dir.glob(File.join(dir, "*.json")).each do |f|
|
|
75
|
+
data = JSON.parse(File.read(f))
|
|
76
|
+
if data["available_at"]
|
|
77
|
+
available_at = Time.parse(data["available_at"])
|
|
78
|
+
next if available_at > now
|
|
79
|
+
end
|
|
80
|
+
candidates << { file: f, data: data, priority: data["priority"] || 0, mtime: File.mtime(f) }
|
|
81
|
+
rescue JSON::ParserError
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return [] if candidates.empty?
|
|
86
|
+
|
|
87
|
+
candidates.sort_by! { |c| [-c[:priority], c[:mtime]] }
|
|
88
|
+
chosen = candidates.first(count)
|
|
89
|
+
|
|
90
|
+
chosen.map do |c|
|
|
91
|
+
File.delete(c[:file])
|
|
92
|
+
data = c[:data]
|
|
93
|
+
Tina4::Job.new(
|
|
94
|
+
topic: data["topic"] || topic.to_s,
|
|
95
|
+
payload: data["payload"],
|
|
96
|
+
id: data["id"],
|
|
97
|
+
priority: data["priority"] || 0,
|
|
98
|
+
available_at: data["available_at"] ? Time.parse(data["available_at"]) : nil,
|
|
99
|
+
attempts: data["attempts"] || 0
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
66
105
|
def acknowledge(message)
|
|
67
106
|
# File already deleted on dequeue
|
|
68
107
|
end
|
|
@@ -219,7 +258,7 @@ module Tina4
|
|
|
219
258
|
end
|
|
220
259
|
|
|
221
260
|
# Retry all dead letter jobs for this topic. Returns true if any were re-queued.
|
|
222
|
-
def retry_job(topic, delay_seconds: 0)
|
|
261
|
+
def retry_job(topic, job_id: nil, delay_seconds: 0)
|
|
223
262
|
return false unless Dir.exist?(@dead_letter_dir)
|
|
224
263
|
|
|
225
264
|
available_at = delay_seconds > 0 ? Time.now + delay_seconds : nil
|
|
@@ -228,6 +267,7 @@ module Tina4
|
|
|
228
267
|
Dir.glob(File.join(@dead_letter_dir, "*.json")).each do |file|
|
|
229
268
|
data = JSON.parse(File.read(file))
|
|
230
269
|
next unless data["topic"] == topic.to_s
|
|
270
|
+
next if job_id && data["id"] != job_id.to_s
|
|
231
271
|
|
|
232
272
|
msg = Tina4::Job.new(
|
|
233
273
|
topic: data["topic"],
|
|
@@ -239,6 +279,7 @@ module Tina4
|
|
|
239
279
|
enqueue(msg)
|
|
240
280
|
File.delete(file)
|
|
241
281
|
count += 1
|
|
282
|
+
break if job_id # found the specific job, stop scanning
|
|
242
283
|
rescue JSON::ParserError
|
|
243
284
|
next
|
|
244
285
|
end
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -80,7 +80,7 @@ module Tina4
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
# Route matching
|
|
83
|
-
result = Tina4::Router.
|
|
83
|
+
result = Tina4::Router.match(method, path)
|
|
84
84
|
if result
|
|
85
85
|
route, path_params = result
|
|
86
86
|
rack_response = handle_route(env, route, path_params)
|
|
@@ -191,7 +191,7 @@ module Tina4
|
|
|
191
191
|
response = Tina4::Response.new
|
|
192
192
|
|
|
193
193
|
# Run global middleware (block-based + class-based before_* methods)
|
|
194
|
-
unless Tina4::Middleware.run_before(request, response)
|
|
194
|
+
unless Tina4::Middleware.run_before(Tina4::Middleware.global_middleware, request, response)
|
|
195
195
|
# Middleware halted the request -- return whatever response was set
|
|
196
196
|
return response.to_rack
|
|
197
197
|
end
|
|
@@ -229,7 +229,7 @@ module Tina4
|
|
|
229
229
|
final_response = result.equal?(response) ? result : Tina4::Response.auto_detect(result, response)
|
|
230
230
|
|
|
231
231
|
# Run global after middleware (block-based + class-based after_* methods)
|
|
232
|
-
Tina4::Middleware.run_after(request, final_response)
|
|
232
|
+
Tina4::Middleware.run_after(Tina4::Middleware.global_middleware, request, final_response)
|
|
233
233
|
|
|
234
234
|
final_response.to_rack
|
|
235
235
|
end
|
data/lib/tina4/router.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Tina4
|
|
4
4
|
class Route
|
|
5
5
|
attr_reader :method, :path, :handler, :auth_handler, :swagger_meta,
|
|
6
|
-
:path_regex, :param_names, :
|
|
6
|
+
:path_regex, :param_names, :template
|
|
7
7
|
attr_accessor :auth_required, :cached
|
|
8
8
|
|
|
9
9
|
def initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
@@ -44,6 +44,19 @@ module Tina4
|
|
|
44
44
|
self
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Dual-mode: getter (no args) returns the middleware array;
|
|
48
|
+
# setter (with args) appends middleware and returns self for chaining.
|
|
49
|
+
# Router.post("/api") { ... }.middleware(AuthMiddleware)
|
|
50
|
+
def middleware(*middleware_classes)
|
|
51
|
+
return @middleware if middleware_classes.empty?
|
|
52
|
+
|
|
53
|
+
@middleware = @middleware.dup + middleware_classes
|
|
54
|
+
# Custom middleware means developer handles auth — disable built-in gate
|
|
55
|
+
# unless .secure was explicitly called.
|
|
56
|
+
@auth_required = false unless @auth_required
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
47
60
|
# Returns params hash if matched, false otherwise
|
|
48
61
|
def match?(request_path, request_method = nil)
|
|
49
62
|
return false if request_method && @method != "ANY" && @method != request_method.to_s.upcase
|
|
@@ -209,6 +222,11 @@ module Tina4
|
|
|
209
222
|
@ws_routes ||= []
|
|
210
223
|
end
|
|
211
224
|
|
|
225
|
+
# Parity alias — returns all registered WebSocket routes.
|
|
226
|
+
def get_web_socket_routes
|
|
227
|
+
ws_routes
|
|
228
|
+
end
|
|
229
|
+
|
|
212
230
|
# Register a WebSocket route.
|
|
213
231
|
# The handler block receives (connection, event, data) where:
|
|
214
232
|
# connection — WebSocketConnection with #send, #broadcast, #close, #params
|
|
@@ -240,7 +258,7 @@ module Tina4
|
|
|
240
258
|
@method_index ||= Hash.new { |h, k| h[k] = [] }
|
|
241
259
|
end
|
|
242
260
|
|
|
243
|
-
def
|
|
261
|
+
def add(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
244
262
|
route = Route.new(method, path, handler,
|
|
245
263
|
auth_handler: auth_handler,
|
|
246
264
|
swagger_meta: swagger_meta,
|
|
@@ -251,33 +269,32 @@ module Tina4
|
|
|
251
269
|
Tina4::Log.debug("Route registered: #{method.upcase} #{path}")
|
|
252
270
|
route
|
|
253
271
|
end
|
|
254
|
-
|
|
255
|
-
# Convenience registration methods matching tina4-python pattern
|
|
272
|
+
# Convenience registration methods
|
|
256
273
|
def get(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
257
|
-
|
|
274
|
+
add("GET", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
258
275
|
end
|
|
259
276
|
|
|
260
277
|
def post(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
261
|
-
|
|
278
|
+
add("POST", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
262
279
|
end
|
|
263
280
|
|
|
264
281
|
def put(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
265
|
-
|
|
282
|
+
add("PUT", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
266
283
|
end
|
|
267
284
|
|
|
268
285
|
def patch(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
269
|
-
|
|
286
|
+
add("PATCH", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
270
287
|
end
|
|
271
288
|
|
|
272
289
|
def delete(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
273
|
-
|
|
290
|
+
add("DELETE", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
274
291
|
end
|
|
275
292
|
|
|
276
293
|
def any(path, middleware: [], swagger_meta: {}, template: nil, &block)
|
|
277
|
-
|
|
294
|
+
add("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
|
|
278
295
|
end
|
|
279
296
|
|
|
280
|
-
def find_route(
|
|
297
|
+
def find_route(method, path)
|
|
281
298
|
normalized_method = method.upcase
|
|
282
299
|
# Normalize path once (not per-route)
|
|
283
300
|
normalized_path = path.gsub("\\", "/")
|
|
@@ -293,9 +310,10 @@ module Tina4
|
|
|
293
310
|
nil
|
|
294
311
|
end
|
|
295
312
|
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
313
|
+
# Find a route matching method + path. Returns [route, params] or nil.
|
|
314
|
+
# match(method, path) — consistent with Python, PHP, and Node.
|
|
315
|
+
def match(method, path)
|
|
316
|
+
find_route(method, path)
|
|
299
317
|
end
|
|
300
318
|
|
|
301
319
|
# Register a class-based middleware globally.
|
|
@@ -319,6 +337,7 @@ module Tina4
|
|
|
319
337
|
@method_index = Hash.new { |h, k| h[k] = [] }
|
|
320
338
|
@ws_routes = []
|
|
321
339
|
end
|
|
340
|
+
alias clear clear!
|
|
322
341
|
|
|
323
342
|
def group(prefix, auth_handler: nil, middleware: [], &block)
|
|
324
343
|
GroupContext.new(prefix, auth_handler, middleware).instance_eval(&block)
|
|
@@ -349,7 +368,7 @@ module Tina4
|
|
|
349
368
|
define_method(m) do |path, middleware: [], swagger_meta: {}, template: nil, &handler|
|
|
350
369
|
full_path = "#{@prefix}#{path}"
|
|
351
370
|
combined_middleware = @middleware + middleware
|
|
352
|
-
Tina4::Router.
|
|
371
|
+
Tina4::Router.add(m, full_path, handler,
|
|
353
372
|
auth_handler: @auth_handler,
|
|
354
373
|
swagger_meta: swagger_meta,
|
|
355
374
|
middleware: combined_middleware,
|
data/lib/tina4/seeder.rb
CHANGED
|
@@ -85,6 +85,14 @@ module Tina4
|
|
|
85
85
|
STREET_TYPES = %w[Street Avenue Road Drive Lane Boulevard Way Place].freeze
|
|
86
86
|
COMPANY_WORDS = %w[Tech Global Apex Nova Core Prime Next Blue Bright Smart Swift Peak Fusion Pulse Vertex].freeze
|
|
87
87
|
COMPANY_SUFFIXES = %w[Inc Corp Ltd LLC Group Solutions Systems Labs].freeze
|
|
88
|
+
JOB_TITLES = [
|
|
89
|
+
"Software Engineer", "Product Manager", "Designer", "Data Analyst",
|
|
90
|
+
"DevOps Engineer", "CEO", "CTO", "Sales Manager", "Marketing Lead",
|
|
91
|
+
"Accountant", "Operations Manager", "QA Engineer", "UX Researcher",
|
|
92
|
+
"Support Specialist", "HR Manager", "Technical Writer"
|
|
93
|
+
].freeze
|
|
94
|
+
CURRENCIES = %w[USD EUR GBP JPY CAD AUD CHF ZAR INR CNY].freeze
|
|
95
|
+
CREDIT_CARD_PREFIXES = %w[4111 4242 5500 5105].freeze
|
|
88
96
|
|
|
89
97
|
def initialize(seed: nil)
|
|
90
98
|
@rng = seed ? Random.new(seed) : Random.new
|
|
@@ -221,6 +229,25 @@ module Tina4
|
|
|
221
229
|
"#{w1}#{w2} #{suffix}"
|
|
222
230
|
end
|
|
223
231
|
|
|
232
|
+
def job_title
|
|
233
|
+
JOB_TITLES[@rng.rand(JOB_TITLES.length)]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def currency
|
|
237
|
+
CURRENCIES[@rng.rand(CURRENCIES.length)]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def ip_address
|
|
241
|
+
"#{@rng.rand(1..255)}.#{@rng.rand(0..255)}.#{@rng.rand(0..255)}.#{@rng.rand(1..254)}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Generate a fake credit card number (test numbers only, e.g. 4111...).
|
|
245
|
+
def credit_card
|
|
246
|
+
prefix = CREDIT_CARD_PREFIXES[@rng.rand(CREDIT_CARD_PREFIXES.length)]
|
|
247
|
+
rest = Array.new(12) { @rng.rand(0..9) }.join
|
|
248
|
+
prefix + rest
|
|
249
|
+
end
|
|
250
|
+
|
|
224
251
|
def color_hex
|
|
225
252
|
"#%06x" % @rng.rand(0..0xFFFFFF)
|
|
226
253
|
end
|
|
@@ -235,6 +262,11 @@ module Tina4
|
|
|
235
262
|
Array.new(length) { chars[@rng.rand(chars.length)] }.join
|
|
236
263
|
end
|
|
237
264
|
|
|
265
|
+
# Run a generator block `count` times and return the results.
|
|
266
|
+
def run(count = 1, &block)
|
|
267
|
+
Array.new(count) { block.call }
|
|
268
|
+
end
|
|
269
|
+
|
|
238
270
|
# Generate appropriate data based on field definition and column name.
|
|
239
271
|
def for_field(field_def, column_name = nil)
|
|
240
272
|
col = (column_name || "").to_s.downcase
|
|
@@ -495,7 +527,7 @@ module Tina4
|
|
|
495
527
|
# Run all seed files in the given folder.
|
|
496
528
|
#
|
|
497
529
|
# @param seed_folder [String] path to seed files (default: "seeds")
|
|
498
|
-
def self.
|
|
530
|
+
def self.seed_dir(seed_folder: "seeds", clear: false)
|
|
499
531
|
unless Dir.exist?(seed_folder)
|
|
500
532
|
Tina4::Log.info("Seeder: No seeds folder found at #{seed_folder}")
|
|
501
533
|
return
|
data/lib/tina4/session.rb
CHANGED
|
@@ -108,10 +108,44 @@ module Tina4
|
|
|
108
108
|
@id
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# Start or resume a session. If session_id is given, load that session;
|
|
112
|
+
# otherwise generate a new ID. Returns the session ID string.
|
|
113
|
+
def start(session_id = nil)
|
|
114
|
+
if session_id
|
|
115
|
+
@id = session_id
|
|
116
|
+
@data = load_session
|
|
117
|
+
else
|
|
118
|
+
@id = SecureRandom.hex(32)
|
|
119
|
+
@data = {}
|
|
120
|
+
end
|
|
121
|
+
@modified = false
|
|
122
|
+
@id
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns the current session ID string.
|
|
126
|
+
def get_session_id
|
|
127
|
+
@id
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Reads raw session data for a given session ID from backend storage.
|
|
131
|
+
# Returns the data hash or nil.
|
|
132
|
+
def read(session_id)
|
|
133
|
+
@handler.read(session_id)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Writes raw session data for a given session ID to backend storage.
|
|
137
|
+
def write(session_id, data, ttl = nil)
|
|
138
|
+
if ttl
|
|
139
|
+
@handler.write(session_id, data, ttl)
|
|
140
|
+
else
|
|
141
|
+
@handler.write(session_id, data)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
111
145
|
# Garbage collection: remove expired sessions from the handler
|
|
112
|
-
def gc(
|
|
113
|
-
|
|
114
|
-
@handler.gc(
|
|
146
|
+
def gc(max_lifetime = nil)
|
|
147
|
+
max_lifetime ||= @options[:max_age]
|
|
148
|
+
@handler.gc(max_lifetime) if @handler.respond_to?(:gc)
|
|
115
149
|
end
|
|
116
150
|
|
|
117
151
|
def cookie_header(cookie_name = nil)
|
|
@@ -219,9 +253,29 @@ module Tina4
|
|
|
219
253
|
@session.regenerate
|
|
220
254
|
end
|
|
221
255
|
|
|
222
|
-
def gc(
|
|
256
|
+
def gc(max_lifetime = nil)
|
|
257
|
+
ensure_loaded
|
|
258
|
+
@session.gc(max_lifetime)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def start(session_id = nil)
|
|
262
|
+
ensure_loaded
|
|
263
|
+
@session.start(session_id)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def get_session_id
|
|
267
|
+
ensure_loaded
|
|
268
|
+
@session.get_session_id
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def read(session_id)
|
|
272
|
+
ensure_loaded
|
|
273
|
+
@session.read(session_id)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def write(session_id, data, ttl = nil)
|
|
223
277
|
ensure_loaded
|
|
224
|
-
@session.
|
|
278
|
+
@session.write(session_id, data, ttl)
|
|
225
279
|
end
|
|
226
280
|
|
|
227
281
|
def cookie_header(cookie_name = nil)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
|
+
require_relative "cache"
|
|
4
5
|
|
|
5
6
|
module Tina4
|
|
6
7
|
# Cross-engine SQL translator.
|
|
@@ -154,142 +155,4 @@ module Tina4
|
|
|
154
155
|
end
|
|
155
156
|
end
|
|
156
157
|
end
|
|
157
|
-
|
|
158
|
-
# In-memory cache with TTL support for query results.
|
|
159
|
-
#
|
|
160
|
-
# Usage:
|
|
161
|
-
# cache = Tina4::QueryCache.new(default_ttl: 60, max_size: 1000)
|
|
162
|
-
# cache.set("key", "value", ttl: 30)
|
|
163
|
-
# cache.get("key") # => "value"
|
|
164
|
-
#
|
|
165
|
-
class QueryCache
|
|
166
|
-
CacheEntry = Struct.new(:value, :expires_at, :tags)
|
|
167
|
-
|
|
168
|
-
# @param default_ttl [Integer] default TTL in seconds (default: 300)
|
|
169
|
-
# @param max_size [Integer] maximum number of cache entries (default: 1000)
|
|
170
|
-
def initialize(default_ttl: 300, max_size: 1000)
|
|
171
|
-
@default_ttl = default_ttl
|
|
172
|
-
@max_size = max_size
|
|
173
|
-
@store = {}
|
|
174
|
-
@mutex = Mutex.new
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Store a value with optional TTL and tags.
|
|
178
|
-
#
|
|
179
|
-
# @param key [String]
|
|
180
|
-
# @param value [Object]
|
|
181
|
-
# @param ttl [Integer, nil] TTL in seconds (nil uses default)
|
|
182
|
-
# @param tags [Array<String>] optional tags for grouped invalidation
|
|
183
|
-
def set(key, value, ttl: nil, tags: [])
|
|
184
|
-
ttl ||= @default_ttl
|
|
185
|
-
expires_at = Time.now.to_f + ttl
|
|
186
|
-
|
|
187
|
-
@mutex.synchronize do
|
|
188
|
-
# Evict oldest if at capacity
|
|
189
|
-
if @store.size >= @max_size && !@store.key?(key)
|
|
190
|
-
oldest_key = @store.keys.first
|
|
191
|
-
@store.delete(oldest_key)
|
|
192
|
-
end
|
|
193
|
-
@store[key] = CacheEntry.new(value, expires_at, tags)
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Retrieve a cached value. Returns nil if expired or missing.
|
|
198
|
-
#
|
|
199
|
-
# @param key [String]
|
|
200
|
-
# @param default [Object] value to return if key is missing
|
|
201
|
-
# @return [Object, nil]
|
|
202
|
-
def get(key, default = nil)
|
|
203
|
-
@mutex.synchronize do
|
|
204
|
-
entry = @store[key]
|
|
205
|
-
return default unless entry
|
|
206
|
-
|
|
207
|
-
if Time.now.to_f > entry.expires_at
|
|
208
|
-
@store.delete(key)
|
|
209
|
-
return default
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
entry.value
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Check if a key exists and is not expired.
|
|
217
|
-
#
|
|
218
|
-
# @param key [String]
|
|
219
|
-
# @return [Boolean]
|
|
220
|
-
def has?(key)
|
|
221
|
-
@mutex.synchronize do
|
|
222
|
-
entry = @store[key]
|
|
223
|
-
return false unless entry
|
|
224
|
-
|
|
225
|
-
if Time.now.to_f > entry.expires_at
|
|
226
|
-
@store.delete(key)
|
|
227
|
-
return false
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
true
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Delete a key from the cache.
|
|
235
|
-
#
|
|
236
|
-
# @param key [String]
|
|
237
|
-
# @return [Boolean] true if the key was present
|
|
238
|
-
def delete(key)
|
|
239
|
-
@mutex.synchronize do
|
|
240
|
-
!@store.delete(key).nil?
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Clear all entries from the cache.
|
|
245
|
-
def clear
|
|
246
|
-
@mutex.synchronize { @store.clear }
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Clear all entries with a given tag.
|
|
250
|
-
#
|
|
251
|
-
# @param tag [String]
|
|
252
|
-
# @return [Integer] number of entries removed
|
|
253
|
-
def clear_tag(tag)
|
|
254
|
-
@mutex.synchronize do
|
|
255
|
-
keys_to_remove = @store.select { |_k, v| v.tags.include?(tag) }.keys
|
|
256
|
-
keys_to_remove.each { |k| @store.delete(k) }
|
|
257
|
-
keys_to_remove.size
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# Remove all expired entries.
|
|
262
|
-
#
|
|
263
|
-
# @return [Integer] number of entries removed
|
|
264
|
-
def sweep
|
|
265
|
-
@mutex.synchronize do
|
|
266
|
-
now = Time.now.to_f
|
|
267
|
-
keys_to_remove = @store.select { |_k, v| now > v.expires_at }.keys
|
|
268
|
-
keys_to_remove.each { |k| @store.delete(k) }
|
|
269
|
-
keys_to_remove.size
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# Fetch from cache, or compute and store.
|
|
274
|
-
#
|
|
275
|
-
# @param key [String]
|
|
276
|
-
# @param ttl [Integer] TTL in seconds
|
|
277
|
-
# @param block [Proc] factory to compute the value if not cached
|
|
278
|
-
# @return [Object]
|
|
279
|
-
def remember(key, ttl, &block)
|
|
280
|
-
cached = get(key)
|
|
281
|
-
return cached unless cached.nil?
|
|
282
|
-
|
|
283
|
-
value = block.call
|
|
284
|
-
set(key, value, ttl: ttl)
|
|
285
|
-
value
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
# Current number of entries in the cache.
|
|
289
|
-
#
|
|
290
|
-
# @return [Integer]
|
|
291
|
-
def size
|
|
292
|
-
@mutex.synchronize { @store.size }
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
158
|
end
|
data/lib/tina4/test_client.rb
CHANGED
|
@@ -114,7 +114,7 @@ module Tina4
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
# Match route
|
|
117
|
-
result = Tina4::Router.
|
|
117
|
+
result = Tina4::Router.match(method.upcase, clean_path)
|
|
118
118
|
|
|
119
119
|
unless result
|
|
120
120
|
return TestResponse.new([404, { "content-type" => "application/json" }, ['{"error":"Not found"}']])
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/websocket.rb
CHANGED
|
@@ -30,6 +30,71 @@ module Tina4
|
|
|
30
30
|
upgrade.downcase == "websocket"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def get_clients
|
|
34
|
+
@connections
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def start(host: "0.0.0.0", port: 7147)
|
|
38
|
+
require "socket"
|
|
39
|
+
@server_socket = TCPServer.new(host, port)
|
|
40
|
+
@running = true
|
|
41
|
+
@server_thread = Thread.new do
|
|
42
|
+
while @running
|
|
43
|
+
begin
|
|
44
|
+
client = @server_socket.accept
|
|
45
|
+
env = {}
|
|
46
|
+
handle_upgrade(env, client)
|
|
47
|
+
rescue => e
|
|
48
|
+
break unless @running
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stop
|
|
56
|
+
@running = false
|
|
57
|
+
@server_socket&.close rescue nil
|
|
58
|
+
@server_thread&.join(1)
|
|
59
|
+
@connections.each_value { |conn| conn.close rescue nil }
|
|
60
|
+
@connections.clear
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def broadcast(message, exclude: nil, path: nil)
|
|
64
|
+
@connections.each do |id, conn|
|
|
65
|
+
next if exclude && id == exclude
|
|
66
|
+
next if path && conn.path != path
|
|
67
|
+
conn.send_text(message)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ── Rooms ──────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def join_room_for(conn_id, room_name)
|
|
74
|
+
@rooms[room_name] ||= Set.new
|
|
75
|
+
@rooms[room_name].add(conn_id)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def leave_room_for(conn_id, room_name)
|
|
79
|
+
@rooms[room_name]&.delete(conn_id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def room_count(room_name)
|
|
83
|
+
(@rooms[room_name] || Set.new).size
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get_room_connections(room_name)
|
|
87
|
+
ids = @rooms[room_name] || Set.new
|
|
88
|
+
ids.filter_map { |id| @connections[id] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def broadcast_to_room(room_name, message, exclude: nil)
|
|
92
|
+
(get_room_connections(room_name)).each do |conn|
|
|
93
|
+
next if exclude && conn.id == exclude
|
|
94
|
+
conn.send_text(message)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
33
98
|
def handle_upgrade(env, socket)
|
|
34
99
|
key = env["HTTP_SEC_WEBSOCKET_KEY"]
|
|
35
100
|
return unless key
|
|
@@ -78,43 +143,6 @@ module Tina4
|
|
|
78
143
|
end
|
|
79
144
|
end
|
|
80
145
|
|
|
81
|
-
def broadcast(message, exclude: nil, path: nil)
|
|
82
|
-
@connections.each do |id, conn|
|
|
83
|
-
next if exclude && id == exclude
|
|
84
|
-
next if path && conn.path != path
|
|
85
|
-
conn.send_text(message)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# ── Rooms ──────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
def join_room_for(conn_id, room_name)
|
|
92
|
-
@rooms[room_name] ||= Set.new
|
|
93
|
-
@rooms[room_name].add(conn_id)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def leave_room_for(conn_id, room_name)
|
|
97
|
-
@rooms[room_name]&.delete(conn_id)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def room_count(room_name)
|
|
101
|
-
(@rooms[room_name] || Set.new).size
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def get_room_connections(room_name)
|
|
105
|
-
ids = @rooms[room_name] || Set.new
|
|
106
|
-
ids.filter_map { |id| @connections[id] }
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def broadcast_to_room(room_name, message, exclude: nil)
|
|
110
|
-
(get_room_connections(room_name)).each do |conn|
|
|
111
|
-
next if exclude && conn.id == exclude
|
|
112
|
-
conn.send_text(message)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
private
|
|
117
|
-
|
|
118
146
|
def emit(event, *args)
|
|
119
147
|
@handlers[event]&.each { |h| h.call(*args) }
|
|
120
148
|
end
|
|
@@ -218,8 +246,6 @@ module Tina4
|
|
|
218
246
|
nil
|
|
219
247
|
end
|
|
220
248
|
|
|
221
|
-
private
|
|
222
|
-
|
|
223
249
|
def build_frame(opcode, data)
|
|
224
250
|
frame = [0x80 | opcode].pack("C")
|
|
225
251
|
length = data.bytesize
|