prosody 0.4.0 → 0.5.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/AGENTS.md +395 -0
  5. data/ARCHITECTURE.md +2 -2
  6. data/CHANGELOG.md +15 -0
  7. data/CLAUDE.md +1 -0
  8. data/CONFIGURATION.md +167 -0
  9. data/Cargo.lock +660 -326
  10. data/Cargo.toml +2 -1
  11. data/README.md +290 -191
  12. data/examples/keyed_state.rb +15 -3
  13. data/examples/keyed_state_windowing.rb +9 -1
  14. data/ext/prosody/Cargo.toml +2 -1
  15. data/ext/prosody/src/admin.rs +1 -5
  16. data/ext/prosody/src/bridge/mod.rs +17 -32
  17. data/ext/prosody/src/client/config.rs +194 -89
  18. data/ext/prosody/src/client/mod.rs +167 -74
  19. data/ext/prosody/src/client/request.rs +132 -0
  20. data/ext/prosody/src/client/support.rs +122 -0
  21. data/ext/prosody/src/handler/context.rs +24 -20
  22. data/ext/prosody/src/handler/message.rs +50 -0
  23. data/ext/prosody/src/handler/mod.rs +112 -84
  24. data/ext/prosody/src/handler/state/mod.rs +488 -0
  25. data/ext/prosody/src/handler/state/registration.rs +104 -0
  26. data/ext/prosody/src/handler/state/scan.rs +218 -0
  27. data/ext/prosody/src/lib.rs +15 -3
  28. data/ext/prosody/src/published.rs +273 -0
  29. data/ext/prosody/src/scheduler/mod.rs +2 -2
  30. data/ext/prosody/src/scheduler/processor.rs +2 -2
  31. data/ext/prosody/src/scheduler/result.rs +7 -4
  32. data/ext/prosody/src/util.rs +86 -5
  33. data/lib/prosody/configuration.rb +49 -15
  34. data/lib/prosody/handler.rb +63 -10
  35. data/lib/prosody/native_stubs.rb +197 -31
  36. data/lib/prosody/request.rb +45 -0
  37. data/lib/prosody/state.rb +164 -41
  38. data/lib/prosody/version.rb +1 -1
  39. data/lib/prosody.rb +1 -0
  40. data/sig/configuration.rbs +51 -15
  41. data/sig/handler.rbs +12 -4
  42. data/sig/prosody.rbs +43 -2
  43. data/sig/request.rbs +66 -0
  44. data/sig/state.rbs +165 -47
  45. data/steep_expectations.yml +10 -0
  46. data/typecheck/payload_types.rb +14 -3
  47. data/typecheck/payload_types.rbs +4 -2
  48. data/typecheck_negative/payload_types.rb +4 -0
  49. data/typecheck_negative/payload_types.rbs +1 -0
  50. metadata +12 -2
  51. data/ext/prosody/src/handler/state.rs +0 -1035
data/lib/prosody/state.rb CHANGED
@@ -40,7 +40,25 @@ module Prosody
40
40
  # both serializes into `Configuration#state_collections` (via
41
41
  # {#to_state_config}) so the collection is registered before subscribe, and
42
42
  # drives {Prosody::Context#state} to vend the matching typed handle.
