prosody 0.3.0-aarch64-linux → 0.4.0-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/ARCHITECTURE.md +12 -2
- data/CHANGELOG.md +13 -0
- 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/lib/prosody/3.2/prosody.so +0 -0
- data/lib/prosody/3.3/prosody.so +0 -0
- data/lib/prosody/3.4/prosody.so +0 -0
- data/lib/prosody/configuration.rb +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 -10
data/lib/prosody.rb
CHANGED
|
@@ -40,3 +40,8 @@ begin
|
|
|
40
40
|
rescue LoadError
|
|
41
41
|
require_relative "prosody/prosody"
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
# Loaded last: the keyed-state surface reopens the native Prosody::Context and
|
|
45
|
+
# subclasses the error hierarchy, so every native class and base error must
|
|
46
|
+
# already exist.
|
|
47
|
+
require_relative "prosody/state"
|
data/release-please-config.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"bootstrap-sha": "a5c4faa0b7a5bf14d74c7c77de72883b20fc41fa",
|
|
4
|
+
"include-component-in-tag": true,
|
|
5
|
+
"include-v-in-tag": true,
|
|
6
|
+
"tag-separator": "/",
|
|
3
7
|
"packages": {
|
|
4
8
|
".": {
|
|
5
9
|
"release-type": "ruby",
|
data/sig/configuration.rbs
CHANGED
|
@@ -24,7 +24,8 @@ module Prosody
|
|
|
24
24
|
# Create a new configuration with initial values
|
|
25
25
|
#
|
|
26
26
|
# @param kwargs Initial configuration values
|
|
27
|
-
def initialize: (?Hash[Symbol, untyped]
|
|
27
|
+
def initialize: (?Hash[Symbol, untyped] options) ?{ (Configuration) -> void } -> void
|
|
28
|
+
| (**untyped options) ?{ (Configuration) -> void } -> void
|
|
28
29
|
|
|
29
30
|
# Kafka bootstrap server addresses
|
|
30
31
|
def bootstrap_servers: () -> Array[String]?
|
|
@@ -234,6 +235,28 @@ module Prosody
|
|
|
234
235
|
def timer_spans: () -> String?
|
|
235
236
|
def timer_spans=: (_ToS) -> String
|
|
236
237
|
|
|
238
|
+
# Keyed-state collections to register before subscribe. Accepts an array of
|
|
239
|
+
# definition objects (from Prosody.value/map/deque and their message_*
|
|
240
|
+
# siblings) or already-serialized registration hashes.
|
|
241
|
+
def state_collections: () -> Array[untyped]?
|
|
242
|
+
def state_collections=: (untyped) -> Array[untyped]
|
|
243
|
+
|
|
244
|
+
# Root directory for the local keyed-state cache. Falls back to
|
|
245
|
+
# PROSODY_STATE_CACHE_DIR. Must not be an empty string.
|
|
246
|
+
def state_cache_dir: () -> String?
|
|
247
|
+
def state_cache_dir=: (_ToS) -> String
|
|
248
|
+
|
|
249
|
+
# Capacity of the in-memory keyed-state cache, in bytes. Must be positive.
|
|
250
|
+
# Falls back to
|
|
251
|
+
# PROSODY_STATE_CACHE_SIZE_BYTES, then the engine default.
|
|
252
|
+
def state_cache_size_bytes: () -> Integer?
|
|
253
|
+
def state_cache_size_bytes=: (Integer | _ToInt) -> Integer
|
|
254
|
+
|
|
255
|
+
# Delay in whole seconds between staging a provisional cell and the recovery
|
|
256
|
+
# sweep. Falls back to PROSODY_STATE_RECOVERY_DELAY (read by core).
|
|
257
|
+
def state_recovery_delay: () -> Float?
|
|
258
|
+
def state_recovery_delay=: (Numeric) -> Float
|
|
259
|
+
|
|
237
260
|
# Convert configuration to a hash with non-nil values only
|
|
238
261
|
def to_hash: () -> Hash[Symbol, untyped]
|
|
239
262
|
end
|
data/sig/handler.rbs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
module Prosody
|
|
2
|
+
# Base error class for all Prosody errors (defined in lib/prosody/handler.rb).
|
|
3
|
+
class Error < ::StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
2
6
|
# Abstract base for all errors raised by EventHandler methods.
|
|
3
7
|
# Subclasses must implement `#permanent?` to indicate retry behavior.
|
|
4
8
|
class EventHandlerError < Prosody::Error
|
|
@@ -22,7 +26,7 @@ module Prosody
|
|
|
22
26
|
|
|
23
27
|
# Mixin providing class-level methods to wrap instance methods so that
|
|
24
28
|
# specified exceptions are re-wrapped as PermanentError or TransientError.
|
|
25
|
-
module ErrorClassification
|
|
29
|
+
module ErrorClassification : Module
|
|
26
30
|
# Wraps the given instance method so that specified exception types
|
|
27
31
|
# are caught and re-raised as Prosody::PermanentError.
|
|
28
32
|
#
|
|
@@ -54,7 +58,7 @@ module Prosody
|
|
|
54
58
|
|
|
55
59
|
# Abstract base class for handling incoming messages from Prosody.
|
|
56
60
|
# Subclasses must implement `#on_message` to process received messages.
|
|
57
|
-
class EventHandler
|
|
61
|
+
class EventHandler[Payload = json_value]
|
|
58
62
|
extend ErrorClassification
|
|
59
63
|
|
|
60
64
|
# Process a single message received from Prosody.
|
|
@@ -64,7 +68,7 @@ module Prosody
|
|
|
64
68
|
# @param context the message context containing metadata and control capabilities
|
|
65
69
|
# @param message the message content with payload and metadata
|
|
66
70
|
# @raise [NotImplementedError] if not overridden by subclass
|
|
67
|
-
def on_message: (Context context, Message message) -> void
|
|
71
|
+
def on_message: (Context context, Message[Payload] message) -> void
|
|
68
72
|
|
|
69
73
|
# Process a timer that has fired.
|
|
70
74
|
# This method is called when a previously scheduled timer fires.
|
data/sig/processor.rbs
CHANGED
|
@@ -40,13 +40,17 @@ module Prosody
|
|
|
40
40
|
# OpenTelemetry context carrier for trace propagation
|
|
41
41
|
attr_reader carrier: Hash[String, String]
|
|
42
42
|
|
|
43
|
+
# Structured event fields for error reporting
|
|
44
|
+
attr_reader event_context: Hash[Symbol, untyped]
|
|
45
|
+
|
|
43
46
|
# Creates a new execute command with all required parameters
|
|
44
47
|
def initialize: (
|
|
45
|
-
task_id
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
String task_id,
|
|
49
|
+
Hash[String, String] carrier,
|
|
50
|
+
Hash[Symbol, untyped] event_context,
|
|
51
|
+
^() -> untyped block,
|
|
52
|
+
^(bool, untyped) -> untyped callback,
|
|
53
|
+
CancellationToken token
|
|
50
54
|
) -> void
|
|
51
55
|
end
|
|
52
56
|
|
|
@@ -82,19 +86,31 @@ module Prosody
|
|
|
82
86
|
# @yield the block to execute asynchronously
|
|
83
87
|
# @return token that can be used to cancel the task
|
|
84
88
|
def submit: (
|
|
85
|
-
task_id
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
String task_id,
|
|
90
|
+
Hash[String, String] carrier,
|
|
91
|
+
Hash[String | Symbol, untyped] event_context,
|
|
92
|
+
^(bool, untyped) -> untyped callback
|
|
88
93
|
) { () -> untyped } -> CancellationToken
|
|
89
94
|
|
|
90
95
|
private
|
|
91
96
|
|
|
97
|
+
def running?: () -> bool?
|
|
98
|
+
|
|
92
99
|
# Main processing loop for the async thread
|
|
93
100
|
def process_commands: () -> void
|
|
94
101
|
|
|
95
|
-
# Handles execution of a task with proper context propagation and error handling
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
def handle_execute: (Commands::Execute,
|
|
102
|
+
# Handles execution of a task with proper context propagation and error handling.
|
|
103
|
+
# The barrier belongs to the external async gem and is intentionally kept
|
|
104
|
+
# untyped rather than publishing a partial signature for that dependency.
|
|
105
|
+
def handle_execute: (Commands::Execute, untyped) -> void
|
|
106
|
+
|
|
107
|
+
def run_with_cancellation: (
|
|
108
|
+
String,
|
|
109
|
+
CancellationToken,
|
|
110
|
+
^() -> untyped,
|
|
111
|
+
^(bool, untyped) -> untyped,
|
|
112
|
+
untyped,
|
|
113
|
+
Hash[Symbol, untyped]
|
|
114
|
+
) -> void
|
|
99
115
|
end
|
|
100
116
|
end
|
data/sig/prosody.rbs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
module Prosody
|
|
2
|
+
# Any value representable by JSON. This is the default payload type for
|
|
3
|
+
# messages and handlers; applications can substitute a narrower record type.
|
|
4
|
+
type json_value = nil | bool | Integer | Float | String | Array[json_value] | Hash[String, json_value]
|
|
5
|
+
|
|
2
6
|
@logger: Logger?
|
|
3
7
|
|
|
4
8
|
# Returns the module-level logger, creating a default if not yet set.
|
|
@@ -65,7 +69,7 @@ module Prosody
|
|
|
65
69
|
|
|
66
70
|
# Represents a Kafka message with its metadata and payload.
|
|
67
71
|
# Provides access to message properties like topic, partition, offset, and content.
|
|
68
|
-
class Message
|
|
72
|
+
class Message[Payload = json_value]
|
|
69
73
|
# Returns the Kafka topic this message was published to.
|
|
70
74
|
# @return [String] The topic name
|
|
71
75
|
def topic: () -> String
|
|
@@ -87,8 +91,8 @@ module Prosody
|
|
|
87
91
|
def timestamp: () -> Time
|
|
88
92
|
|
|
89
93
|
# Returns the deserialized message payload.
|
|
90
|
-
# @return [
|
|
91
|
-
def payload: () ->
|
|
94
|
+
# @return [Payload] The message content
|
|
95
|
+
def payload: () -> Payload
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
# Represents a timer that was scheduled to fire at a specific time.
|
|
@@ -118,6 +122,7 @@ module Prosody
|
|
|
118
122
|
# @raise [ArgumentError] If the configuration is invalid
|
|
119
123
|
# @raise [RuntimeError] If client initialization fails
|
|
120
124
|
def self.new: (Hash[Symbol, untyped] | Configuration config) -> Client
|
|
125
|
+
| (**untyped options) -> Client
|
|
121
126
|
|
|
122
127
|
# Returns the current state of the consumer.
|
|
123
128
|
#
|
|
@@ -141,10 +146,10 @@ module Prosody
|
|
|
141
146
|
#
|
|
142
147
|
# @param topic [String] The destination topic name
|
|
143
148
|
# @param key [String] The message key for partitioning
|
|
144
|
-
# @param payload [
|
|
149
|
+
# @param payload [json_value] The message payload (will be serialized)
|
|
145
150
|
# @return [void]
|
|
146
151
|
# @raise [RuntimeError] If the message cannot be sent
|
|
147
|
-
def send_message: (String topic, String key,
|
|
152
|
+
def send_message: (String topic, String key, json_value payload) -> void
|
|
148
153
|
|
|
149
154
|
# Subscribes to Kafka topics using the provided handler.
|
|
150
155
|
# The handler must implement an `on_message(context, message)` method.
|
|
@@ -152,7 +157,7 @@ module Prosody
|
|
|
152
157
|
# @param handler [EventHandler] A handler object that processes messages
|
|
153
158
|
# @return [void]
|
|
154
159
|
# @raise [RuntimeError] If subscription fails
|
|
155
|
-
def subscribe: (EventHandler) -> void
|
|
160
|
+
def subscribe: [Payload] (EventHandler[Payload]) -> void
|
|
156
161
|
|
|
157
162
|
# Unsubscribes from all topics, stopping message processing.
|
|
158
163
|
#
|
data/sig/sentry.rbs
ADDED
data/sig/state.rbs
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
module Prosody
|
|
2
|
+
# --- error taxonomy -----------------------------------------------------
|
|
3
|
+
# Raised for keyed-state failures a retry cannot resolve (unregistered
|
|
4
|
+
# collection, identity mismatch, duplicate registration, bad TTL). Never
|
|
5
|
+
# Terminal: core folds Terminal into Transient.
|
|
6
|
+
class PermanentStateError < PermanentError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Raised for keyed-state failures that may succeed on retry, plus every
|
|
10
|
+
# caller mistake (null write, wrong item shape, invalid index, invalid
|
|
11
|
+
# direction). Never Terminal.
|
|
12
|
+
class TransientStateError < TransientError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Raised when a JSON null is written to a collection. Null is not storable
|
|
16
|
+
# (indistinguishable from absence); use clear/delete instead.
|
|
17
|
+
class NullValueError < TransientStateError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Concrete immutable definition object returned by the constructor helpers.
|
|
21
|
+
class StateDefinition < Data
|
|
22
|
+
attr_reader name: String
|
|
23
|
+
attr_reader kind: String
|
|
24
|
+
attr_reader payload: String
|
|
25
|
+
attr_reader ttl_seconds: Integer?
|
|
26
|
+
attr_reader read_uncommitted: bool?
|
|
27
|
+
attr_reader keyset_limit: Integer?
|
|
28
|
+
attr_reader capacity: Integer?
|
|
29
|
+
|
|
30
|
+
def to_state_config: () -> Hash[Symbol, untyped]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# --- definition interfaces (phantom-typed over the payload) --------------
|
|
34
|
+
# The runtime returns a single frozen StateDefinition (Data) that structurally
|
|
35
|
+
# satisfies every interface below; the type parameter is annotation-only.
|
|
36
|
+
interface _ValueDefinition[T = json_value]
|
|
37
|
+
def name: () -> String
|
|
38
|
+
def kind: () -> "value"
|
|
39
|
+
def payload: () -> "json"
|
|
40
|
+
def to_state_config: () -> Hash[Symbol, untyped]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
interface _MapDefinition[V = json_value]
|
|
44
|
+
def name: () -> String
|
|
45
|
+
def kind: () -> "map"
|
|
46
|
+
def payload: () -> "json"
|
|
47
|
+
def to_state_config: () -> Hash[Symbol, untyped]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
interface _DequeDefinition[T = json_value]
|
|
51
|
+
def name: () -> String
|
|
52
|
+
def kind: () -> "deque"
|
|
53
|
+
def payload: () -> "json"
|
|
54
|
+
def to_state_config: () -> Hash[Symbol, untyped]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
interface _MessageValueDefinition[P = json_value]
|
|
58
|
+
def name: () -> String
|
|
59
|
+
def kind: () -> "value"
|
|
60
|
+
def payload: () -> "message"
|
|
61
|
+
def to_state_config: () -> Hash[Symbol, untyped]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
interface _MessageMapDefinition[P = json_value]
|
|
65
|
+
def name: () -> String
|
|
66
|
+
def kind: () -> "map"
|
|
67
|
+
def payload: () -> "message"
|
|
68
|
+
def to_state_config: () -> Hash[Symbol, untyped]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
interface _MessageDequeDefinition[P = json_value]
|
|
72
|
+
def name: () -> String
|
|
73
|
+
def kind: () -> "deque"
|
|
74
|
+
def payload: () -> "message"
|
|
75
|
+
def to_state_config: () -> Hash[Symbol, untyped]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# --- definition constructors --------------------------------------------
|
|
79
|
+
# The `message_*` siblings return the same structural definition interfaces as
|
|
80
|
+
# their JSON counterparts (the runtime object is one StateDefinition); the
|
|
81
|
+
# `[P]` type parameter is annotation-only, naming the message payload type.
|
|
82
|
+
def self.value: [T] (_ToS name, ?ttl: Integer?, ?read_uncommitted: bool?) -> _ValueDefinition[T]
|
|
83
|
+
def self.map: [V] (_ToS name, ?ttl: Integer?, ?keyset_limit: Integer?, ?read_uncommitted: bool?) -> _MapDefinition[V]
|
|
84
|
+
def self.deque: [T] (_ToS name, ?ttl: Integer?, ?capacity: Integer?, ?read_uncommitted: bool?) -> _DequeDefinition[T]
|
|
85
|
+
def self.message_value: [P] (_ToS name, ?ttl: Integer?, ?read_uncommitted: bool?) -> _MessageValueDefinition[P]
|
|
86
|
+
def self.message_map: [P] (_ToS name, ?ttl: Integer?, ?keyset_limit: Integer?, ?read_uncommitted: bool?) -> _MessageMapDefinition[P]
|
|
87
|
+
def self.message_deque: [P] (_ToS name, ?ttl: Integer?, ?capacity: Integer?, ?read_uncommitted: bool?) -> _MessageDequeDefinition[P]
|
|
88
|
+
|
|
89
|
+
# --- handles ------------------------------------------------------------
|
|
90
|
+
# A single-value keyed-state handle. Reads return the stored value or nil.
|
|
91
|
+
# Every op is fiber-yield async: it looks blocking but never blocks the thread.
|
|
92
|
+
class ValueState[T = json_value]
|
|
93
|
+
def get: () -> T?
|
|
94
|
+
def set: (T value) -> void
|
|
95
|
+
def clear: () -> void
|
|
96
|
+
def commit: () -> nil
|
|
97
|
+
def rollback: () -> nil
|
|
98
|
+
|
|
99
|
+
# idiomatic conveniences
|
|
100
|
+
def value: () -> T? # alias of #get
|
|
101
|
+
def value=: (T value) -> T # alias of #set (assignment yields value)
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
def initialize: (NativeValueState native) -> void
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A String-keyed ordered-map keyed-state handle.
|
|
108
|
+
class MapState[V = json_value]
|
|
109
|
+
include State::Scanning
|
|
110
|
+
|
|
111
|
+
def get: (String key) -> V?
|
|
112
|
+
def get_many: (Array[String] keys) -> Array[V?]
|
|
113
|
+
def set: (String key, V value) -> void
|
|
114
|
+
def delete: (String key) -> nil
|
|
115
|
+
def clear: () -> void
|
|
116
|
+
def commit: () -> nil
|
|
117
|
+
def rollback: () -> nil
|
|
118
|
+
def each_pair: () { (String, V) -> void } -> void
|
|
119
|
+
| () -> Enumerator[[ String, V ], void]
|
|
120
|
+
def reverse_each_pair: () { (String, V) -> void } -> void
|
|
121
|
+
| () -> Enumerator[[ String, V ], void]
|
|
122
|
+
def each_key: () { (String) -> void } -> void
|
|
123
|
+
| () -> Enumerator[String, void]
|
|
124
|
+
def reverse_each_key: () { (String) -> void } -> void
|
|
125
|
+
| () -> Enumerator[String, void]
|
|
126
|
+
|
|
127
|
+
# idiomatic Hash-style conveniences (bounded reads only)
|
|
128
|
+
def []: (String key) -> V? # alias of #get
|
|
129
|
+
def []=: (String key, V value) -> V # alias of #set (assignment yields value)
|
|
130
|
+
def store: (String key, V value) -> V # wrapper over #set, returns value
|
|
131
|
+
def each: () { (String, V) -> void } -> void # alias of #each_pair
|
|
132
|
+
| () -> Enumerator[[ String, V ], void]
|
|
133
|
+
def values_at: (*String keys) -> Array[V?]
|
|
134
|
+
def fetch: (String key) -> V
|
|
135
|
+
| [D] (String key, D default) -> (V | D)
|
|
136
|
+
| [R] (String key) { (String) -> R } -> (V | R)
|
|
137
|
+
def key?: (String key) -> bool
|
|
138
|
+
def has_key?: (String key) -> bool
|
|
139
|
+
def include?: (String key) -> bool
|
|
140
|
+
def member?: (String key) -> bool
|
|
141
|
+
def dig: (String key, *untyped) -> untyped
|
|
142
|
+
def slice: (*String keys) -> Hash[String, V]
|
|
143
|
+
def fetch_values: (*String keys) -> Array[V]
|
|
144
|
+
| [R] (*String keys) { (String) -> R } -> Array[V | R]
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
def initialize: (NativeMapState native) -> void
|
|
148
|
+
def traverse: (Symbol direction) { (String, V) -> void } -> void
|
|
149
|
+
| (Symbol direction) -> Enumerator[[ String, V ], void]
|
|
150
|
+
def traverse_keys: (Symbol direction) { (String) -> void } -> void
|
|
151
|
+
| (Symbol direction) -> Enumerator[String, void]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A deque keyed-state handle.
|
|
155
|
+
class DequeState[T = json_value]
|
|
156
|
+
include State::Scanning
|
|
157
|
+
|
|
158
|
+
def push: (T value) -> void
|
|
159
|
+
def unshift: (T value) -> void
|
|
160
|
+
def pop: () -> T?
|
|
161
|
+
def shift: () -> T?
|
|
162
|
+
def length: () -> Integer
|
|
163
|
+
def size: () -> Integer
|
|
164
|
+
def empty?: () -> bool
|
|
165
|
+
def clear: () -> void
|
|
166
|
+
def commit: () -> nil
|
|
167
|
+
def rollback: () -> nil
|
|
168
|
+
def get: (Integer index) -> T?
|
|
169
|
+
def each: () { (T) -> void } -> void
|
|
170
|
+
| () -> Enumerator[T, void]
|
|
171
|
+
def reverse_each: () { (T) -> void } -> void
|
|
172
|
+
| () -> Enumerator[T, void]
|
|
173
|
+
|
|
174
|
+
# idiomatic Array-style conveniences (bounded reads only). #get and #fetch
|
|
175
|
+
# take a single Integer index (negatives resolve from the back, Array-style);
|
|
176
|
+
# #[] and #at are deliberately absent because they would invite a range read
|
|
177
|
+
# the remote deque cannot honor.
|
|
178
|
+
def prepend: (T value) -> self # wrapper over #unshift
|
|
179
|
+
def append: (T value) -> self # wrapper over #push
|
|
180
|
+
def <<: (T value) -> self
|
|
181
|
+
def first: () -> T?
|
|
182
|
+
def last: () -> T?
|
|
183
|
+
def fetch: (Integer index) -> T
|
|
184
|
+
| [D] (Integer index, D default) -> (T | D)
|
|
185
|
+
| [R] (Integer index) { (Integer) -> R } -> (T | R)
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
def initialize: (NativeDequeState native) -> void
|
|
189
|
+
def at_negative: (Integer index) -> T?
|
|
190
|
+
def traverse: (Symbol direction) { (T) -> void } -> void
|
|
191
|
+
| (Symbol direction) -> Enumerator[T, void]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Native single-value handle (Prosody::NativeValueState), wrapped by ValueState.
|
|
195
|
+
class NativeValueState
|
|
196
|
+
def get: () -> untyped
|
|
197
|
+
def set: (untyped value) -> void
|
|
198
|
+
def clear: () -> void
|
|
199
|
+
def commit: () -> nil
|
|
200
|
+
def rollback: () -> nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Native ordered-map handle (Prosody::NativeMapState), wrapped by MapState.
|
|
204
|
+
class NativeMapState
|
|
205
|
+
def get: (String key) -> untyped
|
|
206
|
+
def contains_key: (String key) -> bool
|
|
207
|
+
def get_many: (Array[String] keys) -> Array[untyped]
|
|
208
|
+
def set: (String key, untyped value) -> void
|
|
209
|
+
def remove: (String key) -> void
|
|
210
|
+
def clear: () -> void
|
|
211
|
+
def scan: (String direction) -> StateScan
|
|
212
|
+
def keys: (String direction) -> StateScan
|
|
213
|
+
def commit: () -> nil
|
|
214
|
+
def rollback: () -> nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Native deque handle (Prosody::NativeDequeState), wrapped by DequeState.
|
|
218
|
+
class NativeDequeState
|
|
219
|
+
def len: () -> Integer
|
|
220
|
+
def is_empty: () -> bool
|
|
221
|
+
def get: (Integer index) -> untyped
|
|
222
|
+
def peek_front: () -> untyped
|
|
223
|
+
def peek_back: () -> untyped
|
|
224
|
+
def push_back: (untyped value) -> void
|
|
225
|
+
def push_front: (untyped value) -> void
|
|
226
|
+
def pop_front: () -> untyped
|
|
227
|
+
def pop_back: () -> untyped
|
|
228
|
+
def clear: () -> void
|
|
229
|
+
def scan: (String direction) -> StateScan
|
|
230
|
+
def commit: () -> nil
|
|
231
|
+
def rollback: () -> nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Native scan cursor (Prosody::StateScan), driven by the wrapper traversal.
|
|
235
|
+
class StateScan
|
|
236
|
+
def next: () -> untyped
|
|
237
|
+
def close: () -> void
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# --- internal routing module --------------------------------------------
|
|
241
|
+
module State
|
|
242
|
+
VEND: Hash[Array[String], [ Symbol, Symbol ]]
|
|
243
|
+
|
|
244
|
+
# Adds keyed-state vending to the native context.
|
|
245
|
+
module Vending
|
|
246
|
+
def state: [T] (_ValueDefinition[T]) -> ValueState[T]
|
|
247
|
+
| [V] (_MapDefinition[V]) -> MapState[V]
|
|
248
|
+
| [T] (_DequeDefinition[T]) -> DequeState[T]
|
|
249
|
+
| [P] (_MessageValueDefinition[P]) -> ValueState[Message[P]]
|
|
250
|
+
| [P] (_MessageMapDefinition[P]) -> MapState[Message[P]]
|
|
251
|
+
| [P] (_MessageDequeDefinition[P]) -> DequeState[Message[P]]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Shared native-scan driving mixed into the traversal handles.
|
|
255
|
+
module Scanning
|
|
256
|
+
private
|
|
257
|
+
def scan_each: (Symbol direction, ?Symbol opener) { (untyped) -> void } -> void
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Reopened to add keyed-state vending (see lib/prosody/state.rb).
|
|
262
|
+
class Context
|
|
263
|
+
include State::Vending
|
|
264
|
+
|
|
265
|
+
def value_state: (String name) -> NativeValueState
|
|
266
|
+
def map_state: (String name) -> NativeMapState
|
|
267
|
+
def deque_state: (String name) -> NativeDequeState
|
|
268
|
+
def message_value_state: (String name) -> NativeValueState
|
|
269
|
+
def message_map_state: (String name) -> NativeMapState
|
|
270
|
+
def message_deque_state: (String name) -> NativeDequeState
|
|
271
|
+
end
|
|
272
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
- file: typecheck_negative/payload_types.rb
|
|
3
|
+
diagnostics:
|
|
4
|
+
- range:
|
|
5
|
+
start:
|
|
6
|
+
line: 10
|
|
7
|
+
character: 41
|
|
8
|
+
end:
|
|
9
|
+
line: 10
|
|
10
|
+
character: 51
|
|
11
|
+
severity: ERROR
|
|
12
|
+
message: |-
|
|
13
|
+
Cannot pass a value of type `::Object` as an argument of type `::Prosody::json_value`
|
|
14
|
+
::Object <: ::Prosody::json_value
|
|
15
|
+
::Object <: (nil | bool | ::Integer | ::Float | ::String | ::Array[::Prosody::json_value] | ::Hash[::String, ::Prosody::json_value])
|
|
16
|
+
::Object <: nil
|
|
17
|
+
code: Ruby::ArgumentTypeMismatch
|
|
18
|
+
- range:
|
|
19
|
+
start:
|
|
20
|
+
line: 13
|
|
21
|
+
character: 22
|
|
22
|
+
end:
|
|
23
|
+
line: 13
|
|
24
|
+
character: 38
|
|
25
|
+
severity: ERROR
|
|
26
|
+
message: |-
|
|
27
|
+
Cannot pass a value of type `::String` as an argument of type `::Integer`
|
|
28
|
+
::String <: ::Integer
|
|
29
|
+
::Object <: ::Integer
|
|
30
|
+
::BasicObject <: ::Integer
|
|
31
|
+
code: Ruby::ArgumentTypeMismatch
|
|
32
|
+
- range:
|
|
33
|
+
start:
|
|
34
|
+
line: 14
|
|
35
|
+
character: 42
|
|
36
|
+
end:
|
|
37
|
+
line: 14
|
|
38
|
+
character: 57
|
|
39
|
+
severity: ERROR
|
|
40
|
+
message: |-
|
|
41
|
+
Cannot pass a value of type `::negative_event` as an argument of type `::Prosody::Message[untyped]`
|
|
42
|
+
::negative_event <: ::Prosody::Message[untyped]
|
|
43
|
+
{ "amount" => ::Integer } <: ::Prosody::Message[untyped]
|
|
44
|
+
::Hash["amount", ::Integer] <: ::Prosody::Message[untyped]
|
|
45
|
+
::Object <: ::Prosody::Message[untyped]
|
|
46
|
+
::BasicObject <: ::Prosody::Message[untyped]
|
|
47
|
+
code: Ruby::ArgumentTypeMismatch
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is checked by Steep as a consumer of Prosody's public signatures.
|
|
4
|
+
# It is not loaded at runtime.
|
|
5
|
+
|
|
6
|
+
ORDER_BACKLOG = Prosody.message_deque("order-backlog")
|
|
7
|
+
ORDER_TOTALS = Prosody.map("order-totals")
|
|
8
|
+
|
|
9
|
+
class TypedOrderHandler < Prosody::EventHandler
|
|
10
|
+
def on_message(context, message)
|
|
11
|
+
order_id = message.payload["order_id"]
|
|
12
|
+
total = message.payload["total"]
|
|
13
|
+
|
|
14
|
+
# Message-backed state preserves the handler's payload type.
|
|
15
|
+
backlog = context.state(ORDER_BACKLOG)
|
|
16
|
+
backlog.push(message)
|
|
17
|
+
oldest = backlog.first
|
|
18
|
+
|
|
19
|
+
# This call is a regression constraint: state(ORDER_TOTALS) must infer as
|
|
20
|
+
# MapState[Integer], not untyped or a generic JSON-valued state handle.
|
|
21
|
+
consume_totals(context.state(ORDER_TOTALS))
|
|
22
|
+
|
|
23
|
+
consume_order(order_id, total)
|
|
24
|
+
consume_order(oldest.payload["order_id"], oldest.payload["total"]) if oldest
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def consume_order(order_id, total)
|
|
30
|
+
"#{order_id.upcase}:#{total + 1}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def consume_totals(totals)
|
|
34
|
+
totals.set("latest", 1)
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class DefaultPayloadConsumer
|
|
40
|
+
def payload(message)
|
|
41
|
+
message.payload
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type order_event = {
|
|
2
|
+
"order_id" => String,
|
|
3
|
+
"total" => Integer
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
ORDER_BACKLOG: Prosody::_MessageDequeDefinition[order_event]
|
|
7
|
+
ORDER_TOTALS: Prosody::_MapDefinition[Integer]
|
|
8
|
+
|
|
9
|
+
class TypedOrderHandler < Prosody::EventHandler[order_event]
|
|
10
|
+
def on_message: (Prosody::Context context, Prosody::Message[order_event] message) -> void
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def consume_order: (String order_id, Integer total) -> String
|
|
15
|
+
def consume_totals: (Prosody::MapState[Integer] totals) -> nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class DefaultPayloadConsumer
|
|
19
|
+
def payload: (Prosody::Message message) -> Prosody::json_value
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Deliberately invalid calls. Diagnostics are committed in
|
|
4
|
+
# steep_expectations.yml, so Steep fails if these errors disappear.
|
|
5
|
+
NEGATIVE_TOTALS = Prosody.map("negative-totals")
|
|
6
|
+
NEGATIVE_MESSAGES = Prosody.message_deque("negative-messages")
|
|
7
|
+
|
|
8
|
+
class NegativeTypeChecks
|
|
9
|
+
def check(client, context, message)
|
|
10
|
+
client.send_message("events", "key", Object.new)
|
|
11
|
+
# @type var totals: Prosody::MapState[Integer]
|
|
12
|
+
totals = context.state(NEGATIVE_TOTALS)
|
|
13
|
+
totals.set("key", "not-an-integer")
|
|
14
|
+
context.state(NEGATIVE_MESSAGES).push(message.payload)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type negative_event = { "amount" => Integer }
|
|
2
|
+
|
|
3
|
+
NEGATIVE_TOTALS: Prosody::_MapDefinition[Integer]
|
|
4
|
+
NEGATIVE_MESSAGES: Prosody::_MessageDequeDefinition[negative_event]
|
|
5
|
+
|
|
6
|
+
class NegativeTypeChecks
|
|
7
|
+
def check: (Prosody::Client client, Prosody::Context context, Prosody::Message[negative_event] message) -> void
|
|
8
|
+
end
|