prosody 0.3.0-aarch64-linux → 0.5.0-aarch64-linux
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/.cargo/config.toml +3 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +395 -0
- data/ARCHITECTURE.md +14 -4
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +395 -0
- data/CONFIGURATION.md +167 -0
- data/README.md +436 -146
- data/Rakefile +11 -1
- data/examples/keyed_state.rb +70 -0
- data/examples/keyed_state.rbs +18 -0
- data/examples/keyed_state_windowing.rb +55 -0
- data/examples/keyed_state_windowing.rbs +16 -0
- data/lib/prosody/3.2/prosody.so +0 -0
- data/lib/prosody/3.3/prosody.so +0 -0
- data/lib/prosody/3.4/prosody.so +0 -0
- data/lib/prosody/configuration.rb +71 -11
- data/lib/prosody/handler.rb +65 -8
- data/lib/prosody/native_stubs.rb +550 -9
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +816 -0
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +6 -0
- data/release-please-config.json +4 -0
- data/sig/configuration.rbs +70 -11
- data/sig/handler.rbs +17 -5
- data/sig/processor.rbs +28 -12
- data/sig/prosody.rbs +53 -7
- data/sig/request.rbs +66 -0
- data/sig/sentry.rbs +6 -0
- data/sig/state.rbs +390 -0
- data/steep_expectations.yml +57 -0
- data/typecheck/payload_types.rb +54 -0
- data/typecheck/payload_types.rbs +22 -0
- data/typecheck_negative/payload_types.rb +20 -0
- data/typecheck_negative/payload_types.rbs +9 -0
- metadata +27 -10
data/Rakefile
CHANGED
|
@@ -23,4 +23,14 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
23
23
|
|
|
24
24
|
require "standard/rake"
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
desc "Validate RBS signatures over the whole sig/ tree"
|
|
27
|
+
task :rbs do
|
|
28
|
+
sh "rbs -r logger -I sig validate"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "Type-check the Ruby implementation against its RBS signatures"
|
|
32
|
+
task :steep do
|
|
33
|
+
sh "steep check --with-expectations --severity-level=error"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
task default: %i[compile spec standard rbs steep]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prosody"
|
|
4
|
+
require "logger"
|
|
5
|
+
|
|
6
|
+
# Keyed-state example: per-key value/map/deque collections that survive across
|
|
7
|
+
# events. Definitions are declared once and reused for both registration (on the
|
|
8
|
+
# client) and binding (inside the handler). Every state op yields the fiber,
|
|
9
|
+
# never the thread.
|
|
10
|
+
|
|
11
|
+
# Definitions: declared once, reused for registration and binding. See
|
|
12
|
+
# keyed_state.rbs for payload and state types checked by Steep.
|
|
13
|
+
CART = Prosody.value("cart", ttl: 30 * 24 * 3600) # ValueState
|
|
14
|
+
TOTALS = Prosody.map("totals") # keys are always String
|
|
15
|
+
BACKLOG = Prosody.message_deque("backlog", capacity: 100) # bounded window of messages
|
|
16
|
+
|
|
17
|
+
class KeyedStateHandler < Prosody::EventHandler
|
|
18
|
+
def on_excise(context, message)
|
|
19
|
+
puts "Excise #{message.key}"
|
|
20
|
+
context.state(CART).clear
|
|
21
|
+
context.state(TOTALS).clear
|
|
22
|
+
context.state(BACKLOG).clear
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(logger:)
|
|
27
|
+
@logger = logger
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_message(context, message)
|
|
31
|
+
payload = message.payload
|
|
32
|
+
cart = context.state(CART) # bound for this attempt only
|
|
33
|
+
current = cart.get || {"items" => []} # Hash, or nil when absent
|
|
34
|
+
cart.set(current.merge("items" => current["items"] + [payload["order_id"]]))
|
|
35
|
+
|
|
36
|
+
totals = context.state(TOTALS)
|
|
37
|
+
totals.set(message.key, payload["total"])
|
|
38
|
+
# Steep infers key as String and total as Integer from TOTALS's RBS type.
|
|
39
|
+
totals.each_pair { |key, total| @logger.info(format_total(key, total)) }
|
|
40
|
+
|
|
41
|
+
backlog = context.state(BACKLOG)
|
|
42
|
+
backlog.push(message) # stores the full Prosody::Message
|
|
43
|
+
oldest = backlog.get(0) # Message[order_event]?, preserving payload shape
|
|
44
|
+
@logger.info("oldest order: #{format_order_id(oldest.payload["order_id"])}") if oldest
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_timer(_context, _timer)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def format_total(key, total)
|
|
53
|
+
"#{key}=#{total}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_order_id(order_id)
|
|
57
|
+
order_id
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if __FILE__ == $PROGRAM_NAME
|
|
62
|
+
client = Prosody::Client.new(
|
|
63
|
+
mock: true,
|
|
64
|
+
group_id: "keyed-state-example",
|
|
65
|
+
subscribed_topics: "orders",
|
|
66
|
+
state_collections: [CART, TOTALS, BACKLOG]
|
|
67
|
+
)
|
|
68
|
+
client.subscribe(KeyedStateHandler.new(logger: Logger.new($stdout)))
|
|
69
|
+
client.shutdown
|
|
70
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type keyed_state_cart = { "items" => Array[String] }
|
|
2
|
+
type keyed_state_order_event = { "order_id" => String, "total" => Integer }
|
|
3
|
+
|
|
4
|
+
CART: Prosody::_ValueDefinition[keyed_state_cart]
|
|
5
|
+
TOTALS: Prosody::_MapDefinition[Integer]
|
|
6
|
+
BACKLOG: Prosody::_MessageDequeDefinition[keyed_state_order_event]
|
|
7
|
+
|
|
8
|
+
class KeyedStateHandler < Prosody::EventHandler[keyed_state_order_event]
|
|
9
|
+
@logger: Logger
|
|
10
|
+
|
|
11
|
+
def initialize: (logger: Logger) -> void
|
|
12
|
+
def on_message: (Prosody::Context context, Prosody::Message[keyed_state_order_event] message) -> void
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def format_total: (String key, Integer total) -> String
|
|
17
|
+
def format_order_id: (String order_id) -> String
|
|
18
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prosody"
|
|
4
|
+
|
|
5
|
+
# The complete typed counterpart of the README burst-windowing example.
|
|
6
|
+
ACTIVITY_WINDOW = Prosody.value("activity-window")
|
|
7
|
+
PENDING_ACTIVITIES = Prosody.message_deque("pending-activities", capacity: 100)
|
|
8
|
+
|
|
9
|
+
class ActivityWindowHandler < Prosody::EventHandler
|
|
10
|
+
def on_excise(context, message)
|
|
11
|
+
puts "Excise #{message.key}"
|
|
12
|
+
context.state(PENDING_ACTIVITIES).clear
|
|
13
|
+
context.state(ACTIVITY_WINDOW).clear
|
|
14
|
+
context.clear_scheduled
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def on_message(context, message)
|
|
19
|
+
window = context.state(ACTIVITY_WINDOW)
|
|
20
|
+
pending = context.state(PENDING_ACTIVITIES)
|
|
21
|
+
|
|
22
|
+
if window.get
|
|
23
|
+
pending.push(message)
|
|
24
|
+
else
|
|
25
|
+
notify(message.key, [message])
|
|
26
|
+
window.set(true)
|
|
27
|
+
context.clear_and_schedule(Time.now + 5 * 60)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_timer(context, timer)
|
|
32
|
+
pending = context.state(PENDING_ACTIVITIES)
|
|
33
|
+
batch = pending.each.to_a
|
|
34
|
+
notify(timer.key, batch) unless batch.empty?
|
|
35
|
+
pending.clear
|
|
36
|
+
context.state(ACTIVITY_WINDOW).clear
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def notify(user_id, activities)
|
|
42
|
+
puts "notify #{user_id}: #{activities.length} activities"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if __FILE__ == $PROGRAM_NAME
|
|
47
|
+
client = Prosody::Client.new(
|
|
48
|
+
mock: true,
|
|
49
|
+
group_id: "activity-window-example",
|
|
50
|
+
subscribed_topics: "activities",
|
|
51
|
+
state_collections: [ACTIVITY_WINDOW, PENDING_ACTIVITIES]
|
|
52
|
+
)
|
|
53
|
+
client.subscribe(ActivityWindowHandler.new)
|
|
54
|
+
client.shutdown
|
|
55
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type activity_event = {
|
|
2
|
+
"actor" => String,
|
|
3
|
+
"action" => String
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
ACTIVITY_WINDOW: Prosody::_ValueDefinition[bool]
|
|
7
|
+
PENDING_ACTIVITIES: Prosody::_MessageDequeDefinition[activity_event]
|
|
8
|
+
|
|
9
|
+
class ActivityWindowHandler < Prosody::EventHandler[activity_event]
|
|
10
|
+
def on_message: (Prosody::Context context, Prosody::Message[activity_event] message) -> void
|
|
11
|
+
def on_timer: (Prosody::Context context, Prosody::Timer timer) -> void
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def notify: (String user_id, Array[Prosody::Message[activity_event]] activities) -> void
|
|
16
|
+
end
|
data/lib/prosody/3.2/prosody.so
CHANGED
|
Binary file
|
data/lib/prosody/3.3/prosody.so
CHANGED
|
Binary file
|
data/lib/prosody/3.4/prosody.so
CHANGED
|
Binary file
|
|
@@ -131,7 +131,7 @@ module Prosody
|
|
|
131
131
|
config_param :max_retry_delay, converter: ->(v) { duration_converter(v) }
|
|
132
132
|
|
|
133
133
|
# Global shared cache capacity across all partitions for message deduplication.
|
|
134
|
-
#
|
|
134
|
+
# Must be at least 1. Default: 8192.
|
|
135
135
|
config_param :idempotence_cache_size, converter: ->(v) { Integer(v) }
|
|
136
136
|
|
|
137
137
|
# Version string for cache-busting deduplication hashes. Changing this
|
|
@@ -156,6 +156,21 @@ module Prosody
|
|
|
156
156
|
# Identifier for the system producing messages.
|
|
157
157
|
config_param :source_system, converter: lambda(&:to_s)
|
|
158
158
|
|
|
159
|
+
# Address for the peer listener.
|
|
160
|
+
config_param :peer_bind_address, converter: lambda(&:to_s)
|
|
161
|
+
|
|
162
|
+
# gRPC connect URI that other clients use for this client.
|
|
163
|
+
config_param :peer_advertised_connect, converter: lambda(&:to_s)
|
|
164
|
+
|
|
165
|
+
# Network name used to identify direct routes.
|
|
166
|
+
config_param :peer_network_name, converter: lambda(&:to_s)
|
|
167
|
+
|
|
168
|
+
# Maximum number of peer channels and registrations in each cache.
|
|
169
|
+
config_param :peer_cache_capacity, converter: ->(v) { Integer(v) }
|
|
170
|
+
|
|
171
|
+
# Duration of each peer registration lease.
|
|
172
|
+
config_param :peer_registration_ttl, converter: ->(v) { duration_converter(v) }
|
|
173
|
+
|
|
159
174
|
# Topic to send failed messages to.
|
|
160
175
|
config_param :failure_topic, converter: lambda(&:to_s)
|
|
161
176
|
|
|
@@ -172,7 +187,7 @@ module Prosody
|
|
|
172
187
|
end
|
|
173
188
|
}
|
|
174
189
|
|
|
175
|
-
# Keyspace
|
|
190
|
+
# Keyspace used for persistent Prosody data in Cassandra.
|
|
176
191
|
config_param :cassandra_keyspace, converter: lambda(&:to_s)
|
|
177
192
|
|
|
178
193
|
# Preferred datacenter for Cassandra query routing.
|
|
@@ -187,7 +202,7 @@ module Prosody
|
|
|
187
202
|
# Password for authenticating with Cassandra.
|
|
188
203
|
config_param :cassandra_password, converter: lambda(&:to_s)
|
|
189
204
|
|
|
190
|
-
# Retention period for
|
|
205
|
+
# Retention period for persistent timer and deferral data in Cassandra.
|
|
191
206
|
# Accepts duration objects or numeric values (in seconds).
|
|
192
207
|
config_param :cassandra_retention, converter: ->(v) { duration_converter(v) }
|
|
193
208
|
|
|
@@ -235,23 +250,28 @@ module Prosody
|
|
|
235
250
|
# Maximum delay between deferred retries (in seconds).
|
|
236
251
|
config_param :defer_max_delay, converter: ->(v) { duration_converter(v) }
|
|
237
252
|
|
|
238
|
-
# Failure rate threshold for
|
|
253
|
+
# Failure rate threshold for disabling deferral (0.0 to 1.0).
|
|
239
254
|
config_param :defer_failure_threshold, converter: ->(v) { Float(v) }
|
|
240
255
|
|
|
241
256
|
# Sliding window duration (in seconds) for failure rate tracking.
|
|
242
257
|
config_param :defer_failure_window, converter: ->(v) { duration_converter(v) }
|
|
243
258
|
|
|
244
|
-
# Cache size for defer middleware.
|
|
245
|
-
config_param :defer_cache_size, converter: ->(v) { Integer(v) }
|
|
246
|
-
|
|
247
259
|
# Maximum deferred store cache entries per Cassandra defer store. Env: PROSODY_DEFER_STORE_CACHE_SIZE
|
|
248
260
|
config_param :defer_store_cache_size, converter: ->(v) { Integer(v) }
|
|
249
261
|
|
|
250
|
-
#
|
|
251
|
-
|
|
262
|
+
# Kafka message loader configuration
|
|
263
|
+
#
|
|
264
|
+
# Maximum messages retained by the shared Kafka loader.
|
|
265
|
+
# Env: PROSODY_LOADER_CACHE_SIZE. Default: 1024.
|
|
266
|
+
config_param :loader_cache_size, converter: ->(v) { Integer(v) }
|
|
252
267
|
|
|
253
|
-
#
|
|
254
|
-
|
|
268
|
+
# Timeout for Kafka loader seek operations (in seconds).
|
|
269
|
+
# Env: PROSODY_LOADER_SEEK_TIMEOUT. Default: 30 seconds.
|
|
270
|
+
config_param :loader_seek_timeout, converter: ->(v) { duration_converter(v) }
|
|
271
|
+
|
|
272
|
+
# Sequential-read distance before the loader seeks.
|
|
273
|
+
# Env: PROSODY_LOADER_DISCARD_THRESHOLD. Default: 100.
|
|
274
|
+
config_param :loader_discard_threshold, converter: ->(v) { Integer(v) }
|
|
255
275
|
|
|
256
276
|
# Timeout configuration
|
|
257
277
|
#
|
|
@@ -281,6 +301,46 @@ module Prosody
|
|
|
281
301
|
# Overrides the PROSODY_TIMER_SPANS environment variable. Default: "follows_from".
|
|
282
302
|
config_param :timer_spans, converter: ->(v) { v.to_s }
|
|
283
303
|
|
|
304
|
+
# Keyed-state collections to register before subscribe.
|
|
305
|
+
#
|
|
306
|
+
# Accepts an array of StateDefinition objects (from Prosody.value/map/deque
|
|
307
|
+
# and their message_* siblings) or already-serialized registration hashes.
|
|
308
|
+
# Duplicate names within the set are rejected by the native layer.
|
|
309
|
+
config_param :state_collections,
|
|
310
|
+
converter: lambda { |v|
|
|
311
|
+
list = v.is_a?(Hash) ? [v] : Array(v)
|
|
312
|
+
list.map { |d| d.respond_to?(:to_state_config) ? d.to_state_config : d }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Subsystem under which published JSON collections are advertised.
|
|
316
|
+
# Uses PROSODY_SUBSYSTEM when omitted. Published collections require it.
|
|
317
|
+
config_param :subsystem, converter: lambda(&:to_s)
|
|
318
|
+
|
|
319
|
+
# Disk workspace for the local keyed-state cache. Each live client
|
|
320
|
+
# needs its own directory. Falls back to the
|
|
321
|
+
# PROSODY_STATE_CACHE_DIR environment variable. Must not be an empty string.
|
|
322
|
+
config_param :state_cache_dir, converter: lambda(&:to_s)
|
|
323
|
+
|
|
324
|
+
# Capacity of the owning keyed-state cache, such as "64 MiB". Uses
|
|
325
|
+
# PROSODY_STATE_OWNED_CACHE_SIZE when omitted. Otherwise, the engine
|
|
326
|
+
# selects its default.
|
|
327
|
+
config_param :state_owned_cache_size, converter: lambda(&:to_s)
|
|
328
|
+
|
|
329
|
+
# Capacity of the published-state read-through cache, such as "1 MiB".
|
|
330
|
+
# Uses PROSODY_STATE_READ_CACHE_SIZE when omitted. It then uses the owned
|
|
331
|
+
# cache size when set, or 1 MiB when both sizes are unset.
|
|
332
|
+
config_param :state_read_cache_size, converter: lambda(&:to_s)
|
|
333
|
+
|
|
334
|
+
# Default published-read cache policy in seconds, or false to bypass it.
|
|
335
|
+
# Uses PROSODY_STATE_READ_CACHE_TTL when omitted, then 5 seconds.
|
|
336
|
+
config_param :state_read_cache,
|
|
337
|
+
converter: ->(v) { (v == true || v == false) ? v : Float(v) }
|
|
338
|
+
|
|
339
|
+
# Delay in whole seconds between staging a provisional cell and the
|
|
340
|
+
# keyed-state recovery sweep. Every registered TTL must strictly exceed this.
|
|
341
|
+
# Must be a whole number of seconds >= 1 (validated natively).
|
|
342
|
+
config_param :state_recovery_delay, converter: ->(v) { duration_converter(v) }
|
|
343
|
+
|
|
284
344
|
# Operation mode of the client.
|
|
285
345
|
#
|
|
286
346
|
# Valid values:
|
data/lib/prosody/handler.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Prosody
|
|
|
69
69
|
#
|
|
70
70
|
# @param [Symbol] method_name the name of the method to wrap
|
|
71
71
|
# @param [Class<Exception>] exception_classes one or more Exception subclasses to catch
|
|
72
|
-
# @return [
|
|
72
|
+
# @return [Prosody::json_value] The wrapped method result
|
|
73
73
|
# @raise [ArgumentError] if no exception classes given
|
|
74
74
|
# @raise [NameError] if method_name is not defined on this class or its ancestors
|
|
75
75
|
def permanent(method_name, *exception_classes)
|
|
@@ -81,7 +81,7 @@ module Prosody
|
|
|
81
81
|
#
|
|
82
82
|
# @param [Symbol] method_name the name of the method to wrap
|
|
83
83
|
# @param [Class<Exception>] exception_classes one or more Exception subclasses to catch
|
|
84
|
-
# @return [
|
|
84
|
+
# @return [Prosody::json_value] The wrapped method result
|
|
85
85
|
# @raise [ArgumentError] if no exception classes given
|
|
86
86
|
# @raise [NameError] if method_name is not defined on this class or its ancestors
|
|
87
87
|
def transient(method_name, *exception_classes)
|
|
@@ -96,7 +96,7 @@ module Prosody
|
|
|
96
96
|
# @param [Symbol] method_name the method to wrap
|
|
97
97
|
# @param [Array<Class<Exception>>] exception_classes exceptions to catch
|
|
98
98
|
# @param [Class<EventHandlerError>] error_class the error class to wrap caught exceptions in
|
|
99
|
-
# @return [
|
|
99
|
+
# @return [Prosody::json_value] the message response
|
|
100
100
|
def wrap_errors(method_name, exception_classes, error_class)
|
|
101
101
|
# Must specify at least one exception class
|
|
102
102
|
if exception_classes.empty?
|
|
@@ -114,9 +114,10 @@ module Prosody
|
|
|
114
114
|
super(*args, &block)
|
|
115
115
|
rescue *exception_classes => e
|
|
116
116
|
# The new exception's #cause will be set automatically
|
|
117
|
-
raise error_class.new(e.message)
|
|
117
|
+
Kernel.raise error_class.new(e.message)
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
|
+
wrapper.instance_variable_set(:@prosody_error_wrapper, true)
|
|
120
121
|
|
|
121
122
|
prepend wrapper
|
|
122
123
|
end
|
|
@@ -127,8 +128,10 @@ module Prosody
|
|
|
127
128
|
# --------------------------------------------------------------------------
|
|
128
129
|
|
|
129
130
|
# Abstract base class for handling incoming messages and timers from Prosody.
|
|
130
|
-
#
|
|
131
|
-
#
|
|
131
|
+
# The RBS type parameters describe the message payload and handler response.
|
|
132
|
+
# Both parameters default to +Prosody::json_value+. Declare narrower types in
|
|
133
|
+
# your application's RBS to check payload access and handler responses.
|
|
134
|
+
# Subclasses must implement `#on_message`, `#on_excise`, and `#on_timer`.
|
|
132
135
|
# They may also use `permanent` or `transient` decorators to control retry logic.
|
|
133
136
|
#
|
|
134
137
|
# @example
|
|
@@ -144,6 +147,10 @@ module Prosody
|
|
|
144
147
|
# # Process message...
|
|
145
148
|
# end
|
|
146
149
|
#
|
|
150
|
+
# def on_excise(context, message)
|
|
151
|
+
# # Process excise record...
|
|
152
|
+
# end
|
|
153
|
+
#
|
|
147
154
|
# def on_timer(context, trigger)
|
|
148
155
|
# # Process timer event...
|
|
149
156
|
# end
|
|
@@ -151,18 +158,68 @@ module Prosody
|
|
|
151
158
|
class EventHandler
|
|
152
159
|
extend ErrorClassification
|
|
153
160
|
|
|
161
|
+
HANDLER_METHODS = %i[on_message on_excise on_timer].freeze
|
|
162
|
+
|
|
163
|
+
def self.validate_handler!(handler)
|
|
164
|
+
owners = handler.class.ancestors.take_while { |owner| owner != self }
|
|
165
|
+
|
|
166
|
+
HANDLER_METHODS.each do |name|
|
|
167
|
+
validate_method!(handler, owners, name)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.validate_method!(handler, owners, name)
|
|
172
|
+
implemented = owners.any? do |owner|
|
|
173
|
+
!owner.instance_variable_get(:@prosody_error_wrapper) &&
|
|
174
|
+
(owner.instance_methods(false) + owner.private_instance_methods(false)).include?(name)
|
|
175
|
+
end
|
|
176
|
+
raise ArgumentError, "handler must implement ##{name}" unless implemented
|
|
177
|
+
method = handler.method(name)
|
|
178
|
+
while method.owner.instance_variable_get(:@prosody_error_wrapper)
|
|
179
|
+
method = method.super_method
|
|
180
|
+
raise ArgumentError, "handler must implement ##{name}" unless method
|
|
181
|
+
end
|
|
182
|
+
return if accepts_two_parameters?(method)
|
|
183
|
+
|
|
184
|
+
raise ArgumentError, "handler ##{name} must accept two parameters"
|
|
185
|
+
end
|
|
186
|
+
private_class_method :validate_method!
|
|
187
|
+
|
|
188
|
+
def self.accepts_two_parameters?(method)
|
|
189
|
+
parameters = method.parameters
|
|
190
|
+
required = parameters.count { |kind, _| kind == :req }
|
|
191
|
+
positional = parameters.count do |parameter|
|
|
192
|
+
parameter.first == :req || parameter.first == :opt
|
|
193
|
+
end
|
|
194
|
+
has_rest = parameters.any? { |kind, _| kind == :rest }
|
|
195
|
+
has_required_keyword = parameters.any? { |kind, _| kind == :keyreq }
|
|
196
|
+
|
|
197
|
+
!has_required_keyword && required <= 2 && (has_rest || positional >= 2)
|
|
198
|
+
end
|
|
199
|
+
private_class_method :accepts_two_parameters?
|
|
200
|
+
|
|
154
201
|
# Process a single message received from Prosody.
|
|
155
202
|
# This method must be implemented by subclasses to define
|
|
156
203
|
# custom message handling logic.
|
|
157
204
|
#
|
|
158
205
|
# @param [Context] context the message context
|
|
159
|
-
# @param [Message] message the message payload
|
|
206
|
+
# @param [Message<Payload>] message the message and its typed JSON payload
|
|
160
207
|
# @raise [NotImplementedError] if not overridden by subclass
|
|
161
|
-
# @return [
|
|
208
|
+
# @return [Prosody::json_value] the message response
|
|
162
209
|
def on_message(context, message)
|
|
163
210
|
raise NotImplementedError, "Subclasses must implement #on_message"
|
|
164
211
|
end
|
|
165
212
|
|
|
213
|
+
# Process an excise record for a key.
|
|
214
|
+
#
|
|
215
|
+
# @param [Context] context the event context
|
|
216
|
+
# @param [ExciseMessage] message the excise record metadata
|
|
217
|
+
# @raise [NotImplementedError] if not overridden by a subclass
|
|
218
|
+
# @return [Prosody::json_value] the excise response
|
|
219
|
+
def on_excise(context, message)
|
|
220
|
+
raise NotImplementedError, "Subclasses must implement #on_excise"
|
|
221
|
+
end
|
|
222
|
+
|
|
166
223
|
# Process a timer event when it fires.
|
|
167
224
|
# This method must be implemented by subclasses to handle
|
|
168
225
|
# scheduled timer events if they can fire.
|