prosody 0.5.1 → 0.6.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.config/rail.toml +26 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/.ruby-version +1 -1
  5. data/.taplo.toml +1 -1
  6. data/AGENTS.md +29 -15
  7. data/CHANGELOG.md +7 -0
  8. data/CONFIGURATION.md +19 -16
  9. data/Cargo.lock +254 -235
  10. data/Cargo.toml +11 -8
  11. data/README.md +100 -23
  12. data/examples/keyed_state.rb +10 -2
  13. data/examples/keyed_state.rbs +1 -0
  14. data/ext/prosody/Cargo.toml +1 -1
  15. data/ext/prosody/src/admin.rs +36 -36
  16. data/ext/prosody/src/bridge/mod.rs +5 -10
  17. data/ext/prosody/src/client/config/connections.rs +170 -0
  18. data/ext/prosody/src/client/config/middleware.rs +184 -0
  19. data/ext/prosody/src/client/config/mod.rs +396 -0
  20. data/ext/prosody/src/client/config/state.rs +323 -0
  21. data/ext/prosody/src/client/mod.rs +31 -117
  22. data/ext/prosody/src/client/readers.rs +106 -0
  23. data/ext/prosody/src/client/request.rs +2 -2
  24. data/ext/prosody/src/client/support.rs +13 -32
  25. data/ext/prosody/src/gvl.rs +8 -6
  26. data/ext/prosody/src/handler/{context.rs → context/mod.rs} +36 -150
  27. data/ext/prosody/src/handler/context/vending.rs +138 -0
  28. data/ext/prosody/src/handler/message.rs +36 -0
  29. data/ext/prosody/src/handler/mod.rs +18 -8
  30. data/ext/prosody/src/handler/state/deque.rs +144 -0
  31. data/ext/prosody/src/handler/state/mod.rs +163 -268
  32. data/ext/prosody/src/handler/state/query.rs +264 -0
  33. data/ext/prosody/src/handler/state/registration.rs +15 -86
  34. data/ext/prosody/src/handler/state/scan.rs +33 -141
  35. data/ext/prosody/src/handler/state/set.rs +98 -0
  36. data/ext/prosody/src/lib.rs +54 -36
  37. data/ext/prosody/src/logging.rs +6 -6
  38. data/ext/prosody/src/published.rs +171 -152
  39. data/ext/prosody/src/scheduler/result.rs +2 -1
  40. data/ext/prosody/src/util.rs +65 -3
  41. data/lib/prosody/client.rb +32 -0
  42. data/lib/prosody/configuration.rb +20 -12
  43. data/lib/prosody/demand.rb +27 -0
  44. data/lib/prosody/native_stubs/client.rb +157 -0
  45. data/lib/prosody/native_stubs/context.rb +178 -0
  46. data/lib/prosody/native_stubs/message.rb +133 -0
  47. data/lib/prosody/native_stubs.rb +11 -956
  48. data/lib/prosody/state/deque.rb +253 -0
  49. data/lib/prosody/state/map.rb +313 -0
  50. data/lib/prosody/state/set.rb +121 -0
  51. data/lib/prosody/state/value.rb +58 -0
  52. data/lib/prosody/state.rb +157 -677
  53. data/lib/prosody/version.rb +1 -1
  54. data/lib/prosody.rb +2 -1
  55. data/sig/configuration.rbs +19 -10
  56. data/sig/prosody.rbs +37 -4
  57. data/sig/published.rbs +102 -0
  58. data/sig/state.rbs +109 -122
  59. data/typecheck/payload_types.rb +12 -0
  60. data/typecheck/payload_types.rbs +1 -0
  61. metadata +30 -11
  62. data/ext/prosody/src/client/config.rs +0 -1300
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The deque collection: the owned {Prosody::DequeState} handle and the
4
+ # {Prosody::PublishedDeque} reader.
5
+
6
+ module Prosody
7
+ # A deque keyed-state handle.
8
+ #
9
+ # Traversal is explicit: {#each}/{#reverse_each} yield single elements over a
10
+ # native scan, closing the scan via `ensure`. No aggregate-mixin methods are
11
+ # provided.
12
+ class DequeState < State::Handle
13
+ include State::Scanning
14
+
15
+ # Appends an element at the back.
16
+ #
17
+ # @param value [Object] the element (JSON, or a message)
18
+ # @return [void]
19
+ # @raise [PermanentStateError] if `value` is `nil` (use {#clear} to delete)
20
+ def push(value) = @native.push_back(value)
21
+
22
+ # Prepends an element at the front.
23
+ #
24
+ # @param value [Object] the element (JSON, or a message)
25
+ # @return [void]
26
+ # @raise [PermanentStateError] if `value` is `nil` (use {#clear} to delete)
27
+ def unshift(value) = @native.push_front(value)
28
+
29
+ # Removes and returns the back element.
30
+ #
31
+ # @return [Object, nil] the removed element, or `nil` when empty
32
+ def pop = @native.pop_back
33
+
34
+ # Removes and returns the front element.
35
+ #
36
+ # @return [Object, nil] the removed element, or `nil` when empty
37
+ def shift = @native.pop_front
38
+
39
+ # The number of live elements.
40
+ #
41
+ # @return [Integer]
42
+ def length = @native.len
43
+
44
+ alias_method :size, :length
45
+
46
+ # Whether the deque holds no live elements.
47
+ #
48
+ # @return [Boolean]
49
+ def empty? = @native.is_empty
50
+
51
+ # Removes every element.
52
+ #
53
+ # @return [void]
54
+ def clear = @native.clear
55
+
56
+ # Reads the element at `index`, resolving negatives Array-style (mirrors
57
+ # +Array#[]+'s read domain, without the indexer). A non-negative index
58
+ # reads from the front; `-1` is the back element, `-n` the nth from the end.
59
+ # `-1` fast-paths through {#last} (no length read); other negatives resolve
60
+ # against the current length (one length read + one element read),
61
+ # consistent because the deque has a single writer per attempt.
62
+ #
63
+ # @param index [Integer] the position (negative counts from the back)
64
+ # @return [Object, nil] the element, or `nil` outside the bounds
65
+ # @raise [TransientStateError] if `index` is not an Integer
66
+ def get(index)
67
+ unless index.is_a?(Integer)
68
+ raise TransientStateError, "get: index must be an Integer, got #{index.inspect}"
69
+ end
70
+ index.negative? ? at_negative(index) : @native.get(index)
71
+ end
72
+
73
+ # Traverses the live elements in index order.
74
+ #
75
+ # Without a block, returns an {Enumerator} over the native scan. Each step
76
+ # fiber-yields; the scan is closed via `ensure` on stop or exception.
77
+ #
78
+ # Accepts the position keywords documented on {State::Scanning}. Positions
79
+ # count from the front and must be non-negative. To read from the back,
80
+ # use {#reverse_each} with `limit:`.
81
+ #
82
+ # @param query [Hash] optional `from:`, `after:`, `to:`, `before:`,
83
+ # `range:`, and `limit:` keywords
84
+ # @yieldparam element [Object]
85
+ # @return [Enumerator, void]
86
+ # @raise [ArgumentError, TypeError] if a query keyword is invalid
87
+ def each(**query, &block) = traverse(:scan, :forward, query, &block)
88
+
89
+ # Traverses the live elements in reverse index order.
90
+ #
91
+ # @param query [Hash] optional position keywords, as on {#each}
92
+ # @yieldparam element [Object]
93
+ # @return [Enumerator, void]
94
+ def reverse_each(**query, &block) = traverse(:scan, :backward, query, &block)
95
+
96
+ # --- idiomatic Array-style conveniences -----------------------------
97
+ # Composed from the canonical ops above; bounded reads only (no +to_a+,
98
+ # +map+, +sort+, or +Enumerable+ that would materialize the whole deque).
99
+ #
100
+ # Deliberately NOT provided: +[]+ and +at+. This is a remote deque; +get+
101
+ # and +fetch+ accept a single +Integer+ index (negatives resolve from the
102
+ # back, Array-style), but wearing +Array+'s +[]+/+at+ would invite a range
103
+ # read (+deque[0..2]+) that cannot be honored. Use the explicit {#get}, or
104
+ # {#first}/{#last} for the ends.
105
+
106
+ # Prepends +value+, returning +self+ for chaining (mirrors +Array#prepend+).
107
+ # A wrapper, not an alias: the native write returns +nil+.
108
+ #
109
+ # @param value [Object]
110
+ # @return [self]
111
+ def prepend(value)
112
+ unshift(value)
113
+ self
114
+ end
115
+
116
+ # Appends +value+, returning +self+ for chaining (mirrors +Array#append+).
117
+ # A wrapper, not an alias: the native write returns +nil+.
118
+ #
119
+ # @param value [Object]
120
+ # @return [self]
121
+ def append(value)
122
+ push(value)
123
+ self
124
+ end
125
+
126
+ # Appends +value+ and returns +self+ (mirrors +Array#<<+). Alias of
127
+ # {#append}.
128
+ alias_method :<<, :append
129
+
130
+ # The front element, or +nil+ when empty (mirrors +Array#first+). An
131
+ # endpoint-slot read in one round trip (no length read). Under a TTL an
132
+ # expired front slot yields +nil+ even when live interior elements remain —
133
+ # a peek never searches inward.
134
+ #
135
+ # @return [Object, nil]
136
+ def first = @native.peek_front
137
+
138
+ # The back element, or +nil+ when empty (mirrors +Array#last+). An
139
+ # endpoint-slot read in one round trip (no length read); same TTL-hole
140
+ # semantics as {#first}.
141
+ #
142
+ # @return [Object, nil]
143
+ def last = @native.peek_back
144
+
145
+ # Reads the element at +index+, raising or defaulting when out of range
146
+ # (mirrors +Array#fetch+). A +nil+ result is unambiguously "out of range"
147
+ # under the null ban. A negative index counts from the back, as in {#get}.
148
+ # A non-Integer index raises {TransientStateError}.
149
+ #
150
+ # @param index [Integer] the position (negative counts from the back)
151
+ # @param default [Object] returned when +index+ is out of range
152
+ # @yieldparam index [Integer] called (instead of +default+) when out of range
153
+ # @return [Object]
154
+ # @raise [IndexError] when out of range and no default or block is given
155
+ # @raise [TransientStateError] if +index+ is not an Integer
156
+ def fetch(index, *default, &block)
157
+ if default.length > 1
158
+ raise ArgumentError, "wrong number of arguments (given #{default.length + 1}, expected 1..2)"
159
+ end
160
+ unless index.is_a?(Integer)
161
+ raise TransientStateError, "fetch: index must be an Integer, got #{index.inspect}"
162
+ end
163
+ warn "warning: block supersedes default value argument" if block && !default.empty?
164
+
165
+ value = get(index)
166
+ case value
167
+ when nil
168
+ return block.call(index) if block
169
+ raise IndexError, "index #{index} outside deque bounds" if default.empty?
170
+
171
+ # Steep merges the overloads, so it cannot type the default as D.
172
+ default.fetch(0) #: untyped
173
+ else
174
+ value
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ # Resolves a negative Array-style index against the current length: +-1+
181
+ # fast-paths through {#last} (no length read), other negatives read the
182
+ # length and index from the front. Returns +nil+ when the index resolves
183
+ # before the front (past the far end of the deque).
184
+ def at_negative(index)
185
+ return @native.peek_back if index == -1
186
+
187
+ resolved = @native.len + index
188
+ resolved.negative? ? nil : @native.get(resolved)
189
+ end
190
+ end
191
+
192
+ # A read-only view of a published deque, opened by
193
+ # +client.state(subsystem, definition)+. Each read takes the user key and
194
+ # sees only committed state. A failed read raises the same state errors as
195
+ # an owned handle. A read in a forked child process raises +RuntimeError+.
196
+ class PublishedDeque
197
+ include State::Scanning
198
+
199
+ # @param native [Prosody::NativePublishedDeque] the native reader
200
+ def initialize(native) = @native = native
201
+
202
+ # Reads the committed element at +index+ in the deque for +key+. A
203
+ # negative index counts from the back, as in {DequeState#get}.
204
+ #
205
+ # @param index [Integer] the position (negative counts from the back)
206
+ # @return [Object, nil] the element, or +nil+ outside the bounds
207
+ # @raise [TransientStateError] if +index+ is not an Integer
208
+ def get(key, index)
209
+ unless index.is_a?(Integer)
210
+ raise TransientStateError, "get: index must be an Integer, got #{index.inspect}"
211
+ end
212
+
213
+ return @native.get(key, index) unless index.negative?
214
+ # This repeats DequeState#at_negative, because a reader needs the key.
215
+ return last(key) if index == -1
216
+
217
+ resolved = length(key) + index
218
+ resolved.negative? ? nil : @native.get(key, resolved)
219
+ end
220
+
221
+ # The number of committed elements in the deque for +key+.
222
+ #
223
+ # @return [Integer]
224
+ def length(key) = @native.length(key)
225
+ alias_method :size, :length
226
+
227
+ # Whether the deque for +key+ has no committed elements.
228
+ #
229
+ # @return [Boolean]
230
+ def empty?(key) = @native.is_empty(key)
231
+
232
+ # The front element of the deque for +key+.
233
+ #
234
+ # @return [Object, nil] the element, or +nil+ when the deque is empty
235
+ def first(key) = @native.peek_front(key)
236
+
237
+ # The back element of the deque for +key+.
238
+ #
239
+ # @return [Object, nil] the element, or +nil+ when the deque is empty
240
+ def last(key) = @native.peek_back(key)
241
+
242
+ # Traverses the committed elements for +key+ from front to back. Each
243
+ # traversal accepts the position keywords documented on {State::Scanning}.
244
+ #
245
+ # @return [Enumerator, void]
246
+ def each(key, **query, &block) = traverse(:scan, key, :forward, query, &block)
247
+
248
+ # Traverses the committed elements for +key+ from back to front.
249
+ #
250
+ # @return [Enumerator, void]
251
+ def reverse_each(key, **query, &block) = traverse(:scan, key, :backward, query, &block)
252
+ end
253
+ end
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The map collection: the owned {Prosody::MapState} handle and the
4
+ # {Prosody::PublishedMap} reader.
5
+
6
+ module Prosody
7
+ # A `String`-keyed ordered-map keyed-state handle.
8
+ #
9
+ # Traversal is explicit: {#each_pair}/{#reverse_each_pair} yield `key, value`
10
+ # pairs over a native scan, closing the scan via `ensure`. The map has no
11
+ # aggregate mixin methods, because they would load the whole remote
12
+ # collection.
13
+ class MapState < State::Handle
14
+ include State::Scanning
15
+
16
+ # Reads the value for `key`.
17
+ #
18
+ # @param key [String] the map key
19
+ # @return [Object, nil] the value, or `nil` when the key is absent
20
+ def get(key) = @native.get(key)
21
+
22
+ # Reads several keys in a single isolated batch.
23
+ #
24
+ # @param keys [Array<String>] the keys to read, in order
25
+ # @return [Array<Object, nil>] one result per input key; `nil` for absent keys
26
+ def get_many(keys) = @native.get_many(keys)
27
+
28
+ # Tests several keys for presence in a single batch. Like {#key?}, it
29
+ # decodes no values.
30
+ #
31
+ # @param keys [Array<String>] the keys to test, in order
32
+ # @return [Array<Boolean>] one result per input key
33
+ def contains_many(keys) = @native.contains_many(keys)
34
+
35
+ # Whether the map holds no live entries (mirrors +Hash#empty?+).
36
+ #
37
+ # @return [Boolean]
38
+ def empty? = @native.is_empty
39
+
40
+ # Inserts or overwrites `key`.
41
+ #
42
+ # @param key [String] the map key
43
+ # @param value [Object] the value to store (JSON, or a message)
44
+ # @return [void]
45
+ # @raise [PermanentStateError] if `value` is `nil` (use {#delete} to remove)
46
+ def set(key, value) = @native.set(key, value)
47
+
48
+ # Removes `key`.
49
+ #
50
+ # Documented divergence from `Hash#delete`: this returns `nil`, never the
51
+ # removed value (the erased FFI seam does not surface it).
52
+ #
53
+ # @param key [String] the map key
54
+ # @return [nil]
55
+ def delete(key)
56
+ @native.remove(key)
57
+ nil
58
+ end
59
+
60
+ # Removes every entry.
61
+ #
62
+ # @return [void]
63
+ def clear = @native.clear
64
+
65
+ # Traverses the live entries in key order, yielding `key, value`.
66
+ #
67
+ # Without a block, returns an {Enumerator} over the native scan. Each step
68
+ # fiber-yields; the scan is closed via `ensure` on stop or exception. The
69
+ # enumerator is valid only within the current handler invocation.
70
+ #
71
+ # Every map traversal accepts the query keywords documented on
72
+ # {State::Scanning}. For keyset paging, pass the last key of the previous
73
+ # page as `after:` and the page size as `limit:`.
74
+ #
75
+ # @param query [Hash] optional `from:`, `after:`, `to:`, `before:`,
76
+ # `range:`, `prefix:`, and `limit:` keywords
77
+ # @yieldparam key [String]
78
+ # @yieldparam value [Object]
79
+ # @return [Enumerator, void]
80
+ # @raise [ArgumentError, TypeError] if a query keyword is invalid
81
+ def each_pair(**query, &block) = traverse(:scan, :forward, query, &block)
82
+
83
+ # Traverses the live entries in reverse key order, yielding `key, value`.
84
+ #
85
+ # @param query [Hash] optional query keywords, as on {#each_pair}
86
+ # @yieldparam key [String]
87
+ # @yieldparam value [Object]
88
+ # @return [Enumerator, void]
89
+ def reverse_each_pair(**query, &block) = traverse(:scan, :backward, query, &block)
90
+
91
+ # Traverses the live keys in key order, yielding each key (mirrors
92
+ # +Hash#each_key+). The key scan decodes no values, so a message-backed map
93
+ # fetches no Kafka messages. It can still read the store. Without a block,
94
+ # it returns an {Enumerator} that reads on demand. The map has no +keys+
95
+ # array, because it would load the whole remote keyset. The block form
96
+ # returns +nil+, as {#each_pair} does.
97
+ #
98
+ # @param query [Hash] optional query keywords, as on {#each_pair}
99
+ # @yieldparam key [String]
100
+ # @return [Enumerator, void]
101
+ def each_key(**query, &block) = traverse(:keys, :forward, query, &block)
102
+
103
+ # Traverses the live keys in reverse key order, yielding each key.
104
+ #
105
+ # @param query [Hash] optional query keywords, as on {#each_pair}
106
+ # @yieldparam key [String]
107
+ # @return [Enumerator, void]
108
+ def reverse_each_key(**query, &block) = traverse(:keys, :backward, query, &block)
109
+
110
+ # Traverses the live values in key order (mirrors +Hash#each_value+).
111
+ #
112
+ # @param query [Hash] optional query keywords, as on {#each_pair}
113
+ # @yieldparam value [Object]
114
+ # @return [Enumerator, void]
115
+ def each_value(**query, &block) = traverse_values(:forward, query, &block)
116
+
117
+ # Traverses the live values in reverse key order.
118
+ #
119
+ # @param query [Hash] optional query keywords, as on {#each_pair}
120
+ # @yieldparam value [Object]
121
+ # @return [Enumerator, void]
122
+ def reverse_each_value(**query, &block) = traverse_values(:backward, query, &block)
123
+
124
+ # --- idiomatic Hash-style aliases and conveniences ------------------
125
+ # Each is composed from the canonical ops above and adds no capability
126
+ # the naming matrix lacks. Bounded reads only: there is deliberately no
127
+ # +keys+/+values+/+to_h+/+count+ or +Enumerable+, which would materialize
128
+ # the whole (potentially unbounded) remote collection.
129
+
130
+ # Reads +key+. Idiomatic alias of {#get} (mirrors +Hash#[]+).
131
+ alias_method :[], :get
132
+
133
+ # Writes +key+. Idiomatic alias of {#set} (mirrors +Hash#[]=+). As with any
134
+ # Ruby +[]=+, `map[key] = value` evaluates to +value+ regardless of return.
135
+ alias_method :[]=, :set
136
+
137
+ # Writes +key+, returning the stored +value+ (mirrors +Hash#store+). A
138
+ # wrapper, not an alias: a caller sees the return value of +store+, and
139
+ # the native write returns +nil+.
140
+ #
141
+ # @param key [String]
142
+ # @param value [Object]
143
+ # @return [Object] the stored +value+
144
+ def store(key, value)
145
+ set(key, value)
146
+ value
147
+ end
148
+
149
+ # Traverses live entries in key order. Idiomatic alias of {#each_pair}
150
+ # (mirrors +Hash#each+).
151
+ alias_method :each, :each_pair
152
+
153
+ # Reads several keys positionally (mirrors +Hash#values_at+).
154
+ #
155
+ # @param keys [Array<String>] the keys to read
156
+ # @return [Array<Object, nil>] one result per key; +nil+ for absent keys
157
+ def values_at(*keys) = get_many(keys)
158
+
159
+ # Reads +key+, raising or defaulting when absent (mirrors +Hash#fetch+).
160
+ # Performs a single read; a +nil+ result is unambiguously "absent" under
161
+ # the null ban.
162
+ #
163
+ # @param key [String]
164
+ # @param default [Object] returned when +key+ is absent
165
+ # @yieldparam key [String] called (instead of +default+) when +key+ is absent
166
+ # @return [Object]
167
+ # @raise [KeyError] when +key+ is absent and no default or block is given
168
+ def fetch(key, *default, &block)
169
+ if default.length > 1
170
+ raise ArgumentError, "wrong number of arguments (given #{default.length + 1}, expected 1..2)"
171
+ end
172
+ warn "warning: block supersedes default value argument" if block && !default.empty?
173
+
174
+ value = @native.get(key)
175
+ return value unless value.nil?
176
+ return block.call(key) if block
177
+ raise KeyError.new("key not found: #{key.inspect}", key: key, receiver: self) if default.empty?
178
+
179
+ # Steep merges the overloads, so it cannot type the default as D.
180
+ default.fetch(0) #: untyped
181
+ end
182
+
183
+ # Whether +key+ has a live value (mirrors +Hash#key?+). The check decodes
184
+ # no value, so a message-backed map fetches no Kafka message. It returns
185
+ # +true+ for an entry whose message cannot be fetched. A cache miss can
186
+ # still read the store.
187
+ #
188
+ # @param key [String]
189
+ # @return [Boolean]
190
+ def key?(key) = @native.contains_key(key)
191
+ alias_method :has_key?, :key?
192
+ alias_method :include?, :key?
193
+ alias_method :member?, :key?
194
+
195
+ # Reads +key+ and digs into the nested value (mirrors +Hash#dig+). A single
196
+ # bounded read; digging continues in the returned local value.
197
+ #
198
+ # @param key [String]
199
+ # @return [Object, nil]
200
+ # @raise [TypeError] if a nested value does not respond to +dig+
201
+ def dig(key, *rest)
202
+ value = @native.get(key)
203
+ return value if rest.empty? || value.nil?
204
+
205
+ unless value.respond_to?(:dig)
206
+ raise TypeError, "#{value.class} does not have #dig method"
207
+ end
208
+
209
+ value.dig(*rest)
210
+ end
211
+
212
+ # Reads +keys+ as a single bounded batch, returning a +Hash+ of only the
213
+ # keys that are present (mirrors +Hash#slice+). Absent keys are omitted.
214
+ #
215
+ # @param keys [Array<String>] the keys to read
216
+ # @return [Hash{String => Object}] present keys mapped to their values
217
+ def slice(*keys) = keys.zip(get_many(keys)).to_h.compact
218
+
219
+ # Reads +keys+ as a single bounded batch, requiring every key to be present
220
+ # (mirrors +Hash#fetch_values+). Without a block, a missing key raises
221
+ # {KeyError}; with a block, the block is called with each missing key and
222
+ # its result substituted.
223
+ #
224
+ # @param keys [Array<String>] the keys to read, in order
225
+ # @yieldparam key [String] called for each absent key
226
+ # @return [Array<Object>] one value per key, in order
227
+ # @raise [KeyError] when a key is absent and no block is given
228
+ def fetch_values(*keys, &block)
229
+ keys.zip(get_many(keys)).map do |key, value|
230
+ case value
231
+ when nil
232
+ next block.call(key) if block
233
+
234
+ raise KeyError.new("key not found: #{key.inspect}", key: key, receiver: self)
235
+ else
236
+ value
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ # A read-only view of a published map, opened by
243
+ # +client.state(subsystem, definition)+. Each read takes the user key and
244
+ # sees only committed state. A failed read raises the same state errors as
245
+ # an owned handle. A read in a forked child process raises +RuntimeError+.
246
+ class PublishedMap
247
+ include State::Scanning
248
+
249
+ # @param native [Prosody::NativePublishedMap] the native reader
250
+ def initialize(native) = @native = native
251
+
252
+ # Reads the committed entry for +map_key+ in the map for +key+.
253
+ #
254
+ # @return [Object, nil] the value, or +nil+ when the entry is absent
255
+ def get(key, map_key) = @native.get(key, map_key)
256
+
257
+ # Reads several entries of the map for +key+ in one batch.
258
+ #
259
+ # @return [Array<Object, nil>] one result per map key; +nil+ for an absent entry
260
+ def get_many(key, map_keys) = @native.get_many(key, map_keys)
261
+
262
+ # Whether the map for +key+ has a committed entry for +map_key+.
263
+ #
264
+ # @return [Boolean]
265
+ def key?(key, map_key) = @native.contains_key(key, map_key)
266
+ alias_method :has_key?, :key?
267
+ alias_method :include?, :key?
268
+ alias_method :member?, :key?
269
+
270
+ # Tests several entries of the map for +key+ in one batch.
271
+ #
272
+ # @return [Array<Boolean>] one result per map key
273
+ def contains_many(key, map_keys) = @native.contains_many(key, map_keys)
274
+
275
+ # Whether the map for +key+ has no committed entries.
276
+ #
277
+ # @return [Boolean]
278
+ def empty?(key) = @native.is_empty(key)
279
+
280
+ # Traverses the committed entries for +key+ in key order, yielding one
281
+ # +[map_key, value]+ pair for each entry. Each traversal accepts the query
282
+ # keywords documented on {State::Scanning}.
283
+ #
284
+ # @return [Enumerator, void]
285
+ def each_pair(key, **query, &block) = traverse(:scan, key, :forward, query, &block)
286
+
287
+ # Traverses the committed entries for +key+ in reverse key order.
288
+ #
289
+ # @return [Enumerator, void]
290
+ def reverse_each_pair(key, **query, &block) = traverse(:scan, key, :backward, query, &block)
291
+
292
+ # Traverses the committed map keys for +key+ in key order.
293
+ #
294
+ # @return [Enumerator, void]
295
+ def each_key(key, **query, &block) = traverse(:keys, key, :forward, query, &block)
296
+
297
+ # Traverses the committed map keys for +key+ in reverse key order.
298
+ #
299
+ # @return [Enumerator, void]
300
+ def reverse_each_key(key, **query, &block) = traverse(:keys, key, :backward, query, &block)
301
+
302
+ # Traverses the committed values for +key+ in key order.
303
+ #
304
+ # @return [Enumerator, void]
305
+ def each_value(key, **query, &block) = traverse_values(key, :forward, query, &block)
306
+
307
+ # Traverses the committed values for +key+ in reverse key order.
308
+ #
309
+ # @return [Enumerator, void]
310
+ def reverse_each_value(key, **query, &block) = traverse_values(key, :backward, query, &block)
311
+ alias_method :each, :each_pair
312
+ end
313
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosody
4
+ # A presence-only ordered set of String members, vended by
5
+ # +context.state(Prosody.set(...))+. It mirrors Ruby's +Set+.
6
+ #
7
+ # Writes are buffered and made durable when the handler succeeds or by
8
+ # {#commit}. Every operation yields the fiber, never the thread. There is
9
+ # deliberately no +Enumerable+ or +to_a+: they would read the whole remote
10
+ # set. Traverse members with {#each} or {#reverse_each}.
11
+ class SetState < State::Handle
12
+ include State::Scanning
13
+
14
+ # Adds +member+ (mirrors +Set#add+).
15
+ #
16
+ # @param member [String]
17
+ # @return [self]
18
+ def add(member)
19
+ @native.insert(member)
20
+ self
21
+ end
22
+
23
+ alias_method :<<, :add
24
+
25
+ # Removes +member+ when present (mirrors +Set#delete+).
26
+ #
27
+ # @param member [String]
28
+ # @return [self]
29
+ def delete(member)
30
+ @native.remove(member)
31
+ self
32
+ end
33
+
34
+ # Whether +member+ belongs to the set (mirrors +Set#include?+).
35
+ #
36
+ # @param member [String]
37
+ # @return [Boolean]
38
+ def include?(member) = @native.contains(member)
39
+
40
+ alias_method :member?, :include?
41
+
42
+ # Tests several members in a single batch.
43
+ #
44
+ # @param members [Array<String>] the members to test, in order
45
+ # @return [Array<Boolean>] one result per input member
46
+ def contains_many(members) = @native.contains_many(members)
47
+
48
+ # Whether the set has no live members (mirrors +Set#empty?+).
49
+ #
50
+ # @return [Boolean]
51
+ def empty? = @native.is_empty
52
+
53
+ # Removes every member (mirrors +Set#clear+).
54
+ #
55
+ # @return [self]
56
+ def clear
57
+ @native.clear
58
+ self
59
+ end
60
+
61
+ # Traverses the live members in ascending order.
62
+ #
63
+ # Without a block, returns an {Enumerator} over the native scan. Accepts
64
+ # the query keywords documented on {State::Scanning}, including
65
+ # +prefix:+.
66
+ #
67
+ # @param query [Hash] optional query keywords
68
+ # @yieldparam member [String]
69
+ # @return [Enumerator, void]
70
+ # @raise [ArgumentError, TypeError] if a query keyword is invalid
71
+ def each(**query, &block) = traverse(:keys, :forward, query, &block)
72
+
73
+ # Traverses the live members in descending order.
74
+ #
75
+ # @param query [Hash] optional query keywords, as on {#each}
76
+ # @yieldparam member [String]
77
+ # @return [Enumerator, void]
78
+ def reverse_each(**query, &block) = traverse(:keys, :backward, query, &block)
79
+ end
80
+
81
+ # A read-only view of a published set, opened by
82
+ # +client.state(subsystem, definition)+. Each read takes the user key and
83
+ # sees only committed state. A failed read raises the same state errors as
84
+ # an owned handle. A read in a forked child process raises +RuntimeError+.
85
+ class PublishedSet
86
+ include State::Scanning
87
+
88
+ # @param native [Prosody::NativePublishedSet] the native reader
89
+ def initialize(native)
90
+ @native = native
91
+ end
92
+
93
+ # Whether +member+ belongs to the committed set for +key+.
94
+ #
95
+ # @return [Boolean]
96
+ def include?(key, member) = @native.contains(key, member)
97
+
98
+ alias_method :member?, :include?
99
+
100
+ # Tests several members of the committed set for +key+ in one batch.
101
+ #
102
+ # @return [Array<Boolean>] one result per input member
103
+ def contains_many(key, members) = @native.contains_many(key, members)
104
+
105
+ # Whether the committed set for +key+ has no members.
106
+ #
107
+ # @return [Boolean]
108
+ def empty?(key) = @native.is_empty(key)
109
+
110
+ # Traverses the committed members for +key+ in ascending order. Accepts
111
+ # the query keywords documented on {State::Scanning}.
112
+ #
113
+ # @return [Enumerator, void]
114
+ def each(key, **query, &block) = traverse(:keys, key, :forward, query, &block)
115
+
116
+ # Traverses the committed members for +key+ in descending order.
117
+ #
118
+ # @return [Enumerator, void]
119
+ def reverse_each(key, **query, &block) = traverse(:keys, key, :backward, query, &block)
120
+ end
121
+ end