43
- StateDefinition = Data.define(:name, :kind, :payload, :ttl_seconds, :read_uncommitted, :keyset_limit, :capacity) do
43
+ StateAccess = Data.define(:vend_method, :wrapper, :published_vend_method, :published_wrapper)
44
+ private_constant :StateAccess
45
+ VALUE_ACCESS = StateAccess.new(vend_method: :value_state, wrapper: :ValueState,
46
+ published_vend_method: :published_value, published_wrapper: :PublishedValue)
47
+ MAP_ACCESS = StateAccess.new(vend_method: :map_state, wrapper: :MapState,
48
+ published_vend_method: :published_map, published_wrapper: :PublishedMap)
49
+ DEQUE_ACCESS = StateAccess.new(vend_method: :deque_state, wrapper: :DequeState,
50
+ published_vend_method: :published_deque, published_wrapper: :PublishedDeque)
51
+ MESSAGE_VALUE_ACCESS = StateAccess.new(vend_method: :message_value_state, wrapper: :ValueState,
52
+ published_vend_method: nil, published_wrapper: nil)
53
+ MESSAGE_MAP_ACCESS = StateAccess.new(vend_method: :message_map_state, wrapper: :MapState,
54
+ published_vend_method: nil, published_wrapper: nil)
55
+ MESSAGE_DEQUE_ACCESS = StateAccess.new(vend_method: :message_deque_state, wrapper: :DequeState,
56
+ published_vend_method: nil, published_wrapper: nil)
57
+ private_constant :VALUE_ACCESS, :MAP_ACCESS, :DEQUE_ACCESS,
58
+ :MESSAGE_VALUE_ACCESS, :MESSAGE_MAP_ACCESS, :MESSAGE_DEQUE_ACCESS
59
+
60
+ StateDefinition = Data.define(:name, :kind, :payload, :ttl_seconds, :read_uncommitted,
61
+ :published, :read_cache, :keyset_limit, :capacity, :access) do
44
62
  # Serializes this definition into the native-registration hash, omitting
45
63
  # unset optionals so they fall back to the core defaults.
46
64
  #
@@ -49,6 +67,7 @@ module Prosody
49
67
  config = {name: name, kind: kind, payload: payload}
50
68
  config[:ttl_seconds] = ttl_seconds unless ttl_seconds.nil?
51
69
  config[:read_uncommitted] = read_uncommitted unless read_uncommitted.nil?
70
+ config[:published] = published unless published.nil?
52
71
  config[:keyset_limit] = keyset_limit unless keyset_limit.nil?
53
72
  config[:capacity] = capacity unless capacity.nil?
54
73
  config
@@ -61,9 +80,11 @@ module Prosody
61
80
  # @param ttl [Integer, nil] optional per-write TTL in whole seconds
62
81
  # @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
63
82
  # @return [StateDefinition] a frozen definition
64
- def self.value(name, ttl: nil, read_uncommitted: nil)
83
+ def self.value(name, ttl: nil, read_uncommitted: nil, published: nil, read_cache: nil)
65
84
  StateDefinition.new(name: name.to_s, kind: "value", payload: "json",
66
- ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: nil)
85
+ ttl_seconds: ttl, read_uncommitted: read_uncommitted, published: published,
86
+ read_cache: read_cache, keyset_limit: nil, capacity: nil,
87
+ access: VALUE_ACCESS)
67
88
  end
68
89
 
69
90
  # Defines a `String`-keyed ordered map JSON collection.
@@ -73,9 +94,11 @@ module Prosody
73
94
  # @param keyset_limit [Integer, nil] optional map-only keyset bound (`0..=4096`)
74
95
  # @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
75
96
  # @return [StateDefinition] a frozen definition
76
- def self.map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)
97
+ def self.map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil, published: nil, read_cache: nil)
77
98
  StateDefinition.new(name: name.to_s, kind: "map", payload: "json",
78
- ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: keyset_limit, capacity: nil)
99
+ ttl_seconds: ttl, read_uncommitted: read_uncommitted, published: published,
100
+ read_cache: read_cache, keyset_limit: keyset_limit, capacity: nil,
101
+ access: MAP_ACCESS)
79
102
  end
80
103
 
81
104
  # Defines a deque JSON collection.
@@ -87,9 +110,11 @@ module Prosody
87
110
  # and mutable across deploys, never persisted (see {DequeState#push}).
88
111
  # @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
89
112
  # @return [StateDefinition] a frozen definition
90
- def self.deque(name, ttl: nil, capacity: nil, read_uncommitted: nil)
113
+ def self.deque(name, ttl: nil, capacity: nil, read_uncommitted: nil, published: nil, read_cache: nil)
91
114
  StateDefinition.new(name: name.to_s, kind: "deque", payload: "json",
92
- ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: capacity)
115
+ ttl_seconds: ttl, read_uncommitted: read_uncommitted, published: published,
116
+ read_cache: read_cache, keyset_limit: nil, capacity: capacity,
117
+ access: DEQUE_ACCESS)
93
118
  end
94
119
 
95
120
  # Defines a single-value Kafka-message collection (items are full messages).
@@ -100,7 +125,9 @@ module Prosody
100
125
  # @return [StateDefinition] a frozen definition
