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.
- checksums.yaml +4 -4
- data/.config/rail.toml +26 -0
- data/.release-please-manifest.json +1 -1
- data/.ruby-version +1 -1
- data/.taplo.toml +1 -1
- data/AGENTS.md +29 -15
- data/CHANGELOG.md +7 -0
- data/CONFIGURATION.md +19 -16
- data/Cargo.lock +254 -235
- data/Cargo.toml +11 -8
- data/README.md +100 -23
- data/examples/keyed_state.rb +10 -2
- data/examples/keyed_state.rbs +1 -0
- data/ext/prosody/Cargo.toml +1 -1
- data/ext/prosody/src/admin.rs +36 -36
- data/ext/prosody/src/bridge/mod.rs +5 -10
- data/ext/prosody/src/client/config/connections.rs +170 -0
- data/ext/prosody/src/client/config/middleware.rs +184 -0
- data/ext/prosody/src/client/config/mod.rs +396 -0
- data/ext/prosody/src/client/config/state.rs +323 -0
- data/ext/prosody/src/client/mod.rs +31 -117
- data/ext/prosody/src/client/readers.rs +106 -0
- data/ext/prosody/src/client/request.rs +2 -2
- data/ext/prosody/src/client/support.rs +13 -32
- data/ext/prosody/src/gvl.rs +8 -6
- data/ext/prosody/src/handler/{context.rs → context/mod.rs} +36 -150
- data/ext/prosody/src/handler/context/vending.rs +138 -0
- data/ext/prosody/src/handler/message.rs +36 -0
- data/ext/prosody/src/handler/mod.rs +18 -8
- data/ext/prosody/src/handler/state/deque.rs +144 -0
- data/ext/prosody/src/handler/state/mod.rs +163 -268
- data/ext/prosody/src/handler/state/query.rs +264 -0
- data/ext/prosody/src/handler/state/registration.rs +15 -86
- data/ext/prosody/src/handler/state/scan.rs +33 -141
- data/ext/prosody/src/handler/state/set.rs +98 -0
- data/ext/prosody/src/lib.rs +54 -36
- data/ext/prosody/src/logging.rs +6 -6
- data/ext/prosody/src/published.rs +171 -152
- data/ext/prosody/src/scheduler/result.rs +2 -1
- data/ext/prosody/src/util.rs +65 -3
- data/lib/prosody/client.rb +32 -0
- data/lib/prosody/configuration.rb +20 -12
- data/lib/prosody/demand.rb +27 -0
- data/lib/prosody/native_stubs/client.rb +157 -0
- data/lib/prosody/native_stubs/context.rb +178 -0
- data/lib/prosody/native_stubs/message.rb +133 -0
- data/lib/prosody/native_stubs.rb +11 -956
- data/lib/prosody/state/deque.rb +253 -0
- data/lib/prosody/state/map.rb +313 -0
- data/lib/prosody/state/set.rb +121 -0
- data/lib/prosody/state/value.rb +58 -0
- data/lib/prosody/state.rb +157 -677
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +2 -1
- data/sig/configuration.rbs +19 -10
- data/sig/prosody.rbs +37 -4
- data/sig/published.rbs +102 -0
- data/sig/state.rbs +109 -122
- data/typecheck/payload_types.rb +12 -0
- data/typecheck/payload_types.rbs +1 -0
- metadata +30 -11
- data/ext/prosody/src/client/config.rs +0 -1300
data/lib/prosody/state.rb
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module Prosody
|
|
4
4
|
# Base class for errors raised by keyed-state operations that will not
|
|
5
5
|
# succeed on retry (an unregistered collection name, an identity mismatch, a
|
|
6
|
-
# duplicate registration, an invalid TTL).
|
|
6
|
+
# duplicate registration, an invalid TTL, a JSON `null` write). A `null` is
|
|
7
|
+
# not a storable value. Use `clear` (value, deque) or `delete` (map) to
|
|
8
|
+
# remove one.
|
|
7
9
|
#
|
|
8
10
|
# It subclasses {PermanentError} so a rethrown state error is classified as
|
|
9
11
|
# permanent by the result bridge's `#permanent?` path with no bridge change.
|
|
@@ -12,10 +14,10 @@ module Prosody
|
|
|
12
14
|
class PermanentStateError < PermanentError; end
|
|
13
15
|
|
|
14
16
|
# Base class for errors raised by keyed-state operations that may succeed on
|
|
15
|
-
# retry. Every caller/input mistake
|
|
16
|
-
# invalid index, an invalid direction token, an unrepresentable
|
|
17
|
-
# transient so the message retries and stays visible rather than
|
|
18
|
-
# discarded.
|
|
17
|
+
# retry. Every caller/input mistake that the client detects (a wrong item
|
|
18
|
+
# shape, an invalid index, an invalid direction token, an unrepresentable
|
|
19
|
+
# value) is transient so the message retries and stays visible rather than
|
|
20
|
+
# being discarded.
|
|
19
21
|
#
|
|
20
22
|
# It subclasses {TransientError} so a rethrown state error is classified as
|
|
21
23
|
# transient by the result bridge's `#permanent?` path with no bridge change.
|
|
@@ -23,54 +25,31 @@ module Prosody
|
|
|
23
25
|
# @see TransientError
|
|
24
26
|
class TransientStateError < TransientError; end
|
|
25
27
|
|
|
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
28
|
# An immutable keyed-state collection definition.
|
|
37
29
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# {#to_state_config}
|
|
42
|
-
#
|
|
43
|
-
|
|
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
|
-
|
|
30
|
+
# The {Prosody.value}, {Prosody.map}, {Prosody.set}, and {Prosody.deque}
|
|
31
|
+
# constructors and their `message_*` siblings return frozen definitions. A
|
|
32
|
+
# definition serializes into `Configuration#state_collections` through
|
|
33
|
+
# {#to_state_config}, so the collection is registered before subscribe.
|
|
34
|
+
# {Prosody::Context#state} uses it to open the matching typed handle, and
|
|
35
|
+
# {Prosody::Client#state} uses it to open a published reader.
|
|
60
36
|
StateDefinition = Data.define(:name, :kind, :payload, :ttl_seconds, :read_uncommitted,
|
|
61
37
|
:published, :read_cache, :keyset_limit, :capacity, :access) do
|
|
38
|
+
# Every option defaults to +nil+, so a constructor passes only the
|
|
39
|
+
# options its kind takes.
|
|
40
|
+
def initialize(name:, kind:, access:, payload: nil, ttl_seconds: nil, read_uncommitted: nil,
|
|
41
|
+
published: nil, read_cache: nil, keyset_limit: nil, capacity: nil)
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
62
45
|
# Serializes this definition into the native-registration hash, omitting
|
|
63
|
-
# unset optionals so they fall back to the core defaults.
|
|
46
|
+
# unset optionals so they fall back to the core defaults. A set has no
|
|
47
|
+
# payload, so its hash has no payload key.
|
|
64
48
|
#
|
|
65
49
|
# @return [Hash] the registration hash for the native layer
|
|
66
50
|
def to_state_config
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
config[:read_uncommitted] = read_uncommitted unless read_uncommitted.nil?
|
|
70
|
-
config[:published] = published unless published.nil?
|
|
71
|
-
config[:keyset_limit] = keyset_limit unless keyset_limit.nil?
|
|
72
|
-
config[:capacity] = capacity unless capacity.nil?
|
|
73
|
-
config
|
|
51
|
+
to_h.slice(:name, :kind, :payload, :ttl_seconds, :read_uncommitted, :published, :keyset_limit,
|
|
52
|
+
:capacity).compact
|
|
74
53
|
end
|
|
75
54
|
end
|
|
76
55
|
|
|
@@ -79,12 +58,14 @@ module Prosody
|
|
|
79
58
|
# @param name [#to_s] the collection name (unique within the client)
|
|
80
59
|
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
81
60
|
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
61
|
+
# @param published [Boolean, nil] allow read-only access from other consumer groups
|
|
62
|
+
# @param read_cache [Numeric, false, nil] cache TTL in seconds for the readers
|
|
63
|
+
# that +client.state+ opens, +false+ to bypass the cache, or +nil+ to
|
|
64
|
+
# inherit the client default
|
|
82
65
|
# @return [StateDefinition] a frozen definition
|
|
83
66
|
def self.value(name, ttl: nil, read_uncommitted: nil, published: nil, read_cache: nil)
|
|
84
|
-
StateDefinition.new(name: name.to_s, kind: "value", payload: "json",
|
|
85
|
-
|
|
86
|
-
read_cache: read_cache, keyset_limit: nil, capacity: nil,
|
|
87
|
-
access: VALUE_ACCESS)
|
|
67
|
+
StateDefinition.new(name: name.to_s, kind: "value", payload: "json", ttl_seconds: ttl,
|
|
68
|
+
read_uncommitted: read_uncommitted, published: published, read_cache: read_cache, access: VALUE_ACCESS)
|
|
88
69
|
end
|
|
89
70
|
|
|
90
71
|
# Defines a `String`-keyed ordered map JSON collection.
|
|
@@ -93,12 +74,32 @@ module Prosody
|
|
|
93
74
|
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
94
75
|
# @param keyset_limit [Integer, nil] optional map-only keyset bound (`0..=4096`)
|
|
95
76
|
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
77
|
+
# @param published [Boolean, nil] allow read-only access from other consumer groups
|
|
78
|
+
# @param read_cache [Numeric, false, nil] cache TTL in seconds for the readers
|
|
79
|
+
# that +client.state+ opens, +false+ to bypass the cache, or +nil+ to
|
|
80
|
+
# inherit the client default
|
|
96
81
|
# @return [StateDefinition] a frozen definition
|
|
97
82
|
def self.map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil, published: nil, read_cache: nil)
|
|
98
|
-
StateDefinition.new(name: name.to_s, kind: "map", payload: "json",
|
|
99
|
-
|
|
100
|
-
read_cache: read_cache,
|
|
101
|
-
|
|
83
|
+
StateDefinition.new(name: name.to_s, kind: "map", payload: "json", ttl_seconds: ttl,
|
|
84
|
+
keyset_limit: keyset_limit, read_uncommitted: read_uncommitted, published: published,
|
|
85
|
+
read_cache: read_cache, access: MAP_ACCESS)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Defines an ordered set of String members. A set stores membership only,
|
|
89
|
+
# so it has no payload.
|
|
90
|
+
#
|
|
91
|
+
# @param name [#to_s] the collection name (unique within the client)
|
|
92
|
+
# @param ttl [Integer, nil] optional per-write TTL in whole seconds
|
|
93
|
+
# @param keyset_limit [Integer, nil] optional keyset bound (`0..=4096`)
|
|
94
|
+
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
95
|
+
# @param published [Boolean, nil] allow read-only access from other consumer groups
|
|
96
|
+
# @param read_cache [Numeric, false, nil] cache TTL in seconds for the readers
|
|
97
|
+
# that +client.state+ opens, +false+ to bypass the cache, or +nil+ to
|
|
98
|
+
# inherit the client default
|
|
99
|
+
# @return [StateDefinition] a frozen definition
|
|
100
|
+
def self.set(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil, published: nil, read_cache: nil)
|
|
101
|
+
StateDefinition.new(name: name.to_s, kind: "set", ttl_seconds: ttl, keyset_limit: keyset_limit,
|
|
102
|
+
read_uncommitted: read_uncommitted, published: published, read_cache: read_cache, access: SET_ACCESS)
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
# Defines a deque JSON collection.
|
|
@@ -109,12 +110,15 @@ module Prosody
|
|
|
109
110
|
# deque keeps at most this many slots, enforced lazily on push. Runtime-only
|
|
110
111
|
# and mutable across deploys, never persisted (see {DequeState#push}).
|
|
111
112
|
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
113
|
+
# @param published [Boolean, nil] allow read-only access from other consumer groups
|
|
114
|
+
# @param read_cache [Numeric, false, nil] cache TTL in seconds for the readers
|
|
115
|
+
# that +client.state+ opens, +false+ to bypass the cache, or +nil+ to
|
|
116
|
+
# inherit the client default
|
|
112
117
|
# @return [StateDefinition] a frozen definition
|
|
113
118
|
def self.deque(name, ttl: nil, capacity: nil, read_uncommitted: nil, published: nil, read_cache: nil)
|
|
114
|
-
StateDefinition.new(name: name.to_s, kind: "deque", payload: "json",
|
|
115
|
-
|
|
116
|
-
read_cache: read_cache,
|
|
117
|
-
access: DEQUE_ACCESS)
|
|
119
|
+
StateDefinition.new(name: name.to_s, kind: "deque", payload: "json", ttl_seconds: ttl,
|
|
120
|
+
capacity: capacity, read_uncommitted: read_uncommitted, published: published,
|
|
121
|
+
read_cache: read_cache, access: DEQUE_ACCESS)
|
|
118
122
|
end
|
|
119
123
|
|
|
120
124
|
# Defines a single-value Kafka-message collection (items are full messages).
|
|
@@ -124,10 +128,8 @@ module Prosody
|
|
|
124
128
|
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
125
129
|
# @return [StateDefinition] a frozen definition
|
|
126
130
|
def self.message_value(name, ttl: nil, read_uncommitted: nil)
|
|
127
|
-
StateDefinition.new(name: name.to_s, kind: "value", payload: "message",
|
|
128
|
-
|
|
129
|
-
read_cache: nil, keyset_limit: nil, capacity: nil,
|
|
130
|
-
access: MESSAGE_VALUE_ACCESS)
|
|
131
|
+
StateDefinition.new(name: name.to_s, kind: "value", payload: "message", ttl_seconds: ttl,
|
|
132
|
+
read_uncommitted: read_uncommitted, access: MESSAGE_VALUE_ACCESS)
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
# Defines a `String`-keyed ordered map Kafka-message collection.
|
|
@@ -138,10 +140,8 @@ module Prosody
|
|
|
138
140
|
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
139
141
|
# @return [StateDefinition] a frozen definition
|
|
140
142
|
def self.message_map(name, ttl: nil, keyset_limit: nil, read_uncommitted: nil)
|
|
141
|
-
StateDefinition.new(name: name.to_s, kind: "map", payload: "message",
|
|
142
|
-
|
|
143
|
-
read_cache: nil, keyset_limit: keyset_limit, capacity: nil,
|
|
144
|
-
access: MESSAGE_MAP_ACCESS)
|
|
143
|
+
StateDefinition.new(name: name.to_s, kind: "map", payload: "message", ttl_seconds: ttl,
|
|
144
|
+
keyset_limit: keyset_limit, read_uncommitted: read_uncommitted, access: MESSAGE_MAP_ACCESS)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Defines a deque Kafka-message collection.
|
|
@@ -154,25 +154,47 @@ module Prosody
|
|
|
154
154
|
# @param read_uncommitted [Boolean, nil] optional opt-out of transactional staging
|
|
155
155
|
# @return [StateDefinition] a frozen definition
|
|
156
156
|
def self.message_deque(name, ttl: nil, capacity: nil, read_uncommitted: nil)
|
|
157
|
-
StateDefinition.new(name: name.to_s, kind: "deque", payload: "message",
|
|
158
|
-
|
|
159
|
-
read_cache: nil, keyset_limit: nil, capacity: capacity,
|
|
160
|
-
access: MESSAGE_DEQUE_ACCESS)
|
|
157
|
+
StateDefinition.new(name: name.to_s, kind: "deque", payload: "message", ttl_seconds: ttl,
|
|
158
|
+
capacity: capacity, read_uncommitted: read_uncommitted, access: MESSAGE_DEQUE_ACCESS)
|
|
161
159
|
end
|
|
162
160
|
|
|
163
161
|
# Shared state wrapper behavior.
|
|
164
162
|
module State
|
|
163
|
+
# The base class of the owned handles. It holds the native handle and gives
|
|
164
|
+
# the commit and rollback that every collection shares.
|
|
165
|
+
#
|
|
166
|
+
# @api private
|
|
167
|
+
class Handle
|
|
168
|
+
# @param native [Object] the native handle
|
|
169
|
+
def initialize(native)
|
|
170
|
+
@native = native
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Durably commits the buffered operations mid-handler.
|
|
174
|
+
#
|
|
175
|
+
# @return [Symbol] +:applied+ when buffered operations were written, or
|
|
176
|
+
# +:no_op+ when nothing was buffered
|
|
177
|
+
def commit = @native.commit
|
|
178
|
+
|
|
179
|
+
# Discards the buffered uncommitted operations.
|
|
180
|
+
#
|
|
181
|
+
# @return [Symbol] +:applied+ when buffered operations were discarded, or
|
|
182
|
+
# +:no_op+ when nothing was buffered
|
|
183
|
+
def rollback = @native.rollback
|
|
184
|
+
end
|
|
185
|
+
|
|
165
186
|
module Reading
|
|
166
|
-
# Opens a read-only view of a published JSON collection.
|
|
187
|
+
# Opens a read-only view of a published JSON or set collection.
|
|
188
|
+
#
|
|
189
|
+
# @raise [TransientStateError, PermanentStateError] if Prosody cannot
|
|
190
|
+
# open the reader, for example for a zero +read_cache+
|
|
167
191
|
def state(subsystem, definition)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
definition.read_cache == false)
|
|
175
|
-
Prosody.const_get(access.published_wrapper).new(native)
|
|
192
|
+
open, reader = definition.access.reader
|
|
193
|
+
raise ArgumentError, "published state readers support JSON and set collections only" unless open
|
|
194
|
+
|
|
195
|
+
cache = definition.read_cache
|
|
196
|
+
cache = Float(cache) unless cache.nil? || cache == true || cache == false
|
|
197
|
+
reader.new(send(open, subsystem.to_s, definition.name, cache))
|
|
176
198
|
end
|
|
177
199
|
end
|
|
178
200
|
|
|
@@ -182,21 +204,16 @@ module Prosody
|
|
|
182
204
|
module Vending
|
|
183
205
|
# Vends the typed keyed-state handle for `definition`.
|
|
184
206
|
#
|
|
185
|
-
# Handles are cached per context by
|
|
186
|
-
#
|
|
207
|
+
# Handles are cached per context by definition, so repeated vends within
|
|
208
|
+
# one handler invocation return the same wrapper.
|
|
187
209
|
#
|
|
188
210
|
# @param definition [StateDefinition] a frozen collection definition
|
|
189
|
-
# @return [ValueState, MapState, DequeState] the typed handle
|
|
190
|
-
# @raise [TransientStateError] if the definition's kind/payload is unknown
|
|
211
|
+
# @return [ValueState, MapState, SetState, DequeState] the typed handle
|
|
191
212
|
# @raise [PermanentStateError] if the collection name is unregistered or
|
|
192
213
|
# its registered identity mismatches
|
|
193
214
|
def state(definition)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return cache[cache_key] if cache.key?(cache_key)
|
|
197
|
-
|
|
198
|
-
native = public_send(definition.access.vend_method, definition.name)
|
|
199
|
-
cache[cache_key] = Prosody.const_get(definition.access.wrapper).new(native)
|
|
215
|
+
(@state_handles ||= {})[definition] ||=
|
|
216
|
+
definition.access.wrapper.new(send(definition.access.vend_method, definition.name))
|
|
200
217
|
end
|
|
201
218
|
end
|
|
202
219
|
|
|
@@ -204,613 +221,76 @@ module Prosody
|
|
|
204
221
|
# identical native-scan open/close/exhaustion loop; each handle supplies
|
|
205
222
|
# only the per-item yield shape through the block. Kept private (mixed into
|
|
206
223
|
# the handle classes) since it is not part of the public surface.
|
|
224
|
+
#
|
|
225
|
+
# Every traversal method accepts optional query keywords. Map and set
|
|
226
|
+
# traversals take `from:`, `after:`, `to:`, `before:`, `range:`,
|
|
227
|
+
# `prefix:`, and `limit:` over String keys. Deque traversals take the same
|
|
228
|
+
# keywords without `prefix:`, over non-negative positions from the front.
|
|
229
|
+
# `from:`/`after:` start and `to:`/`before:` stop in iteration order, so a
|
|
230
|
+
# reverse traversal starts at the high end. `range:` takes a Ruby `Range`
|
|
231
|
+
# (inclusive, exclusive, beginless, or endless) in ascending order and
|
|
232
|
+
# applies in either direction. A descending `Range` is empty. Every
|
|
233
|
+
# keyword narrows the selection. `limit:` counts yielded
|
|
234
|
+
# items. The native layer translates the keywords and raises
|
|
235
|
+
# `ArgumentError` or `TypeError` for a bad one.
|
|
207
236
|
module Scanning
|
|
208
237
|
private
|
|
209
238
|
|
|
210
|
-
# Opens
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def scan_each(direction, opener = :scan)
|
|
217
|
-
scan_items(@native.public_send(opener, direction)) { |item| yield item }
|
|
218
|
-
end
|
|
239
|
+
# Opens the native cursor `opener` (`:scan` or `:keys`) with `args`,
|
|
240
|
+
# yields each item, and closes the cursor via `ensure` on stop or
|
|
241
|
+
# exception. Without a block, returns an Enumerator. A map scan yields
|
|
242
|
+
# each `[key, value]` pair as one Array, as `Hash#each_pair` does.
|
|
243
|
+
def traverse(opener, *args, &block)
|
|
244
|
+
return enum_for(:traverse, opener, *args) unless block
|
|
219
245
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
246
|
+
scan = @native.public_send(opener, *args)
|
|
247
|
+
while (chunk = scan.next_chunk)
|
|
248
|
+
chunk.each(&block)
|
|
227
249
|
end
|
|
228
250
|
ensure
|
|
229
|
-
scan
|
|
251
|
+
scan&.close
|
|
230
252
|
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
253
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
254
|
+
# Traverses a map scan like {#traverse} and yields only each value.
|
|
255
|
+
def traverse_values(*args)
|
|
256
|
+
return enum_for(:traverse_values, *args) unless block_given?
|
|
269
257
|
|
|
270
|
-
|
|
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}"
|
|
258
|
+
traverse(:scan, *args) { |entry| yield entry[1] }
|
|
291
259
|
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 }
|
|
315
260
|
end
|
|
316
261
|
end
|
|
317
262
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
# Reads return the stored JSON value (or a {Prosody::Message} for message
|
|
321
|
-
# collections), or `nil` when the value is absent. Writes are buffered and
|
|
322
|
-
# made durable by {#commit}. All operations are fiber-yield async: they look
|
|
323
|
-
# blocking but never block the thread.
|
|
324
|
-
class ValueState
|
|
325
|
-
# @param native [Prosody::NativeJsonValueState, Prosody::NativeMessageValueState] the native handle
|
|
326
|
-
def initialize(native)
|
|
327
|
-
@native = native
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
# Reads the current value.
|
|
331
|
-
#
|
|
332
|
-
# @return [Object, nil] the stored value, or `nil` when absent
|
|
333
|
-
def get = @native.get
|
|
334
|
-
|
|
335
|
-
# Buffers a write of the value.
|
|
336
|
-
#
|
|
337
|
-
# @param value [Object] the value to store (JSON, or a message)
|
|
338
|
-
# @return [void]
|
|
339
|
-
# @raise [NullValueError] if `value` is `nil` (use {#clear} to delete)
|
|
340
|
-
def set(value) = @native.set(value)
|
|
341
|
-
|
|
342
|
-
# Buffers a clear of the value.
|
|
343
|
-
#
|
|
344
|
-
# @return [void]
|
|
345
|
-
def clear = @native.clear
|
|
346
|
-
|
|
347
|
-
# Durably commits the buffered operations mid-handler.
|
|
348
|
-
#
|
|
349
|
-
# @return [nil] the erased FFI seam drops the applied/no-op outcome
|
|
350
|
-
def commit = @native.commit
|
|
351
|
-
|
|
352
|
-
# Discards the buffered uncommitted operations.
|
|
353
|
-
#
|
|
354
|
-
# @return [nil]
|
|
355
|
-
def rollback = @native.rollback
|
|
356
|
-
|
|
357
|
-
# Reads the current value. Idiomatic alias of {#get}.
|
|
358
|
-
#
|
|
359
|
-
# @return [Object, nil]
|
|
360
|
-
alias_method :value, :get
|
|
361
|
-
|
|
362
|
-
# Buffers a write of the value. Idiomatic alias of {#set}. As with any Ruby
|
|
363
|
-
# writer, `state.value = x` evaluates to `x` regardless of the return.
|
|
364
|
-
#
|
|
365
|
-
# @param value [Object]
|
|
366
|
-
# @return [void]
|
|
367
|
-
alias_method :value=, :set
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
# A `String`-keyed ordered-map keyed-state handle.
|
|
371
|
-
#
|
|
372
|
-
# Traversal is explicit: {#each_pair}/{#reverse_each_pair} yield `key, value`
|
|
373
|
-
# pairs over a native scan, closing the scan via `ensure`. No aggregate-mixin
|
|
374
|
-
# methods are provided — they would silently materialize an unbounded remote
|
|
375
|
-
# collection.
|
|
376
|
-
class MapState
|
|
377
|
-
include State::Scanning
|
|
378
|
-
|
|
379
|
-
# @param native [Prosody::NativeJsonMapState, Prosody::NativeMessageMapState] the native handle
|
|
380
|
-
def initialize(native)
|
|
381
|
-
@native = native
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
# Reads the value for `key`.
|
|
385
|
-
#
|
|
386
|
-
# @param key [String] the map key
|
|
387
|
-
# @return [Object, nil] the value, or `nil` when the key is absent
|
|
388
|
-
def get(key) = @native.get(key)
|
|
389
|
-
|
|
390
|
-
# Reads several keys in a single isolated batch.
|
|
391
|
-
#
|
|
392
|
-
# @param keys [Array<String>] the keys to read, in order
|
|
393
|
-
# @return [Array<Object, nil>] one result per input key; `nil` for absent keys
|
|
394
|
-
def get_many(keys) = @native.get_many(keys)
|
|
395
|
-
|
|
396
|
-
# Inserts or overwrites `key`.
|
|
397
|
-
#
|
|
398
|
-
# @param key [String] the map key
|
|
399
|
-
# @param value [Object] the value to store (JSON, or a message)
|
|
400
|
-
# @return [void]
|
|
401
|
-
# @raise [NullValueError] if `value` is `nil` (use {#delete} to remove)
|
|
402
|
-
def set(key, value) = @native.set(key, value)
|
|
403
|
-
|
|
404
|
-
# Removes `key`.
|
|
405
|
-
#
|
|
406
|
-
# Documented divergence from `Hash#delete`: this returns `nil`, never the
|
|
407
|
-
# removed value (the erased FFI seam does not surface it).
|
|
408
|
-
#
|
|
409
|
-
# @param key [String] the map key
|
|
410
|
-
# @return [nil]
|
|
411
|
-
def delete(key)
|
|
412
|
-
@native.remove(key)
|
|
413
|
-
nil
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
# Removes every entry.
|
|
417
|
-
#
|
|
418
|
-
# @return [void]
|
|
419
|
-
def clear = @native.clear
|
|
420
|
-
|
|
421
|
-
# Durably commits the buffered operations mid-handler.
|
|
422
|
-
#
|
|
423
|
-
# @return [nil] the erased FFI seam drops the applied/no-op outcome
|
|
424
|
-
def commit = @native.commit
|
|
425
|
-
|
|
426
|
-
# Discards the buffered uncommitted operations.
|
|
427
|
-
#
|
|
428
|
-
# @return [nil]
|
|
429
|
-
def rollback = @native.rollback
|
|
430
|
-
|
|
431
|
-
# Traverses the live entries in key order, yielding `key, value`.
|
|
432
|
-
#
|
|
433
|
-
# Without a block, returns an {Enumerator} over the native scan. Each step
|
|
434
|
-
# fiber-yields; the scan is closed via `ensure` on stop or exception. The
|
|
435
|
-
# enumerator is valid only within the current handler invocation.
|
|
436
|
-
#
|
|
437
|
-
# @yieldparam key [String]
|
|
438
|
-
# @yieldparam value [Object]
|
|
439
|
-
# @return [Enumerator, void]
|
|
440
|
-
def each_pair(&block) = traverse(:forward, &block)
|
|
441
|
-
|
|
442
|
-
# Traverses the live entries in reverse key order, yielding `key, value`.
|
|
443
|
-
#
|
|
444
|
-
# @yieldparam key [String]
|
|
445
|
-
# @yieldparam value [Object]
|
|
446
|
-
# @return [Enumerator, void]
|
|
447
|
-
def reverse_each_pair(&block) = traverse(:backward, &block)
|
|
448
|
-
|
|
449
|
-
# Traverses the live keys in key order, yielding each key (mirrors
|
|
450
|
-
# +Hash#each_key+). The key scan skips value decode and the resolver — a
|
|
451
|
-
# message-backed map yields keys with zero Kafka fetches, though not
|
|
452
|
-
# zero-I/O. Without a block, returns a demand-driven {Enumerator}; there is
|
|
453
|
-
# deliberately no eager +keys+ array (it would materialize the whole remote
|
|
454
|
-
# keyset). Mirrors {#each_pair}'s block-form return (+nil+), not stdlib's
|
|
455
|
-
# +self+, for in-repo sibling consistency.
|
|
456
|
-
#
|
|
457
|
-
# @yieldparam key [String]
|
|
458
|
-
# @return [Enumerator, void]
|
|
459
|
-
def each_key(&block) = traverse_keys(:forward, &block)
|
|
460
|
-
|
|
461
|
-
# Traverses the live keys in reverse key order, yielding each key.
|
|
462
|
-
#
|
|
463
|
-
# @yieldparam key [String]
|
|
464
|
-
# @return [Enumerator, void]
|
|
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)
|
|
468
|
-
|
|
469
|
-
# --- idiomatic Hash-style aliases and conveniences ------------------
|
|
470
|
-
# Each is composed from the canonical ops above and adds no capability
|
|
471
|
-
# the naming matrix lacks. Bounded reads only: there is deliberately no
|
|
472
|
-
# +keys+/+values+/+to_h+/+count+ or +Enumerable+, which would materialize
|
|
473
|
-
# the whole (potentially unbounded) remote collection.
|
|
474
|
-
|
|
475
|
-
# Reads +key+. Idiomatic alias of {#get} (mirrors +Hash#[]+).
|
|
476
|
-
alias_method :[], :get
|
|
477
|
-
|
|
478
|
-
# Writes +key+. Idiomatic alias of {#set} (mirrors +Hash#[]=+). As with any
|
|
479
|
-
# Ruby +[]=+, `map[key] = value` evaluates to +value+ regardless of return.
|
|
480
|
-
alias_method :[]=, :set
|
|
481
|
-
|
|
482
|
-
# Writes +key+, returning the stored +value+ (mirrors +Hash#store+). A
|
|
483
|
-
# wrapper, not an alias: unlike +[]=+, +store+ is called normally, so its
|
|
484
|
-
# return is observed — and the native write returns +nil+.
|
|
485
|
-
#
|
|
486
|
-
# @param key [String]
|
|
487
|
-
# @param value [Object]
|
|
488
|
-
# @return [Object] the stored +value+
|
|
489
|
-
def store(key, value)
|
|
490
|
-
set(key, value)
|
|
491
|
-
value
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
# Traverses live entries in key order. Idiomatic alias of {#each_pair}
|
|
495
|
-
# (mirrors +Hash#each+).
|
|
496
|
-
alias_method :each, :each_pair
|
|
497
|
-
|
|
498
|
-
# Reads several keys positionally (mirrors +Hash#values_at+).
|
|
499
|
-
#
|
|
500
|
-
# @param keys [Array<String>] the keys to read
|
|
501
|
-
# @return [Array<Object, nil>] one result per key; +nil+ for absent keys
|
|
502
|
-
def values_at(*keys) = get_many(keys)
|
|
503
|
-
|
|
504
|
-
# Reads +key+, raising or defaulting when absent (mirrors +Hash#fetch+).
|
|
505
|
-
# Performs a single read; a +nil+ result is unambiguously "absent" under
|
|
506
|
-
# the null ban.
|
|
507
|
-
#
|
|
508
|
-
# @param key [String]
|
|
509
|
-
# @param default [Object] returned when +key+ is absent
|
|
510
|
-
# @yieldparam key [String] called (instead of +default+) when +key+ is absent
|
|
511
|
-
# @return [Object]
|
|
512
|
-
# @raise [KeyError] when +key+ is absent and no default or block is given
|
|
513
|
-
def fetch(key, *default, &block)
|
|
514
|
-
if default.length > 1
|
|
515
|
-
raise ArgumentError, "wrong number of arguments (given #{default.length + 1}, expected 1..2)"
|
|
516
|
-
end
|
|
517
|
-
warn "warning: block supersedes default value argument" if block && !default.empty?
|
|
518
|
-
|
|
519
|
-
value = @native.get(key)
|
|
520
|
-
return value unless value.nil?
|
|
521
|
-
return block.call(key) if block
|
|
522
|
-
return default.first unless default.empty?
|
|
523
|
-
|
|
524
|
-
raise KeyError.new("key not found: #{key.inspect}", key: key, receiver: self)
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
# Whether +key+ has a live value (mirrors +Hash#key?+). A presence check:
|
|
528
|
-
# no value decode and no resolver run (not no-I/O). A message-backed map
|
|
529
|
-
# answers presence with zero Kafka fetches — +true+ even for a
|
|
530
|
-
# present-but-unfetchable cell — though a cache miss may still touch the
|
|
531
|
-
# store.
|
|
532
|
-
#
|
|
533
|
-
# @param key [String]
|
|
534
|
-
# @return [Boolean]
|
|
535
|
-
def key?(key) = @native.contains_key(key)
|
|
536
|
-
alias_method :has_key?, :key?
|
|
537
|
-
alias_method :include?, :key?
|
|
538
|
-
alias_method :member?, :key?
|
|
539
|
-
|
|
540
|
-
# Reads +key+ and digs into the nested value (mirrors +Hash#dig+). A single
|
|
541
|
-
# bounded read; digging continues in the returned local value.
|
|
542
|
-
#
|
|
543
|
-
# @param key [String]
|
|
544
|
-
# @return [Object, nil]
|
|
545
|
-
# @raise [TypeError] if a nested value does not respond to +dig+
|
|
546
|
-
def dig(key, *rest)
|
|
547
|
-
value = @native.get(key)
|
|
548
|
-
return value if rest.empty? || value.nil?
|
|
549
|
-
|
|
550
|
-
unless value.respond_to?(:dig)
|
|
551
|
-
raise TypeError, "#{value.class} does not have #dig method"
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
value.dig(*rest)
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
# Reads +keys+ as a single bounded batch, returning a +Hash+ of only the
|
|
558
|
-
# keys that are present (mirrors +Hash#slice+). Absent keys are omitted.
|
|
559
|
-
#
|
|
560
|
-
# @param keys [Array<String>] the keys to read
|
|
561
|
-
# @return [Hash{String => Object}] present keys mapped to their values
|
|
562
|
-
def slice(*keys)
|
|
563
|
-
result = {}
|
|
564
|
-
keys.zip(get_many(keys)) do |key, value|
|
|
565
|
-
result[key] = value unless value.nil?
|
|
566
|
-
end
|
|
567
|
-
result
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
# Reads +keys+ as a single bounded batch, requiring every key to be present
|
|
571
|
-
# (mirrors +Hash#fetch_values+). Without a block, a missing key raises
|
|
572
|
-
# {KeyError}; with a block, the block is called with each missing key and
|
|
573
|
-
# its result substituted.
|
|
574
|
-
#
|
|
575
|
-
# @param keys [Array<String>] the keys to read, in order
|
|
576
|
-
# @yieldparam key [String] called for each absent key
|
|
577
|
-
# @return [Array<Object>] one value per key, in order
|
|
578
|
-
# @raise [KeyError] when a key is absent and no block is given
|
|
579
|
-
def fetch_values(*keys, &block)
|
|
580
|
-
keys.zip(get_many(keys)).map do |key, value|
|
|
581
|
-
next value unless value.nil?
|
|
582
|
-
next block.call(key) if block
|
|
583
|
-
|
|
584
|
-
raise KeyError.new("key not found: #{key.inspect}", key: key, receiver: self)
|
|
585
|
-
end
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
private
|
|
589
|
-
|
|
590
|
-
def traverse(direction)
|
|
591
|
-
return enum_for(:traverse, direction) unless block_given?
|
|
592
|
-
|
|
593
|
-
# Yield the [key, value] pair as a single Array, matching Hash#each_pair:
|
|
594
|
-
# a two-parameter block auto-splats it (|k, v|), a one-parameter block
|
|
595
|
-
# receives the pair (|pair|), and the no-block Enumerator yields pairs.
|
|
596
|
-
scan_each(direction) { |pair| yield pair }
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
def traverse_keys(direction)
|
|
600
|
-
return enum_for(:traverse_keys, direction) unless block_given?
|
|
601
|
-
|
|
602
|
-
scan_each(direction, :keys) { |key| yield key }
|
|
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
|
|
610
|
-
end
|
|
611
|
-
|
|
612
|
-
# A deque keyed-state handle.
|
|
613
|
-
#
|
|
614
|
-
# Traversal is explicit: {#each}/{#reverse_each} yield single elements over a
|
|
615
|
-
# native scan, closing the scan via `ensure`. No aggregate-mixin methods are
|
|
616
|
-
# provided.
|
|
617
|
-
class DequeState
|
|
618
|
-
include State::Scanning
|
|
619
|
-
|
|
620
|
-
# @param native [Prosody::NativeJsonDequeState, Prosody::NativeMessageDequeState] the native handle
|
|
621
|
-
def initialize(native)
|
|
622
|
-
@native = native
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
# Appends an element at the back.
|
|
626
|
-
#
|
|
627
|
-
# @param value [Object] the element (JSON, or a message)
|
|
628
|
-
# @return [void]
|
|
629
|
-
# @raise [NullValueError] if `value` is `nil`
|
|
630
|
-
def push(value) = @native.push_back(value)
|
|
631
|
-
|
|
632
|
-
# Prepends an element at the front.
|
|
633
|
-
#
|
|
634
|
-
# @param value [Object] the element (JSON, or a message)
|
|
635
|
-
# @return [void]
|
|
636
|
-
# @raise [NullValueError] if `value` is `nil`
|
|
637
|
-
def unshift(value) = @native.push_front(value)
|
|
638
|
-
|
|
639
|
-
# Removes and returns the back element.
|
|
640
|
-
#
|
|
641
|
-
# @return [Object, nil] the removed element, or `nil` when empty
|
|
642
|
-
def pop = @native.pop_back
|
|
643
|
-
|
|
644
|
-
# Removes and returns the front element.
|
|
645
|
-
#
|
|
646
|
-
# @return [Object, nil] the removed element, or `nil` when empty
|
|
647
|
-
def shift = @native.pop_front
|
|
648
|
-
|
|
649
|
-
# The number of live elements.
|
|
650
|
-
#
|
|
651
|
-
# @return [Integer]
|
|
652
|
-
def length = @native.len
|
|
653
|
-
|
|
654
|
-
alias_method :size, :length
|
|
655
|
-
|
|
656
|
-
# Whether the deque holds no live elements.
|
|
657
|
-
#
|
|
658
|
-
# @return [Boolean]
|
|
659
|
-
def empty? = @native.is_empty
|
|
660
|
-
|
|
661
|
-
# Removes every element.
|
|
662
|
-
#
|
|
663
|
-
# @return [void]
|
|
664
|
-
def clear = @native.clear
|
|
665
|
-
|
|
666
|
-
# Durably commits the buffered operations mid-handler.
|
|
667
|
-
#
|
|
668
|
-
# @return [nil] the erased FFI seam drops the applied/no-op outcome
|
|
669
|
-
def commit = @native.commit
|
|
670
|
-
|
|
671
|
-
# Discards the buffered uncommitted operations.
|
|
672
|
-
#
|
|
673
|
-
# @return [nil]
|
|
674
|
-
def rollback = @native.rollback
|
|
675
|
-
|
|
676
|
-
# Reads the element at `index`, resolving negatives Array-style (mirrors
|
|
677
|
-
# +Array#[]+'s read domain, without the indexer). A non-negative index
|
|
678
|
-
# reads from the front; `-1` is the back element, `-n` the nth from the end.
|
|
679
|
-
# `-1` fast-paths through {#last} (no length read); other negatives resolve
|
|
680
|
-
# against the current length (one length read + one element read),
|
|
681
|
-
# consistent because the deque has a single writer per attempt.
|
|
682
|
-
#
|
|
683
|
-
# @param index [Integer] the position (negative counts from the back)
|
|
684
|
-
# @return [Object, nil] the element, or `nil` outside the bounds
|
|
685
|
-
# @raise [TransientStateError] if `index` is not an Integer
|
|
686
|
-
def get(index)
|
|
687
|
-
unless index.is_a?(Integer)
|
|
688
|
-
raise TransientStateError, "get: index must be an Integer, got #{index.inspect}"
|
|
689
|
-
end
|
|
690
|
-
index.negative? ? at_negative(index) : @native.get(index)
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
# Traverses the live elements in index order.
|
|
694
|
-
#
|
|
695
|
-
# Without a block, returns an {Enumerator} over the native scan. Each step
|
|
696
|
-
# fiber-yields; the scan is closed via `ensure` on stop or exception.
|
|
697
|
-
#
|
|
698
|
-
# @yieldparam element [Object]
|
|
699
|
-
# @return [Enumerator, void]
|
|
700
|
-
def each(&block) = traverse(:forward, &block)
|
|
701
|
-
|
|
702
|
-
# Traverses the live elements in reverse index order.
|
|
703
|
-
#
|
|
704
|
-
# @yieldparam element [Object]
|
|
705
|
-
# @return [Enumerator, void]
|
|
706
|
-
def reverse_each(&block) = traverse(:backward, &block)
|
|
707
|
-
|
|
708
|
-
# --- idiomatic Array-style conveniences -----------------------------
|
|
709
|
-
# Composed from the canonical ops above; bounded reads only (no +to_a+,
|
|
710
|
-
# +map+, +sort+, or +Enumerable+ that would materialize the whole deque).
|
|
711
|
-
#
|
|
712
|
-
# Deliberately NOT provided: +[]+ and +at+. This is a remote deque; +get+
|
|
713
|
-
# and +fetch+ accept a single +Integer+ index (negatives resolve from the
|
|
714
|
-
# back, Array-style), but wearing +Array+'s +[]+/+at+ would invite a range
|
|
715
|
-
# read (+deque[0..2]+) that cannot be honored. Use the explicit {#get}, or
|
|
716
|
-
# {#first}/{#last} for the ends.
|
|
717
|
-
|
|
718
|
-
# Prepends +value+, returning +self+ for chaining (mirrors +Array#prepend+).
|
|
719
|
-
# A wrapper, not an alias: the native write returns +nil+.
|
|
720
|
-
#
|
|
721
|
-
# @param value [Object]
|
|
722
|
-
# @return [self]
|
|
723
|
-
def prepend(value)
|
|
724
|
-
unshift(value)
|
|
725
|
-
self
|
|
726
|
-
end
|
|
727
|
-
|
|
728
|
-
# Appends +value+, returning +self+ for chaining (mirrors +Array#append+).
|
|
729
|
-
# A wrapper, not an alias: the native write returns +nil+.
|
|
730
|
-
#
|
|
731
|
-
# @param value [Object]
|
|
732
|
-
# @return [self]
|
|
733
|
-
def append(value)
|
|
734
|
-
push(value)
|
|
735
|
-
self
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
# Appends +value+ at the back and returns +self+ for chaining
|
|
739
|
-
# (mirrors +Array#<<+).
|
|
740
|
-
#
|
|
741
|
-
# @param value [Object]
|
|
742
|
-
# @return [self]
|
|
743
|
-
def <<(value)
|
|
744
|
-
push(value)
|
|
745
|
-
self
|
|
746
|
-
end
|
|
747
|
-
|
|
748
|
-
# The front element, or +nil+ when empty (mirrors +Array#first+). An
|
|
749
|
-
# endpoint-slot read in one round trip (no length read). Under a TTL an
|
|
750
|
-
# expired front slot yields +nil+ even when live interior elements remain —
|
|
751
|
-
# a peek never searches inward.
|
|
752
|
-
#
|
|
753
|
-
# @return [Object, nil]
|
|
754
|
-
def first = @native.peek_front
|
|
755
|
-
|
|
756
|
-
# The back element, or +nil+ when empty (mirrors +Array#last+). An
|
|
757
|
-
# endpoint-slot read in one round trip (no length read); same TTL-hole
|
|
758
|
-
# semantics as {#first}.
|
|
759
|
-
#
|
|
760
|
-
# @return [Object, nil]
|
|
761
|
-
def last = @native.peek_back
|
|
762
|
-
|
|
763
|
-
# Reads the element at +index+, raising or defaulting when out of range
|
|
764
|
-
# (mirrors +Array#fetch+). A +nil+ result is unambiguously "out of range"
|
|
765
|
-
# under the null ban. Negatives resolve Array-style like {#get} — +-1+ is
|
|
766
|
-
# the back element, +-n+ the nth from the end; a fractional or non-Integer
|
|
767
|
-
# index is a caller mistake, rejected {TransientStateError}.
|
|
768
|
-
#
|
|
769
|
-
# @param index [Integer] the position (negative counts from the back)
|
|
770
|
-
# @param default [Object] returned when +index+ is out of range
|
|
771
|
-
# @yieldparam index [Integer] called (instead of +default+) when out of range
|
|
772
|
-
# @return [Object]
|
|
773
|
-
# @raise [IndexError] when out of range and no default or block is given
|
|
774
|
-
# @raise [TransientStateError] if +index+ is not an Integer
|
|
775
|
-
def fetch(index, *default, &block)
|
|
776
|
-
if default.length > 1
|
|
777
|
-
raise ArgumentError, "wrong number of arguments (given #{default.length + 1}, expected 1..2)"
|
|
778
|
-
end
|
|
779
|
-
unless index.is_a?(Integer)
|
|
780
|
-
raise TransientStateError, "fetch: index must be an Integer, got #{index.inspect}"
|
|
781
|
-
end
|
|
782
|
-
warn "warning: block supersedes default value argument" if block && !default.empty?
|
|
783
|
-
|
|
784
|
-
value = index.negative? ? at_negative(index) : @native.get(index)
|
|
785
|
-
return value unless value.nil?
|
|
786
|
-
return block.call(index) if block
|
|
787
|
-
return default.first unless default.empty?
|
|
788
|
-
|
|
789
|
-
raise IndexError, "index #{index} outside deque bounds"
|
|
790
|
-
end
|
|
791
|
-
|
|
792
|
-
private
|
|
793
|
-
|
|
794
|
-
# Resolves a negative Array-style index against the current length: +-1+
|
|
795
|
-
# fast-paths through {#last} (no length read), other negatives read the
|
|
796
|
-
# length and index from the front. Returns +nil+ when the index resolves
|
|
797
|
-
# before the front (past the far end of the deque).
|
|
798
|
-
def at_negative(index)
|
|
799
|
-
return @native.peek_back if index == -1
|
|
800
|
-
|
|
801
|
-
resolved = @native.len + index
|
|
802
|
-
resolved.negative? ? nil : @native.get(resolved)
|
|
803
|
-
end
|
|
804
|
-
|
|
805
|
-
def traverse(direction)
|
|
806
|
-
return enum_for(:traverse, direction) unless block_given?
|
|
263
|
+
class Client
|
|
264
|
+
include State::Reading
|
|
807
265
|
|
|
808
|
-
|
|
809
|
-
end
|
|
266
|
+
private :published_value, :published_map, :published_set, :published_deque
|
|
810
267
|
end
|
|
811
268
|
|
|
812
269
|
# Reopens the native context class to add keyed-state vending.
|
|
813
270
|
class Context
|
|
814
271
|
include State::Vending
|
|
272
|
+
|
|
273
|
+
private :value_state, :map_state, :set_state, :deque_state,
|
|
274
|
+
:message_value_state, :message_map_state, :message_deque_state
|
|
815
275
|
end
|
|
816
276
|
end
|
|
277
|
+
|
|
278
|
+
require_relative "state/value"
|
|
279
|
+
require_relative "state/map"
|
|
280
|
+
require_relative "state/set"
|
|
281
|
+
require_relative "state/deque"
|
|
282
|
+
|
|
283
|
+
module Prosody
|
|
284
|
+
# How a definition opens its handle and, for a published collection, its reader.
|
|
285
|
+
StateAccess = Data.define(:vend_method, :wrapper, :reader)
|
|
286
|
+
private_constant :StateAccess
|
|
287
|
+
VALUE_ACCESS = StateAccess.new(:value_state, ValueState, [:published_value, PublishedValue])
|
|
288
|
+
MAP_ACCESS = StateAccess.new(:map_state, MapState, [:published_map, PublishedMap])
|
|
289
|
+
SET_ACCESS = StateAccess.new(:set_state, SetState, [:published_set, PublishedSet])
|
|
290
|
+
DEQUE_ACCESS = StateAccess.new(:deque_state, DequeState, [:published_deque, PublishedDeque])
|
|
291
|
+
MESSAGE_VALUE_ACCESS = StateAccess.new(:message_value_state, ValueState, nil)
|
|
292
|
+
MESSAGE_MAP_ACCESS = StateAccess.new(:message_map_state, MapState, nil)
|
|
293
|
+
MESSAGE_DEQUE_ACCESS = StateAccess.new(:message_deque_state, DequeState, nil)
|
|
294
|
+
private_constant :VALUE_ACCESS, :MAP_ACCESS, :SET_ACCESS, :DEQUE_ACCESS,
|
|
295
|
+
:MESSAGE_VALUE_ACCESS, :MESSAGE_MAP_ACCESS, :MESSAGE_DEQUE_ACCESS
|
|
296
|
+
end
|