prosody 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/ARCHITECTURE.md +12 -2
- data/CHANGELOG.md +13 -0
- data/Cargo.lock +571 -435
- data/Cargo.toml +6 -6
- data/README.md +211 -20
- data/Rakefile +11 -1
- data/examples/keyed_state.rb +58 -0
- data/examples/keyed_state.rbs +18 -0
- data/examples/keyed_state_windowing.rb +47 -0
- data/examples/keyed_state_windowing.rbs +16 -0
- data/ext/prosody/Cargo.toml +1 -1
- data/ext/prosody/src/client/config.rs +387 -19
- data/ext/prosody/src/handler/context.rs +146 -5
- data/ext/prosody/src/handler/message.rs +17 -0
- data/ext/prosody/src/handler/mod.rs +4 -2
- data/ext/prosody/src/handler/state.rs +1035 -0
- data/lib/prosody/configuration.rb +26 -0
- data/lib/prosody/handler.rb +6 -2
- data/lib/prosody/native_stubs.rb +381 -6
- data/lib/prosody/state.rb +693 -0
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +5 -0
- data/release-please-config.json +4 -0
- data/sig/configuration.rbs +24 -1
- data/sig/handler.rbs +7 -3
- data/sig/processor.rbs +28 -12
- data/sig/prosody.rbs +11 -6
- data/sig/sentry.rbs +6 -0
- data/sig/state.rbs +272 -0
- data/steep_expectations.yml +47 -0
- data/typecheck/payload_types.rb +43 -0
- data/typecheck/payload_types.rbs +20 -0
- data/typecheck_negative/payload_types.rb +16 -0
- data/typecheck_negative/payload_types.rbs +8 -0
- metadata +22 -9
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosody
|
|
4
|
+
# Base class for errors raised by keyed-state operations that will not
|
|
5
|
+
# succeed on retry (an unregistered collection name, an identity mismatch, a
|
|
6
|
+
# duplicate registration, an invalid TTL).
|
|
7
|
+
#
|
|
8
|
+
# It subclasses {PermanentError} so a rethrown state error is classified as
|
|
9
|
+
# permanent by the result bridge's `#permanent?` path with no bridge change.
|
|
10
|
+
#
|
|
11
|
+
# @see PermanentError
|
|
12
|
+
class PermanentStateError < PermanentError; end
|
|
13
|
+
|
|
14
|
+
# Base class for errors raised by keyed-state operations that may succeed on
|
|
15
|
+
# retry. Every caller/input mistake (a null write, a wrong item shape, an
|
|
16
|
+
# invalid index, an invalid direction token, an unrepresentable value) is
|
|
17
|
+
# transient so the message retries and stays visible rather than being
|
|
18
|
+
# discarded.
|
|
19
|
+
#
|
|
20
|
+
# It subclasses {TransientError} so a rethrown state error is classified as
|
|
21
|
+
# transient by the result bridge's `#permanent?` path with no bridge change.
|
|
22
|
+
#
|
|
23
|
+
# @see TransientError
|
|
24
|
+
class TransientStateError < TransientError; end
|
|
25
|
+
|
|
26
|
+
# Raised when a JSON `null` is written to a collection. `null` is not a
|
|
27
|
+
# storable value (it is indistinguishable from absence), so the write is
|
|
28
|
+
# rejected and the stored value is left untouched. Use `clear`/`delete` to
|
|
29
|
+
# express deletion instead.
|
|
30
|
+
#
|
|
31
|
+
# It is transient (a caller mistake), so it retries and stays visible.
|
|
32
|
+
#
|
|
33
|
+
# @see TransientStateError
|
|
34
|
+
class NullValueError < TransientStateError; end
|
|
35
|
+
|
|
36
|
+
# An immutable keyed-state collection definition.
|
|
37
|
+
#
|
|
38
|
+
# Definitions are frozen value objects produced by the {Prosody.value},
|
|
39
|
+
# {Prosody.map}, {Prosody.deque}, and their `message_*` siblings. A definition
|
|
40
|
+
# both serializes into `Configuration#state_collections` (via
|
|
41
|
+
# {#to_state_config}) so the collection is registered before subscribe, and
|
|
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
|
|
44
|
+
# Serializes this definition into the native-registration hash, omitting
|
|
45
|
+
# unset optionals so they fall back to the core defaults.
|
|
46
|
+
#
|
|
47
|
+
# @return [Hash] the registration hash for the native layer
|
|
48
|
+
def to_state_config
|
|
49
|
+
config = {name: name, kind: kind, payload: payload}
|
|
50
|
+
config[:ttl_seconds] = ttl_seconds unless ttl_seconds.nil?
|
|
51
|
+
config[:read_uncommitted] = read_uncommitted unless read_uncommitted.nil?
|
|
52
|
+
config[:keyset_limit] = keyset_limit unless keyset_limit.nil?
|
|
53
|
+
config[:capacity] = capacity unless capacity.nil?
|
|
54
|
+
config
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Defines a single-value JSON collection.
|
|
59
|
+
#
|
|
60
|
+
# @param name [#to_s] the collection name (unique within the client)
|
|
61
|
+
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
62
|
+
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
63
|
+
# @return [StateDefinition] a frozen definition
|
|
64
|
+
def self.value(name, ttl: nil, read_uncommitted: nil)
|
|
65
|
+
StateDefinition.new(name: name.to_s, kind: "value", payload: "json",
|
|
66
|
+
ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: nil)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Defines a `String`-keyed ordered map JSON collection.
|
|
70
|
+
#
|
|
71
|
+
# @param name [#to_s] the collection name (unique within the client)
|
|
72
|
+
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
73
|
+
# @param keyset_limit [Integer, nil] optional map-only keyset bound (`0..=4096`)
|
|
74
|
+
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
75
|
+
# @return [StateDefinition] a frozen definition
|
|
76
|
+
def self.map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)
|
|
77
|
+
StateDefinition.new(name: name.to_s, kind: "map", payload: "json",
|
|
78
|
+
ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: keyset_limit, capacity: nil)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Defines a deque JSON collection.
|
|
82
|
+
#
|
|
83
|
+
# @param name [#to_s] the collection name (unique within the client)
|
|
84
|
+
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
85
|
+
# @param capacity [Integer, nil] optional window bound (at least 1); the
|
|
86
|
+
# deque keeps at most this many slots, enforced lazily on push. Runtime-only
|
|
87
|
+
# and mutable across deploys, never persisted (see {DequeState#push}).
|
|
88
|
+
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
89
|
+
# @return [StateDefinition] a frozen definition
|
|
90
|
+
def self.deque(name, ttl: nil, capacity: nil, read_uncommitted: nil)
|
|
91
|
+
StateDefinition.new(name: name.to_s, kind: "deque", payload: "json",
|
|
92
|
+
ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: capacity)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Defines a single-value Kafka-message collection (items are full messages).
|
|
96
|
+
#
|
|
97
|
+
# @param name [#to_s] the collection name (unique within the client)
|
|
98
|
+
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
99
|
+
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
100
|
+
# @return [StateDefinition] a frozen definition
|
|
101
|
+
def self.message_value(name, ttl: nil, read_uncommitted: nil)
|
|
102
|
+
StateDefinition.new(name: name.to_s, kind: "value", payload: "message",
|
|
103
|
+
ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: nil)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Defines a `String`-keyed ordered map Kafka-message collection.
|
|
107
|
+
#
|
|
108
|
+
# @param name [#to_s] the collection name (unique within the client)
|
|
109
|
+
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
110
|
+
# @param keyset_limit [Integer, nil] optional map-only keyset bound (`0..=4096`)
|
|
111
|
+
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
112
|
+
# @return [StateDefinition] a frozen definition
|
|
113
|
+
def self.message_map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)
|
|
114
|
+
StateDefinition.new(name: name.to_s, kind: "map", payload: "message",
|
|
115
|
+
ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: keyset_limit, capacity: nil)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Defines a deque Kafka-message collection.
|
|
119
|
+
#
|
|
120
|
+
# @param name [#to_s] the collection name (unique within the client)
|
|
121
|
+
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
122
|
+
# @param capacity [Integer, nil] optional window bound (at least 1); the
|
|
123
|
+
# deque keeps at most this many slots, enforced lazily on push. Runtime-only
|
|
124
|
+
# and mutable across deploys, never persisted (see {DequeState#push}).
|
|
125
|
+
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
126
|
+
# @return [StateDefinition] a frozen definition
|
|
127
|
+
def self.message_deque(name, ttl: nil, capacity: nil, read_uncommitted: nil)
|
|
128
|
+
StateDefinition.new(name: name.to_s, kind: "deque", payload: "message",
|
|
129
|
+
ttl_seconds: ttl, read_uncommitted: read_uncommitted, keyset_limit: nil, capacity: capacity)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Internal routing tables shared by the state wrappers.
|
|
133
|
+
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
|
|
144
|
+
|
|
145
|
+
# Adds keyed-state vending to the native context. Included into
|
|
146
|
+
# {Prosody::Context}; kept as a module so the routing can be exercised
|
|
147
|
+
# against a stand-in receiver.
|
|
148
|
+
module Vending
|
|
149
|
+
# Vends the typed keyed-state handle for `definition`.
|
|
150
|
+
#
|
|
151
|
+
# Handles are cached per context by kind, payload, and name, so repeated
|
|
152
|
+
# vends within one handler invocation return the same wrapper.
|
|
153
|
+
#
|
|
154
|
+
# @param definition [StateDefinition] a frozen collection definition
|
|
155
|
+
# @return [ValueState, MapState, DequeState] the typed handle
|
|
156
|
+
# @raise [TransientStateError] if the definition's kind/payload is unknown
|
|
157
|
+
# @raise [PermanentStateError] if the collection name is unregistered or
|
|
158
|
+
# its registered identity mismatches
|
|
159
|
+
def state(definition)
|
|
160
|
+
cache = (@state_handles ||= {})
|
|
161
|
+
cache_key = "#{definition.kind}:#{definition.payload}:#{definition.name}"
|
|
162
|
+
return cache[cache_key] if cache.key?(cache_key)
|
|
163
|
+
|
|
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)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Shared cursor-driving for the explicit-traversal handles. Folds the
|
|
174
|
+
# identical native-scan open/close/exhaustion loop; each handle supplies
|
|
175
|
+
# only the per-item yield shape through the block. Kept private (mixed into
|
|
176
|
+
# the handle classes) since it is not part of the public surface.
|
|
177
|
+
module Scanning
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
# Opens a native scan in `direction`, yields each item, and closes the
|
|
181
|
+
# scan via `ensure` on stop or exception. Direction validity is enforced
|
|
182
|
+
# by the native layer (an invalid token is rejected transient there); the
|
|
183
|
+
# public traversal methods only ever pass `:forward`/`:backward`. `opener`
|
|
184
|
+
# selects the native cursor seam — the default `:scan` yields values (or
|
|
185
|
+
# `[key, value]` pairs), `:keys` yields bare map keys.
|
|
186
|
+
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
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# A single-value keyed-state handle.
|
|
204
|
+
#
|
|
205
|
+
# Reads return the stored JSON value (or a {Prosody::Message} for message
|
|
206
|
+
# collections), or `nil` when the value is absent. Writes are buffered and
|
|
207
|
+
# made durable by {#commit}. All operations are fiber-yield async: they look
|
|
208
|
+
# blocking but never block the thread.
|
|
209
|
+
class ValueState
|
|
210
|
+
# @param native [Prosody::NativeValueState] the vended native handle
|
|
211
|
+
def initialize(native)
|
|
212
|
+
@native = native
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Reads the current value.
|
|
216
|
+
#
|
|
217
|
+
# @return [Object, nil] the stored value, or `nil` when absent
|
|
218
|
+
def get = @native.get
|
|
219
|
+
|
|
220
|
+
# Buffers a write of the value.
|
|
221
|
+
#
|
|
222
|
+
# @param value [Object] the value to store (JSON, or a message)
|
|
223
|
+
# @return [void]
|
|
224
|
+
# @raise [NullValueError] if `value` is `nil` (use {#clear} to delete)
|
|
225
|
+
def set(value) = @native.set(value)
|
|
226
|
+
|
|
227
|
+
# Buffers a clear of the value.
|
|
228
|
+
#
|
|
229
|
+
# @return [void]
|
|
230
|
+
def clear = @native.clear
|
|
231
|
+
|
|
232
|
+
# Durably commits the buffered operations mid-handler.
|
|
233
|
+
#
|
|
234
|
+
# @return [nil] the erased FFI seam drops the applied/no-op outcome
|
|
235
|
+
def commit = @native.commit
|
|
236
|
+
|
|
237
|
+
# Discards the buffered uncommitted operations.
|
|
238
|
+
#
|
|
239
|
+
# @return [nil]
|
|
240
|
+
def rollback = @native.rollback
|
|
241
|
+
|
|
242
|
+
# Reads the current value. Idiomatic alias of {#get}.
|
|
243
|
+
#
|
|
244
|
+
# @return [Object, nil]
|
|
245
|
+
alias_method :value, :get
|
|
246
|
+
|
|
247
|
+
# Buffers a write of the value. Idiomatic alias of {#set}. As with any Ruby
|
|
248
|
+
# writer, `state.value = x` evaluates to `x` regardless of the return.
|
|
249
|
+
#
|
|
250
|
+
# @param value [Object]
|
|
251
|
+
# @return [void]
|
|
252
|
+
alias_method :value=, :set
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# A `String`-keyed ordered-map keyed-state handle.
|
|
256
|
+
#
|
|
257
|
+
# Traversal is explicit: {#each_pair}/{#reverse_each_pair} yield `key, value`
|
|
258
|
+
# pairs over a native scan, closing the scan via `ensure`. No aggregate-mixin
|
|
259
|
+
# methods are provided — they would silently materialize an unbounded remote
|
|
260
|
+
# collection.
|
|
261
|
+
class MapState
|
|
262
|
+
include State::Scanning
|
|
263
|
+
|
|
264
|
+
# @param native [Prosody::NativeMapState] the vended native handle
|
|
265
|
+
def initialize(native)
|
|
266
|
+
@native = native
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Reads the value for `key`.
|
|
270
|
+
#
|
|
271
|
+
# @param key [String] the map key
|
|
272
|
+
# @return [Object, nil] the value, or `nil` when the key is absent
|
|
273
|
+
def get(key) = @native.get(key)
|
|
274
|
+
|
|
275
|
+
# Reads several keys in a single isolated batch.
|
|
276
|
+
#
|
|
277
|
+
# @param keys [Array<String>] the keys to read, in order
|
|
278
|
+
# @return [Array<Object, nil>] one result per input key; `nil` for absent keys
|
|
279
|
+
def get_many(keys) = @native.get_many(keys)
|
|
280
|
+
|
|
281
|
+
# Inserts or overwrites `key`.
|
|
282
|
+
#
|
|
283
|
+
# @param key [String] the map key
|
|
284
|
+
# @param value [Object] the value to store (JSON, or a message)
|
|
285
|
+
# @return [void]
|
|
286
|
+
# @raise [NullValueError] if `value` is `nil` (use {#delete} to remove)
|
|
287
|
+
def set(key, value) = @native.set(key, value)
|
|
288
|
+
|
|
289
|
+
# Removes `key`.
|
|
290
|
+
#
|
|
291
|
+
# Documented divergence from `Hash#delete`: this returns `nil`, never the
|
|
292
|
+
# removed value (the erased FFI seam does not surface it).
|
|
293
|
+
#
|
|
294
|
+
# @param key [String] the map key
|
|
295
|
+
# @return [nil]
|
|
296
|
+
def delete(key)
|
|
297
|
+
@native.remove(key)
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Removes every entry.
|
|
302
|
+
#
|
|
303
|
+
# @return [void]
|
|
304
|
+
def clear = @native.clear
|
|
305
|
+
|
|
306
|
+
# Durably commits the buffered operations mid-handler.
|
|
307
|
+
#
|
|
308
|
+
# @return [nil] the erased FFI seam drops the applied/no-op outcome
|
|
309
|
+
def commit = @native.commit
|
|
310
|
+
|
|
311
|
+
# Discards the buffered uncommitted operations.
|
|
312
|
+
#
|
|
313
|
+
# @return [nil]
|
|
314
|
+
def rollback = @native.rollback
|
|
315
|
+
|
|
316
|
+
# Traverses the live entries in key order, yielding `key, value`.
|
|
317
|
+
#
|
|
318
|
+
# Without a block, returns an {Enumerator} over the native scan. Each step
|
|
319
|
+
# fiber-yields; the scan is closed via `ensure` on stop or exception. The
|
|
320
|
+
# enumerator is valid only within the current handler invocation.
|
|
321
|
+
#
|
|
322
|
+
# @yieldparam key [String]
|
|
323
|
+
# @yieldparam value [Object]
|
|
324
|
+
# @return [Enumerator, void]
|
|
325
|
+
def each_pair(&block) = traverse(:forward, &block)
|
|
326
|
+
|
|
327
|
+
# Traverses the live entries in reverse key order, yielding `key, value`.
|
|
328
|
+
#
|
|
329
|
+
# @yieldparam key [String]
|
|
330
|
+
# @yieldparam value [Object]
|
|
331
|
+
# @return [Enumerator, void]
|
|
332
|
+
def reverse_each_pair(&block) = traverse(:backward, &block)
|
|
333
|
+
|
|
334
|
+
# Traverses the live keys in key order, yielding each key (mirrors
|
|
335
|
+
# +Hash#each_key+). The key scan skips value decode and the resolver — a
|
|
336
|
+
# message-backed map yields keys with zero Kafka fetches, though not
|
|
337
|
+
# zero-I/O. Without a block, returns a demand-driven {Enumerator}; there is
|
|
338
|
+
# deliberately no eager +keys+ array (it would materialize the whole remote
|
|
339
|
+
# keyset). Mirrors {#each_pair}'s block-form return (+nil+), not stdlib's
|
|
340
|
+
# +self+, for in-repo sibling consistency.
|
|
341
|
+
#
|
|
342
|
+
# @yieldparam key [String]
|
|
343
|
+
# @return [Enumerator, void]
|
|
344
|
+
def each_key(&block) = traverse_keys(:forward, &block)
|
|
345
|
+
|
|
346
|
+
# Traverses the live keys in reverse key order, yielding each key.
|
|
347
|
+
#
|
|
348
|
+
# @yieldparam key [String]
|
|
349
|
+
# @return [Enumerator, void]
|
|
350
|
+
def reverse_each_key(&block) = traverse_keys(:backward, &block)
|
|
351
|
+
|
|
352
|
+
# --- idiomatic Hash-style aliases and conveniences ------------------
|
|
353
|
+
# Each is composed from the canonical ops above and adds no capability
|
|
354
|
+
# the naming matrix lacks. Bounded reads only: there is deliberately no
|
|
355
|
+
# +keys+/+values+/+to_h+/+count+ or +Enumerable+, which would materialize
|
|
356
|
+
# the whole (potentially unbounded) remote collection.
|
|
357
|
+
|
|
358
|
+
# Reads +key+. Idiomatic alias of {#get} (mirrors +Hash#[]+).
|
|
359
|
+
alias_method :[], :get
|
|
360
|
+
|
|
361
|
+
# Writes +key+. Idiomatic alias of {#set} (mirrors +Hash#[]=+). As with any
|
|
362
|
+
# Ruby +[]=+, `map[key] = value` evaluates to +value+ regardless of return.
|
|
363
|
+
alias_method :[]=, :set
|
|
364
|
+
|
|
365
|
+
# Writes +key+, returning the stored +value+ (mirrors +Hash#store+). A
|
|
366
|
+
# wrapper, not an alias: unlike +[]=+, +store+ is called normally, so its
|
|
367
|
+
# return is observed — and the native write returns +nil+.
|
|
368
|
+
#
|
|
369
|
+
# @param key [String]
|
|
370
|
+
# @param value [Object]
|
|
371
|
+
# @return [Object] the stored +value+
|
|
372
|
+
def store(key, value)
|
|
373
|
+
set(key, value)
|
|
374
|
+
value
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Traverses live entries in key order. Idiomatic alias of {#each_pair}
|
|
378
|
+
# (mirrors +Hash#each+).
|
|
379
|
+
alias_method :each, :each_pair
|
|
380
|
+
|
|
381
|
+
# Reads several keys positionally (mirrors +Hash#values_at+).
|
|
382
|
+
#
|
|
383
|
+
# @param keys [Array<String>] the keys to read
|
|
384
|
+
# @return [Array<Object, nil>] one result per key; +nil+ for absent keys
|
|
385
|
+
def values_at(*keys) = get_many(keys)
|
|
386
|
+
|
|
387
|
+
# Reads +key+, raising or defaulting when absent (mirrors +Hash#fetch+).
|
|
388
|
+
# Performs a single read; a +nil+ result is unambiguously "absent" under
|
|
389
|
+
# the null ban.
|
|
390
|
+
#
|
|
391
|
+
# @param key [String]
|
|
392
|
+
# @param default [Object] returned when +key+ is absent
|
|
393
|
+
# @yieldparam key [String] called (instead of +default+) when +key+ is absent
|
|
394
|
+
# @return [Object]
|
|
395
|
+
# @raise [KeyError] when +key+ is absent and no default or block is given
|
|
396
|
+
def fetch(key, *default, &block)
|
|
397
|
+
if default.length > 1
|
|
398
|
+
raise ArgumentError, "wrong number of arguments (given #{default.length + 1}, expected 1..2)"
|
|
399
|
+
end
|
|
400
|
+
warn "warning: block supersedes default value argument" if block && !default.empty?
|
|
401
|
+
|
|
402
|
+
value = @native.get(key)
|
|
403
|
+
return value unless value.nil?
|
|
404
|
+
return block.call(key) if block
|
|
405
|
+
return default.first unless default.empty?
|
|
406
|
+
|
|
407
|
+
raise KeyError.new("key not found: #{key.inspect}", key: key, receiver: self)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Whether +key+ has a live value (mirrors +Hash#key?+). A presence check:
|
|
411
|
+
# no value decode and no resolver run (not no-I/O). A message-backed map
|
|
412
|
+
# answers presence with zero Kafka fetches — +true+ even for a
|
|
413
|
+
# present-but-unfetchable cell — though a cache miss may still touch the
|
|
414
|
+
# store.
|
|
415
|
+
#
|
|
416
|
+
# @param key [String]
|
|
417
|
+
# @return [Boolean]
|
|
418
|
+
def key?(key) = @native.contains_key(key)
|
|
419
|
+
alias_method :has_key?, :key?
|
|
420
|
+
alias_method :include?, :key?
|
|
421
|
+
alias_method :member?, :key?
|
|
422
|
+
|
|
423
|
+
# Reads +key+ and digs into the nested value (mirrors +Hash#dig+). A single
|
|
424
|
+
# bounded read; digging continues in the returned local value.
|
|
425
|
+
#
|
|
426
|
+
# @param key [String]
|
|
427
|
+
# @return [Object, nil]
|
|
428
|
+
# @raise [TypeError] if a nested value does not respond to +dig+
|
|
429
|
+
def dig(key, *rest)
|
|
430
|
+
value = @native.get(key)
|
|
431
|
+
return value if rest.empty? || value.nil?
|
|
432
|
+
|
|
433
|
+
unless value.respond_to?(:dig)
|
|
434
|
+
raise TypeError, "#{value.class} does not have #dig method"
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
value.dig(*rest)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Reads +keys+ as a single bounded batch, returning a +Hash+ of only the
|
|
441
|
+
# keys that are present (mirrors +Hash#slice+). Absent keys are omitted.
|
|
442
|
+
#
|
|
443
|
+
# @param keys [Array<String>] the keys to read
|
|
444
|
+
# @return [Hash{String => Object}] present keys mapped to their values
|
|
445
|
+
def slice(*keys)
|
|
446
|
+
result = {}
|
|
447
|
+
keys.zip(get_many(keys)) do |key, value|
|
|
448
|
+
result[key] = value unless value.nil?
|
|
449
|
+
end
|
|
450
|
+
result
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Reads +keys+ as a single bounded batch, requiring every key to be present
|
|
454
|
+
# (mirrors +Hash#fetch_values+). Without a block, a missing key raises
|
|
455
|
+
# {KeyError}; with a block, the block is called with each missing key and
|
|
456
|
+
# its result substituted.
|
|
457
|
+
#
|
|
458
|
+
# @param keys [Array<String>] the keys to read, in order
|
|
459
|
+
# @yieldparam key [String] called for each absent key
|
|
460
|
+
# @return [Array<Object>] one value per key, in order
|
|
461
|
+
# @raise [KeyError] when a key is absent and no block is given
|
|
462
|
+
def fetch_values(*keys, &block)
|
|
463
|
+
keys.zip(get_many(keys)).map do |key, value|
|
|
464
|
+
next value unless value.nil?
|
|
465
|
+
next block.call(key) if block
|
|
466
|
+
|
|
467
|
+
raise KeyError.new("key not found: #{key.inspect}", key: key, receiver: self)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
private
|
|
472
|
+
|
|
473
|
+
def traverse(direction)
|
|
474
|
+
return enum_for(:traverse, direction) unless block_given?
|
|
475
|
+
|
|
476
|
+
# Yield the [key, value] pair as a single Array, matching Hash#each_pair:
|
|
477
|
+
# a two-parameter block auto-splats it (|k, v|), a one-parameter block
|
|
478
|
+
# receives the pair (|pair|), and the no-block Enumerator yields pairs.
|
|
479
|
+
scan_each(direction) { |pair| yield pair }
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def traverse_keys(direction)
|
|
483
|
+
return enum_for(:traverse_keys, direction) unless block_given?
|
|
484
|
+
|
|
485
|
+
scan_each(direction, :keys) { |key| yield key }
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# A deque keyed-state handle.
|
|
490
|
+
#
|
|
491
|
+
# Traversal is explicit: {#each}/{#reverse_each} yield single elements over a
|
|
492
|
+
# native scan, closing the scan via `ensure`. No aggregate-mixin methods are
|
|
493
|
+
# provided.
|
|
494
|
+
class DequeState
|
|
495
|
+
include State::Scanning
|
|
496
|
+
|
|
497
|
+
# @param native [Prosody::NativeDequeState] the vended native handle
|
|
498
|
+
def initialize(native)
|
|
499
|
+
@native = native
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Appends an element at the back.
|
|
503
|
+
#
|
|
504
|
+
# @param value [Object] the element (JSON, or a message)
|
|
505
|
+
# @return [void]
|
|
506
|
+
# @raise [NullValueError] if `value` is `nil`
|
|
507
|
+
def push(value) = @native.push_back(value)
|
|
508
|
+
|
|
509
|
+
# Prepends an element at the front.
|
|
510
|
+
#
|
|
511
|
+
# @param value [Object] the element (JSON, or a message)
|
|
512
|
+
# @return [void]
|
|
513
|
+
# @raise [NullValueError] if `value` is `nil`
|
|
514
|
+
def unshift(value) = @native.push_front(value)
|
|
515
|
+
|
|
516
|
+
# Removes and returns the back element.
|
|
517
|
+
#
|
|
518
|
+
# @return [Object, nil] the removed element, or `nil` when empty
|
|
519
|
+
def pop = @native.pop_back
|
|
520
|
+
|
|
521
|
+
# Removes and returns the front element.
|
|
522
|
+
#
|
|
523
|
+
# @return [Object, nil] the removed element, or `nil` when empty
|
|
524
|
+
def shift = @native.pop_front
|
|
525
|
+
|
|
526
|
+
# The number of live elements.
|
|
527
|
+
#
|
|
528
|
+
# @return [Integer]
|
|
529
|
+
def length = @native.len
|
|
530
|
+
|
|
531
|
+
alias_method :size, :length
|
|
532
|
+
|
|
533
|
+
# Whether the deque holds no live elements.
|
|
534
|
+
#
|
|
535
|
+
# @return [Boolean]
|
|
536
|
+
def empty? = @native.is_empty
|
|
537
|
+
|
|
538
|
+
# Removes every element.
|
|
539
|
+
#
|
|
540
|
+
# @return [void]
|
|
541
|
+
def clear = @native.clear
|
|
542
|
+
|
|
543
|
+
# Durably commits the buffered operations mid-handler.
|
|
544
|
+
#
|
|
545
|
+
# @return [nil] the erased FFI seam drops the applied/no-op outcome
|
|
546
|
+
def commit = @native.commit
|
|
547
|
+
|
|
548
|
+
# Discards the buffered uncommitted operations.
|
|
549
|
+
#
|
|
550
|
+
# @return [nil]
|
|
551
|
+
def rollback = @native.rollback
|
|
552
|
+
|
|
553
|
+
# Reads the element at `index`, resolving negatives Array-style (mirrors
|
|
554
|
+
# +Array#[]+'s read domain, without the indexer). A non-negative index
|
|
555
|
+
# reads from the front; `-1` is the back element, `-n` the nth from the end.
|
|
556
|
+
# `-1` fast-paths through {#last} (no length read); other negatives resolve
|
|
557
|
+
# against the current length (one length read + one element read),
|
|
558
|
+
# consistent because the deque has a single writer per attempt.
|
|
559
|
+
#
|
|
560
|
+
# @param index [Integer] the position (negative counts from the back)
|
|
561
|
+
# @return [Object, nil] the element, or `nil` outside the bounds
|
|
562
|
+
# @raise [TransientStateError] if `index` is not an Integer
|
|
563
|
+
def get(index)
|
|
564
|
+
unless index.is_a?(Integer)
|
|
565
|
+
raise TransientStateError, "get: index must be an Integer, got #{index.inspect}"
|
|
566
|
+
end
|
|
567
|
+
index.negative? ? at_negative(index) : @native.get(index)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Traverses the live elements in index order.
|
|
571
|
+
#
|
|
572
|
+
# Without a block, returns an {Enumerator} over the native scan. Each step
|
|
573
|
+
# fiber-yields; the scan is closed via `ensure` on stop or exception.
|
|
574
|
+
#
|
|
575
|
+
# @yieldparam element [Object]
|
|
576
|
+
# @return [Enumerator, void]
|
|
577
|
+
def each(&block) = traverse(:forward, &block)
|
|
578
|
+
|
|
579
|
+
# Traverses the live elements in reverse index order.
|
|
580
|
+
#
|
|
581
|
+
# @yieldparam element [Object]
|
|
582
|
+
# @return [Enumerator, void]
|
|
583
|
+
def reverse_each(&block) = traverse(:backward, &block)
|
|
584
|
+
|
|
585
|
+
# --- idiomatic Array-style conveniences -----------------------------
|
|
586
|
+
# Composed from the canonical ops above; bounded reads only (no +to_a+,
|
|
587
|
+
# +map+, +sort+, or +Enumerable+ that would materialize the whole deque).
|
|
588
|
+
#
|
|
589
|
+
# Deliberately NOT provided: +[]+ and +at+. This is a remote deque; +get+
|
|
590
|
+
# and +fetch+ accept a single +Integer+ index (negatives resolve from the
|
|
591
|
+
# back, Array-style), but wearing +Array+'s +[]+/+at+ would invite a range
|
|
592
|
+
# read (+deque[0..2]+) that cannot be honored. Use the explicit {#get}, or
|
|
593
|
+
# {#first}/{#last} for the ends.
|
|
594
|
+
|
|
595
|
+
# Prepends +value+, returning +self+ for chaining (mirrors +Array#prepend+).
|
|
596
|
+
# A wrapper, not an alias: the native write returns +nil+.
|
|
597
|
+
#
|
|
598
|
+
# @param value [Object]
|
|
599
|
+
# @return [self]
|
|
600
|
+
def prepend(value)
|
|
601
|
+
unshift(value)
|
|
602
|
+
self
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Appends +value+, returning +self+ for chaining (mirrors +Array#append+).
|
|
606
|
+
# A wrapper, not an alias: the native write returns +nil+.
|
|
607
|
+
#
|
|
608
|
+
# @param value [Object]
|
|
609
|
+
# @return [self]
|
|
610
|
+
def append(value)
|
|
611
|
+
push(value)
|
|
612
|
+
self
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Appends +value+ at the back and returns +self+ for chaining
|
|
616
|
+
# (mirrors +Array#<<+).
|
|
617
|
+
#
|
|
618
|
+
# @param value [Object]
|
|
619
|
+
# @return [self]
|
|
620
|
+
def <<(value)
|
|
621
|
+
push(value)
|
|
622
|
+
self
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# The front element, or +nil+ when empty (mirrors +Array#first+). An
|
|
626
|
+
# endpoint-slot read in one round trip (no length read). Under a TTL an
|
|
627
|
+
# expired front slot yields +nil+ even when live interior elements remain —
|
|
628
|
+
# a peek never searches inward.
|
|
629
|
+
#
|
|
630
|
+
# @return [Object, nil]
|
|
631
|
+
def first = @native.peek_front
|
|
632
|
+
|
|
633
|
+
# The back element, or +nil+ when empty (mirrors +Array#last+). An
|
|
634
|
+
# endpoint-slot read in one round trip (no length read); same TTL-hole
|
|
635
|
+
# semantics as {#first}.
|
|
636
|
+
#
|
|
637
|
+
# @return [Object, nil]
|
|
638
|
+
def last = @native.peek_back
|
|
639
|
+
|
|
640
|
+
# Reads the element at +index+, raising or defaulting when out of range
|
|
641
|
+
# (mirrors +Array#fetch+). A +nil+ result is unambiguously "out of range"
|
|
642
|
+
# under the null ban. Negatives resolve Array-style like {#get} — +-1+ is
|
|
643
|
+
# the back element, +-n+ the nth from the end; a fractional or non-Integer
|
|
644
|
+
# index is a caller mistake, rejected {TransientStateError}.
|
|
645
|
+
#
|
|
646
|
+
# @param index [Integer] the position (negative counts from the back)
|
|
647
|
+
# @param default [Object] returned when +index+ is out of range
|
|
648
|
+
# @yieldparam index [Integer] called (instead of +default+) when out of range
|
|
649
|
+
# @return [Object]
|
|
650
|
+
# @raise [IndexError] when out of range and no default or block is given
|
|
651
|
+
# @raise [TransientStateError] if +index+ is not an Integer
|
|
652
|
+
def fetch(index, *default, &block)
|
|
653
|
+
if default.length > 1
|
|
654
|
+
raise ArgumentError, "wrong number of arguments (given #{default.length + 1}, expected 1..2)"
|
|
655
|
+
end
|
|
656
|
+
unless index.is_a?(Integer)
|
|
657
|
+
raise TransientStateError, "fetch: index must be an Integer, got #{index.inspect}"
|
|
658
|
+
end
|
|
659
|
+
warn "warning: block supersedes default value argument" if block && !default.empty?
|
|
660
|
+
|
|
661
|
+
value = index.negative? ? at_negative(index) : @native.get(index)
|
|
662
|
+
return value unless value.nil?
|
|
663
|
+
return block.call(index) if block
|
|
664
|
+
return default.first unless default.empty?
|
|
665
|
+
|
|
666
|
+
raise IndexError, "index #{index} outside deque bounds"
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
private
|
|
670
|
+
|
|
671
|
+
# Resolves a negative Array-style index against the current length: +-1+
|
|
672
|
+
# fast-paths through {#last} (no length read), other negatives read the
|
|
673
|
+
# length and index from the front. Returns +nil+ when the index resolves
|
|
674
|
+
# before the front (past the far end of the deque).
|
|
675
|
+
def at_negative(index)
|
|
676
|
+
return @native.peek_back if index == -1
|
|
677
|
+
|
|
678
|
+
resolved = @native.len + index
|
|
679
|
+
resolved.negative? ? nil : @native.get(resolved)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def traverse(direction)
|
|
683
|
+
return enum_for(:traverse, direction) unless block_given?
|
|
684
|
+
|
|
685
|
+
scan_each(direction) { |item| yield item }
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
# Reopens the native context class to add keyed-state vending.
|
|
690
|
+
class Context
|
|
691
|
+
include State::Vending
|
|
692
|
+
end
|
|
693
|
+
end
|
data/lib/prosody/version.rb
CHANGED