101
126
  def self.message_value(name, ttl: nil, read_uncommitted: nil)
102
127
  StateDefinition.new(name: name.to_s, kind: "value", payload: "message",
103
- ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: nil)
128
+ ttl_seconds: ttl, read_uncommitted: read_uncommitted, published: nil,
129
+ read_cache: nil, keyset_limit: nil, capacity: nil,
130
+ access: MESSAGE_VALUE_ACCESS)
104
131
  end
105
132
 
106
133
  # Defines a `String`-keyed ordered map Kafka-message collection.
@@ -112,7 +139,9 @@ module Prosody
112
139
  # @return [StateDefinition] a frozen definition
113
140
  def self.message_map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)
114
141
  StateDefinition.new(name: name.to_s, kind: "map", payload: "message",
115
- ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: keyset_limit, capacity: nil)
142
+ ttl_seconds: ttl, read_uncommitted: read_uncommitted, published: nil,
143
+ read_cache: nil, keyset_limit: keyset_limit, capacity: nil,
144
+ access: MESSAGE_MAP_ACCESS)
116
145
  end
117
146
 
118
147
  # Defines a deque Kafka-message collection.
@@ -126,21 +155,26 @@ module Prosody
126
155
  # @return [StateDefinition] a frozen definition
127
156
  def self.message_deque(name, ttl: nil, capacity: nil, read_uncommitted: nil)
128
157
  StateDefinition.new(name: name.to_s, kind: "deque", payload: "message",
129
- ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: capacity)
158
+ ttl_seconds: ttl, read_uncommitted: read_uncommitted, published: nil,
159
+ read_cache: nil, keyset_limit: nil, capacity: capacity,
160
+ access: MESSAGE_DEQUE_ACCESS)
130
161
  end
131
162
 
132
- # Internal routing tables shared by the state wrappers.
163
+ # Shared state wrapper behavior.
133
164
  module State
134
- # Maps a definition's `[kind, payload]` to the native vend method and the
135
- # public wrapper class that wraps the vended native handle.
136
- VEND = {
137
- %w[value json] => [:value_state, :ValueState],
138
- %w[map json] => [:map_state, :MapState],
139
- %w[deque json] => [:deque_state, :DequeState],
140
- %w[value message] => [:message_value_state, :ValueState],
141
- %w[map message] => [:message_map_state, :MapState],
142
- %w[deque message] => [:message_deque_state, :DequeState]
143
- }.freeze
165
+ module Reading
166
+ # Opens a read-only view of a published JSON collection.
167
+ def state(subsystem, definition)
168
+ access = definition.access
169
+ if access.published_vend_method.nil? || access.published_wrapper.nil?
170
+ raise ArgumentError, "published state readers support JSON collections only"
171
+ end
172
+ cache_seconds = definition.read_cache unless definition.read_cache == false
173
+ native = public_send(access.published_vend_method, subsystem.to_s, definition.name, cache_seconds,
174
+ definition.read_cache == false)
175
+ Prosody.const_get(access.published_wrapper).new(native)
176
+ end
177
+ end
144
178
 
145
179
  # Adds keyed-state vending to the native context. Included into
146
180
  # {Prosody::Context}; kept as a module so the routing can be exercised
@@ -161,12 +195,8 @@ module Prosody
161
195
  cache_key = "#{definition.kind}:#{definition.payload}:#{definition.name}"
162
196
  return cache[cache_key] if cache.key?(cache_key)
163
197
 
164
- vend_method, wrapper = VEND.fetch([definition.kind, definition.payload]) do
165
- raise TransientStateError,
166
- "state: unknown collection kind/payload #{[definition.kind, definition.payload].inspect}"
167
- end
168
- native = public_send(vend_method, definition.name)
169
- cache[cache_key] = Prosody.const_get(wrapper).new(native)
198
+ native = public_send(definition.access.vend_method, definition.name)
199
+ cache[cache_key] = Prosody.const_get(definition.access.wrapper).new(native)
170
200
  end
171
201
  end
172
202
 
@@ -184,19 +214,104 @@ module Prosody
184
214
  # selects the native cursor seam — the default `:scan` yields values (or
185
215
  # `[key, value]` pairs), `:keys` yields bare map keys.
