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.
@@ -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
@@ -80,7 +80,7 @@ module Tina4
80
80
  end
81
81
 
82
82
  # Route matching
83
- result = Tina4::Router.find_route(path, method)
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, :middleware, :template
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 add_route(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
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
- add_route("GET", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
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
- add_route("POST", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
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
- add_route("PUT", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
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
- add_route("PATCH", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
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
- add_route("DELETE", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
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
- add_route("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
294
+ add("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
278
295
  end
279
296
 
280
- def find_route(path, method)
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
- # Alias for find_route().
297
- def match(path, method)
298
- find_route(path, method)
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.add_route(m, full_path, handler,
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.seed(seed_folder: "seeds", clear: false)
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(max_age = nil)
113
- max_age ||= @options[:max_age]
114
- @handler.gc(max_age) if @handler.respond_to?(: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(max_age = nil)
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.gc(max_age)
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
@@ -114,7 +114,7 @@ module Tina4
114
114
  end
115
115
 
116
116
  # Match route
117
- result = Tina4::Router.find_route(clean_path, method.upcase)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.10.90"
4
+ VERSION = "3.10.91"
5
5
  end
@@ -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