186
216
  def scan_each(direction, opener = :scan)
187
- scan = @native.public_send(opener, direction.to_s)
188
- begin
189
- # `nil` is the exhaustion sentinel (unambiguous under the null ban);
190
- # terminate on it explicitly rather than on falsiness, so a legal
191
- # stored `false` (or a `[key, false]` pair, always a truthy Array)
192
- # does not stop iteration and drop the tail after it.
193
- until (item = scan.next).nil?
194
- yield item
195
- end
196
- ensure
197
- scan.close
217
+ scan_items(@native.public_send(opener, direction)) { |item| yield item }
218
+ end
219
+
220
+ def scan_items(scan)
221
+ # `nil` is the exhaustion sentinel (unambiguous under the null ban);
222
+ # terminate on it explicitly rather than on falsiness, so a legal
223
+ # stored `false` (or a `[key, false]` pair, always a truthy Array)
224
+ # does not stop iteration and drop the tail after it.
225
+ until (item = scan.next).nil?
226
+ yield item
198
227
  end
228
+ ensure
229
+ scan.close
230
+ end
231
+ end
232
+ end
233
+
234
+ class Client
235
+ include State::Reading
236
+ end
237
+
238
+ class PublishedValue
239
+ def initialize(native) = @native = native
240
+ def get(key) = @native.get(key.to_s)
241
+ end
242
+
243
+ class PublishedMap
244
+ include State::Scanning
245
+
246
+ def initialize(native) = @native = native
247
+ def get(key, map_key) = @native.get(key.to_s, map_key.to_s)
248
+ def get_many(key, map_keys) = @native.get_many(key.to_s, map_keys.map(&:to_s))
249
+ def key?(key, map_key) = @native.contains_key(key.to_s, map_key.to_s)
250
+ alias_method :has_key?, :key?
251
+ alias_method :include?, :key?
252
+ alias_method :member?, :key?
253
+
254
+ def each_pair(key, &block) = traverse(key, :forward, &block)
255
+ def reverse_each_pair(key, &block) = traverse(key, :backward, &block)
256
+ def each_key(key, &block) = traverse_keys(key, :forward, &block)
257
+ def reverse_each_key(key, &block) = traverse_keys(key, :backward, &block)
258
+ def each_value(key, &block) = traverse_values(key, :forward, &block)
259
+ def reverse_each_value(key, &block) = traverse_values(key, :backward, &block)
260
+ alias_method :each, :each_pair
261
+
262
+ private
263
+
264
+ def traverse(key, direction)
265
+ return enum_for(__method__, key, direction) unless block_given?
266
+
267
+ scan_items(@native.scan(key.to_s, direction)) { |entry| yield(*entry) }
268
+ end
269
+
270
+ def traverse_keys(key, direction)
271
+ return enum_for(__method__, key, direction) unless block_given?
272
+
273
+ scan_items(@native.keys(key.to_s, direction)) { |map_key| yield map_key }
274
+ end
275
+
276
+ def traverse_values(key, direction)
277
+ return enum_for(__method__, key, direction) unless block_given?
278
+
279
+ scan_items(@native.scan(key.to_s, direction)) { |entry| yield entry[1] }
280
+ end
281
+ end
282
+
283
+ class PublishedDeque
284
+ include State::Scanning
285
+
286
+ def initialize(native) = @native = native
287
+
288
+ def get(key, index)
289
+ unless index.is_a?(Integer)
290
+ raise TransientStateError, "get: index must be an Integer, got #{index.inspect}"
199
291
  end
292
+
293
+ return @native.get(key.to_s, index) unless index.negative?
294
+ return last(key) if index == -1
295
+
296
+ resolved = length(key) + index
297
+ resolved.negative? ? nil : @native.get(key.to_s, resolved)
298
+ end
299
+
300
+ def length(key) = @native.length(key.to_s)
301
+ alias_method :size, :length
302
+ def empty?(key) = @native.is_empty(key.to_s)
303
+ def first(key) = @native.peek_front(key.to_s)
304
+ def last(key) = @native.peek_back(key.to_s)
305
+
306
+ def each(key, &block) = traverse(key, :forward, &block)
307
+ def reverse_each(key, &block) = traverse(key, :backward, &block)
308
+
309
+ private
310
+
311
+ def traverse(key, direction)
312
+ return enum_for(__method__, key, direction) unless block_given?
313
+
314
+ scan_items(@native.scan(key.to_s, direction)) { |item| yield item }
200
315
  end
201
316
  end
202
317
 
@@ -207,7 +322,7 @@ module Prosody
207
322
  # made durable by {#commit}. All operations are fiber-yield async: they look
208
323
  # blocking but never block the thread.
209
324
  class ValueState
210
- # @param native [Prosody::NativeValueState] the vended native handle
325
+ # @param native [Prosody::NativeJsonValueState, Prosody::NativeMessageValueState] the native handle
211
326
  def initialize(native)
212
327
  @native = native
213
328
  end
@@ -261,7 +376,7 @@ module Prosody
261
376
  class MapState
262
377
  include State::Scanning
263
378
 
264
- # @param native [Prosody::NativeMapState] the vended native handle
379
+ # @param native [Prosody::NativeJsonMapState, Prosody::NativeMessageMapState] the native handle
265
380
  def initialize(native)
266
381
  @native = native
267
382
  end
@@ -348,6 +463,8 @@ module Prosody
348
463
  # @yieldparam key [String]
349
464
  # @return [Enumerator, void]
350
465
  def reverse_each_key(&block) = traverse_keys(:backward, &block)
466
+ def each_value(&block) = traverse_values(:forward, &block)
467
+ def reverse_each_value(&block) = traverse_values(:backward, &block)
351
468
 
352
469
  # --- idiomatic Hash-style aliases and conveniences ------------------
353
470
  # Each is composed from the canonical ops above and adds no capability
@@ -484,6 +601,12 @@ module Prosody
484
601
 
485
602
  scan_each(direction, :keys) { |key| yield key }
486
603
  end
604
+
605
+ def traverse_values(direction)
606
+ return enum_for(:traverse_values, direction) unless block_given?
607
+
608
+ scan_each(direction) { |entry| yield entry[1] }
609
+ end
487
610
  end
488
611
 
489
612
  # A deque keyed-state handle.
@@ -494,7 +617,7 @@ module Prosody
494
617
  class DequeState
495
618
  include State::Scanning
496
619
 
497
- # @param native [Prosody::NativeDequeState] the vended native handle
620
+ # @param native [Prosody::NativeJsonDequeState, Prosody::NativeMessageDequeState] the native handle
498
621
  def initialize(native)
499
622
  @native = native
500
623
  end
@@ -6,5 +6,5 @@ module Prosody
6
6
  # This version number follows semantic versioning and is used by the
7
7
  # gem system to identify the library version. It should be updated
8
8
  # according to semver guidelines when making releases.
9
- VERSION = "0.4.0"
9
+ VERSION = "0.5.0"
10
10
  end
data/lib/prosody.rb CHANGED
@@ -45,3 +45,4 @@ end
45
45
  # subclasses the error hierarchy, so every native class and base error must
46
46
  # already exist.
47
47
  require_relative "prosody/state"
48
+ require_relative "prosody/request"
@@ -103,6 +103,26 @@ module Prosody
103
103
  def source_system: () -> String?
104
104
  def source_system=: (_ToS) -> String
105
105
 
106
+ # Address for the peer listener. Prosody reads PROSODY_PEER_BIND_ADDRESS when absent.
107
+ def peer_bind_address: () -> String?
108
+ def peer_bind_address=: (_ToS) -> String
109
+
110
+ # Connect URI for remote peers. Prosody reads PROSODY_PEER_ADVERTISED_CONNECT when absent.
111
+ def peer_advertised_connect: () -> String?
112
+ def peer_advertised_connect=: (_ToS) -> String
113
+
114
+ # Network name for direct routes. Prosody reads PROSODY_PEER_NETWORK_NAME when absent.
115
+ def peer_network_name: () -> String?
116
+ def peer_network_name=: (_ToS) -> String
117
+
118
+ # Peer cache capacity. Prosody reads PROSODY_PEER_CACHE_CAPACITY when absent.
119
+ def peer_cache_capacity: () -> Integer?
120
+ def peer_cache_capacity=: (Integer | _ToInt) -> Integer
121
+
122
+ # Peer registration lease. Prosody reads PROSODY_PEER_REGISTRATION_TTL when absent.
123
+ def peer_registration_ttl: () -> Float?
124
+ def peer_registration_ttl=: (Numeric) -> Float
125
+
106
126
  # Topic to send failed messages to
107
127
  def failure_topic: () -> String?
108
128
  def failure_topic=: (_ToS) -> String
@@ -199,21 +219,21 @@ module Prosody
199
219
  def defer_failure_window: () -> Float?
200
220
  def defer_failure_window=: (Numeric) -> Float
201
221
 
202
- # Defer cache size
203
- def defer_cache_size: () -> Integer?
204
- def defer_cache_size=: (Integer | _ToInt) -> Integer
205
-
206
222
  # Maximum deferred store cache entries. Env: PROSODY_DEFER_STORE_CACHE_SIZE
207
223
  def defer_store_cache_size: () -> Integer?
208
224
  def defer_store_cache_size=: (Integer | _ToInt) -> Integer
209
225
 
210
- # Defer seek timeout (seconds)
211
- def defer_seek_timeout: () -> Float?
212
- def defer_seek_timeout=: (Numeric) -> Float
226
+ # Maximum messages retained by the shared Kafka loader. Env: PROSODY_LOADER_CACHE_SIZE. Default: 1024
227
+ def loader_cache_size: () -> Integer?
228
+ def loader_cache_size=: (Integer | _ToInt) -> Integer
213
229
 
214
- # Defer discard threshold (messages)
215
- def defer_discard_threshold: () -> Integer?
216
- def defer_discard_threshold=: (Integer | _ToInt) -> Integer
230
+ # Kafka loader seek timeout (seconds). Env: PROSODY_LOADER_SEEK_TIMEOUT. Default: 30
231
+ def loader_seek_timeout: () -> Float?
232
+ def loader_seek_timeout=: (Numeric) -> Float
233
+
234
+ # Sequential-read distance before the loader seeks. Env: PROSODY_LOADER_DISCARD_THRESHOLD. Default: 100
235
+ def loader_discard_threshold: () -> Integer?
236
+ def loader_discard_threshold=: (Integer | _ToInt) -> Integer
217
237
 
218
238
  # Handler execution timeout (seconds)
219
239
  def timeout: () -> Float?
@@ -241,16 +261,32 @@ module Prosody
241
261
  def state_collections: () -> Array[untyped]?
242
262
  def state_collections=: (untyped) -> Array[untyped]
243
263
 
264
+ # Subsystem under which published JSON collections are advertised.
265
+ # Uses PROSODY_SUBSYSTEM when omitted. Published collections require it.
266
+ def subsystem: () -> String?
267
+ def subsystem=: (_ToS) -> String
268
+
244
269
  # Root directory for the local keyed-state cache. Falls back to
245
270
  # PROSODY_STATE_CACHE_DIR. Must not be an empty string.
246
271
  def state_cache_dir: () -> String?
247
272
  def state_cache_dir=: (_ToS) -> String
248
273
 
249
- # Capacity of the in-memory keyed-state cache, in bytes. Must be positive.
250
- # Falls back to
251
- # PROSODY_STATE_CACHE_SIZE_BYTES, then the engine default.
252
- def state_cache_size_bytes: () -> Integer?
253
- def state_cache_size_bytes=: (Integer | _ToInt) -> Integer
274
+ # Capacity of the owning keyed-state cache. Accepts a human-readable size.
275
+ # Uses PROSODY_STATE_OWNED_CACHE_SIZE when omitted. Otherwise, the engine
276
+ # selects its default.
277
+ def state_owned_cache_size: () -> String?
278
+ def state_owned_cache_size=: (String | _ToS) -> String
279
+
280
+ # Capacity of the published-state read cache. Uses
281
+ # PROSODY_STATE_READ_CACHE_SIZE when omitted. It then uses the owned cache
282
+ # size when set, or 1 MiB when both sizes are unset.
283
+ def state_read_cache_size: () -> String?
284
+ def state_read_cache_size=: (String | _ToS) -> String
285
+
286
+ # Default published-read cache TTL in seconds, or false to bypass it. Uses
287
+ # PROSODY_STATE_READ_CACHE_TTL when omitted, then 5 seconds.
288
+ def state_read_cache: () -> (Float | false)?
289
+ def state_read_cache=: (Numeric | false) -> (Float | false)
254
290
 
255
291
  # Delay in whole seconds between staging a provisional cell and the recovery
256
292
  # sweep. Falls back to PROSODY_STATE_RECOVERY_DELAY (read by core).
data/sig/handler.rbs CHANGED
@@ -56,11 +56,16 @@ module Prosody
56
56
  def wrap_errors: (Symbol method_name, Array[Class] exception_classes, singleton(EventHandlerError) error_class) -> void
57
57
  end
58
58
 
59
- # Abstract base class for handling incoming messages from Prosody.
60
- # Subclasses must implement `#on_message` to process received messages.
61
- class EventHandler[Payload = json_value]
59
+ # Abstract base class for handling events from Prosody.
60
+ # Subclasses must implement all three handler methods.
61
+ class EventHandler[Payload = json_value, Response = json_value]
62
62
  extend ErrorClassification
63
63
 
64
+ def self.validate_handler!: [Payload, Response] (EventHandler[Payload, Response] handler) -> void
65
+
66
+ private def self.validate_method!: [Payload, Response] (EventHandler[Payload, Response] handler, Array[Module] owners, Symbol name) -> void
67
+ private def self.accepts_two_parameters?: (Method method) -> bool
68
+
64
69
  # Process a single message received from Prosody.
65
70
  # This method must be implemented by subclasses to define
66
71
  # custom message handling logic.
@@ -68,7 +73,10 @@ module Prosody
68
73
  # @param context the message context containing metadata and control capabilities
69
74
  # @param message the message content with payload and metadata
70
75
  # @raise [NotImplementedError] if not overridden by subclass
71
- def on_message: (Context context, Message[Payload] message) -> void
76
+ def on_message: (Context context, Message[Payload] message) -> Response
77
+
78
+ # Process an excise record.
79
+ def on_excise: (Context context, ExciseMessage message) -> Response
72
80
 
73
81
  # Process a timer that has fired.
74
82
  # This method is called when a previously scheduled timer fires.
data/sig/prosody.rbs CHANGED
@@ -12,6 +12,24 @@ module Prosody
12
12
  # Sets the module-level logger. Pass nil to reset to the default.
13
13
  def self.logger=: (Logger?) -> Logger?
14
14
 
15
+ # Exports buffered telemetry without shutting down the process-global pipeline.
16
+ def self.flush_telemetry: () -> void
17
+
18
+ # Exports buffered telemetry and shuts down the process-global pipeline.
19
+ def self.shutdown_telemetry: () -> void
20
+
21
+ # Manages Kafka topics.
22
+ class AdminClient
23
+ # Creates an admin client for the specified Kafka bootstrap servers.
24
+ def self.new: (Array[String]) -> AdminClient
25
+
26
+ # Creates a topic.
27
+ def create_topic: (String, Integer, Integer) -> void
28
+
29
+ # Deletes a topic.
30
+ def delete_topic: (String) -> void
31
+ end
32
+
15
33
  # Wrapper for dynamically-typed results from async operations.
16
34
  # This is an internal class used for bridging Rust and Ruby.
17
35
  class DynamicResult
@@ -95,6 +113,15 @@ module Prosody
95
113
  def payload: () -> Payload
96
114
  end
97
115
 
116
+ # Represents an excise record with no payload.
117
+ class ExciseMessage
118
+ def topic: () -> String
119
+ def partition: () -> Integer
120
+ def offset: () -> Integer
121
+ def key: () -> String
122
+ def timestamp: () -> Time
123
+ end
124
+
98
125
  # Represents a timer that was scheduled to fire at a specific time.
99
126
  # Timer instances are passed to EventHandler's on_timer method when timers fire.
100
127
  class Timer
@@ -126,7 +153,7 @@ module Prosody
126
153
 
127
154
  # Returns the current state of the consumer.
128
155
  #
129
- # @return [Symbol] The consumer state: :unconfigured, :configured, or :running
156
+ # @return [Symbol] The consumer state: :shut_down, :unconfigured, :configured, or :running
130
157
  def consumer_state: () -> Symbol
131
158
 
132
159
  # Returns the number of Kafka partitions currently assigned to this consumer.
@@ -151,13 +178,16 @@ module Prosody
151
178
  # @raise [RuntimeError] If the message cannot be sent
152
179
  def send_message: (String topic, String key, json_value payload) -> void
153
180
 
181
+ # Sends an excise record for a key.
182
+ def excise: (String topic, String key) -> void
183
+
154
184
  # Subscribes to Kafka topics using the provided handler.
155
185
  # The handler must implement an `on_message(context, message)` method.
156
186
  #
157
187
  # @param handler [EventHandler] A handler object that processes messages
158
188
  # @return [void]
159
189
  # @raise [RuntimeError] If subscription fails
160
- def subscribe: [Payload] (EventHandler[Payload]) -> void
190
+ def subscribe: [Payload, Response] (EventHandler[Payload, Response]) -> void
161
191
 
162
192
  # Unsubscribes from all topics, stopping message processing.
163
193
  #
@@ -165,6 +195,13 @@ module Prosody
165
195
  # @raise [RuntimeError] If unsubscription fails
166
196
  def unsubscribe: () -> void
167
197
 
198
+ # Shuts down all client services.
199
+ # Concurrent and repeated calls wait for the same shutdown operation.
200
+ #
201
+ # @return [void]
202
+ # @raise [RuntimeError] If shutdown fails
203
+ def shutdown: () -> void
204
+
168
205
  # Returns the configured source system identifier.
169
206
  #
170
207
  # The source system is used to identify the originating service or
@@ -172,5 +209,9 @@ module Prosody
172
209
  #
173
210
  # @return [String] The source system identifier
174
211
  def source_system: () -> String
212
+
213
+ def state: [T] (_ToS subsystem, _ValueDefinition[T] definition) -> PublishedValue[T]
214
+ | [V] (_ToS subsystem, _MapDefinition[V] definition) -> PublishedMap[V]
215
+ | [T] (_ToS subsystem, _DequeDefinition[T] definition) -> PublishedDeque[T]
175
216
  end
176
217
  end
data/sig/request.rbs ADDED
@@ -0,0 +1,66 @@
1
+ module Prosody
2
+ class Success[Value] < Data
3
+ attr_reader value: Value
4
+ end
5
+
6
+ class Failure < Data
7
+ attr_reader error: response_error
8
+ end
9
+
10
+ class HandlerError < Data
11
+ attr_reader message: String
12
+ end
13
+
14
+ class Timeout < Data
15
+ def message: () -> String
16
+ end
17
+
18
+ class FormatMismatch < Data
19
+ def message: () -> String
20
+ end
21
+
22
+ class MalformedResponse < Data
23
+ def message: () -> String
24
+ end
25
+
26
+ type response_error = HandlerError | Timeout | FormatMismatch | MalformedResponse
27
+ type outcome[Value] = Success[Value] | Failure
28
+ type outcomes[Value] = Hash[String, outcome[Value]]
29
+ type native_request = {
30
+ topic: String,
31
+ key: String,
32
+ payload: json_value,
33
+ subsystems: Array[String],
34
+ timeout: Integer | Float
35
+ }
36
+ type native_excise_request = {
37
+ topic: String,
38
+ key: String,
39
+ subsystems: Array[String],
40
+ timeout: Integer | Float
41
+ }
42
+
43
+ class Client
44
+ # The timeout value uses seconds.
45
+ # The method raises if the request cannot start or the Kafka send fails.
46
+ def request: (
47
+ topic: String,
48
+ key: String,
49
+ payload: json_value,
50
+ subsystems: Array[String],
51
+ timeout: Integer | Float
52
+ ) -> outcomes[json_value]
53
+
54
+ def request_excise: (
55
+ topic: String,
56
+ key: String,
57
+ subsystems: Array[String],
58
+ timeout: Integer | Float
59
+ ) -> outcomes[json_value]
60
+
61
+ private
62
+
63
+ def native_request: (native_request request) -> outcomes[json_value]
64
+ def native_request_excise: (native_excise_request request) -> outcomes[json_value]
65
+ end
66
+ end