takagi 0.1.0 → 1.1.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/.rubocop.yml +70 -7
- data/.yard/templates/default/layout/html/layout.erb +34 -0
- data/AGENTS.md +16 -0
- data/CHANGELOG.md +158 -1
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +590 -23
- data/ROADMAP.md +55 -0
- data/Rakefile +4 -4
- data/Steepfile +39 -0
- data/bin/takagi-dev +159 -0
- data/docs/FIRST_PLUGIN_GUIDE.md +224 -0
- data/docs/HOOKS.md +31 -0
- data/examples/client_lifecycle_example.rb +118 -0
- data/examples/cloud_gateway_app.rb +217 -0
- data/examples/nested_api_app.rb +258 -0
- data/examples/simple_device_app.rb +71 -0
- data/examples/takagi.yml +138 -0
- data/lib/takagi/application.rb +256 -0
- data/lib/takagi/base/middleware_management.rb +39 -0
- data/lib/takagi/base/plugin_management.rb +75 -0
- data/lib/takagi/base/reactor_management.rb +104 -0
- data/lib/takagi/base/server_lifecycle.rb +156 -0
- data/lib/takagi/base.rb +103 -11
- data/lib/takagi/branding.rb +88 -0
- data/lib/takagi/cbor/decoder.rb +385 -0
- data/lib/takagi/cbor/encoder.rb +260 -0
- data/lib/takagi/cbor/error.rb +17 -0
- data/lib/takagi/cbor/version.rb +9 -0
- data/lib/takagi/client/response.rb +236 -0
- data/lib/takagi/client.rb +265 -0
- data/lib/takagi/client_base.rb +204 -0
- data/lib/takagi/coap/code_helpers.rb +190 -0
- data/lib/takagi/coap/registries/base.rb +165 -0
- data/lib/takagi/coap/registries/content_format.rb +71 -0
- data/lib/takagi/coap/registries/message_type.rb +69 -0
- data/lib/takagi/coap/registries/method.rb +38 -0
- data/lib/takagi/coap/registries/option.rb +71 -0
- data/lib/takagi/coap/registries/response.rb +93 -0
- data/lib/takagi/coap/registries/signaling.rb +34 -0
- data/lib/takagi/coap/signaling.rb +10 -0
- data/lib/takagi/coap.rb +37 -0
- data/lib/takagi/composite_router.rb +186 -0
- data/lib/takagi/config.rb +337 -0
- data/lib/takagi/controller/resource_allocator.rb +164 -0
- data/lib/takagi/controller/thread_pool.rb +144 -0
- data/lib/takagi/controller.rb +319 -0
- data/lib/takagi/core/attribute_set.rb +128 -0
- data/lib/takagi/discovery/core_link_format.rb +137 -0
- data/lib/takagi/errors.rb +536 -0
- data/lib/takagi/event_bus/address_prefix.rb +142 -0
- data/lib/takagi/event_bus/async_executor.rb +235 -0
- data/lib/takagi/event_bus/coap_bridge.rb +208 -0
- data/lib/takagi/event_bus/future.rb +153 -0
- data/lib/takagi/event_bus/lru_cache.rb +157 -0
- data/lib/takagi/event_bus/message_buffer.rb +237 -0
- data/lib/takagi/event_bus/observer_cleanup.rb +110 -0
- data/lib/takagi/event_bus/scope.rb +74 -0
- data/lib/takagi/event_bus.rb +594 -0
- data/lib/takagi/helpers.rb +88 -0
- data/lib/takagi/hooks.rb +82 -0
- data/lib/takagi/initializer.rb +18 -0
- data/lib/takagi/logger.rb +15 -6
- data/lib/takagi/message/base.rb +155 -0
- data/lib/takagi/message/deduplication_cache.rb +84 -0
- data/lib/takagi/message/inbound.rb +147 -0
- data/lib/takagi/message/outbound.rb +223 -0
- data/lib/takagi/message/request.rb +158 -0
- data/lib/takagi/message/retransmission_manager.rb +193 -0
- data/lib/takagi/middleware/authentication.rb +19 -0
- data/lib/takagi/middleware/caching.rb +23 -0
- data/lib/takagi/middleware/debugging.rb +16 -0
- data/lib/takagi/middleware/logging.rb +14 -0
- data/lib/takagi/middleware/metrics.rb +440 -0
- data/lib/takagi/middleware/rate_limiting.rb +24 -0
- data/lib/takagi/middleware_stack.rb +166 -0
- data/lib/takagi/network/base.rb +76 -0
- data/lib/takagi/network/framing/tcp.rb +222 -0
- data/lib/takagi/network/framing/udp.rb +110 -0
- data/lib/takagi/network/registry.rb +72 -0
- data/lib/takagi/network/tcp.rb +60 -0
- data/lib/takagi/network/tcp_sender.rb +21 -0
- data/lib/takagi/network/udp.rb +61 -0
- data/lib/takagi/network/udp_sender.rb +20 -0
- data/lib/takagi/observable/emitter.rb +62 -0
- data/lib/takagi/observable/reactor.rb +488 -0
- data/lib/takagi/observable/registry.rb +122 -0
- data/lib/takagi/observe_registry.rb +10 -0
- data/lib/takagi/observer/client.rb +68 -0
- data/lib/takagi/observer/registry.rb +137 -0
- data/lib/takagi/observer/sender.rb +39 -0
- data/lib/takagi/observer/watcher.rb +43 -0
- data/lib/takagi/plugin.rb +313 -0
- data/lib/takagi/profiles.rb +176 -0
- data/lib/takagi/reactor.rb +23 -0
- data/lib/takagi/reactor_registry.rb +64 -0
- data/lib/takagi/registry/base.rb +268 -0
- data/lib/takagi/response_builder.rb +141 -0
- data/lib/takagi/router/metadata_extractor.rb +133 -0
- data/lib/takagi/router/route_matcher.rb +83 -0
- data/lib/takagi/router.rb +284 -25
- data/lib/takagi/serialization/base.rb +102 -0
- data/lib/takagi/serialization/cbor_serializer.rb +92 -0
- data/lib/takagi/serialization/json_serializer.rb +96 -0
- data/lib/takagi/serialization/octet_stream_serializer.rb +82 -0
- data/lib/takagi/serialization/registry.rb +187 -0
- data/lib/takagi/serialization/text_serializer.rb +87 -0
- data/lib/takagi/serialization.rb +117 -0
- data/lib/takagi/server/multi.rb +41 -0
- data/lib/takagi/server/registry.rb +71 -0
- data/lib/takagi/server/tcp.rb +249 -0
- data/lib/takagi/server/udp.rb +139 -0
- data/lib/takagi/server/udp_worker.rb +174 -0
- data/lib/takagi/server.rb +1 -31
- data/lib/takagi/server_registry.rb +10 -0
- data/lib/takagi/tcp_client.rb +142 -0
- data/lib/takagi/version.rb +2 -1
- data/lib/takagi.rb +24 -3
- data/sig/takagi/application.rbs +48 -0
- data/sig/takagi/base/middleware_management.rbs +33 -0
- data/sig/takagi/base/reactor_management.rbs +52 -0
- data/sig/takagi/base/server_lifecycle.rbs +54 -0
- data/sig/takagi/base.rbs +48 -0
- data/sig/takagi/cbor/decoder.rbs +171 -0
- data/sig/takagi/cbor/encoder.rbs +146 -0
- data/sig/takagi/cbor/error.rbs +19 -0
- data/sig/takagi/cbor/version.rbs +7 -0
- data/sig/takagi/client/response.rbs +148 -0
- data/sig/takagi/client.rbs +119 -0
- data/sig/takagi/client_base.rbs +135 -0
- data/sig/takagi/coap/code_helpers.rbs +91 -0
- data/sig/takagi/coap/registries/base.rbs +95 -0
- data/sig/takagi/coap/registries/content_format.rbs +47 -0
- data/sig/takagi/coap/registries/message_type.rbs +53 -0
- data/sig/takagi/coap/registries/method.rbs +27 -0
- data/sig/takagi/coap/registries/option.rbs +43 -0
- data/sig/takagi/coap/registries/response.rbs +52 -0
- data/sig/takagi/coap.rbs +24 -0
- data/sig/takagi/composite_router.rbs +46 -0
- data/sig/takagi/config.rbs +134 -0
- data/sig/takagi/controller.rbs +73 -0
- data/sig/takagi/core/attribute_set.rbs +57 -0
- data/sig/takagi/discovery/core_link_format.rbs +50 -0
- data/sig/takagi/event_bus/address_prefix.rbs +78 -0
- data/sig/takagi/event_bus/async_executor.rbs +88 -0
- data/sig/takagi/event_bus/coap_bridge.rbs +93 -0
- data/sig/takagi/event_bus/future.rbs +78 -0
- data/sig/takagi/event_bus/lru_cache.rbs +86 -0
- data/sig/takagi/event_bus/message_buffer.rbs +133 -0
- data/sig/takagi/event_bus/observer_cleanup.rbs +62 -0
- data/sig/takagi/event_bus.rbs +320 -0
- data/sig/takagi/helpers.rbs +34 -0
- data/sig/takagi/initializer.rbs +9 -0
- data/sig/takagi/logger.rbs +17 -0
- data/sig/takagi/message/base.rbs +64 -0
- data/sig/takagi/message/deduplication_cache.rbs +49 -0
- data/sig/takagi/message/inbound.rbs +76 -0
- data/sig/takagi/message/outbound.rbs +48 -0
- data/sig/takagi/message/request.rbs +32 -0
- data/sig/takagi/message/retransmission_manager.rbs +76 -0
- data/sig/takagi/middleware/authentication.rbs +11 -0
- data/sig/takagi/middleware/caching.rbs +13 -0
- data/sig/takagi/middleware/debugging.rbs +9 -0
- data/sig/takagi/middleware/logging.rbs +7 -0
- data/sig/takagi/middleware/metrics.rbs +15 -0
- data/sig/takagi/middleware/rate_limiting.rbs +13 -0
- data/sig/takagi/middleware_stack.rbs +69 -0
- data/sig/takagi/network/tcp_sender.rbs +10 -0
- data/sig/takagi/network/udp_sender.rbs +14 -0
- data/sig/takagi/observe_registry.rbs +36 -0
- data/sig/takagi/observer/client.rbs +36 -0
- data/sig/takagi/observer/sender.rbs +12 -0
- data/sig/takagi/observer/watcher.rbs +18 -0
- data/sig/takagi/profiles.rbs +33 -0
- data/sig/takagi/reactor.rbs +20 -0
- data/sig/takagi/reactor_registry.rbs +14 -0
- data/sig/takagi/response_builder.rbs +12 -0
- data/sig/takagi/router/metadata_extractor.rbs +71 -0
- data/sig/takagi/router/route_matcher.rbs +43 -0
- data/sig/takagi/router.rbs +166 -0
- data/sig/takagi/serialization.rbs +32 -0
- data/sig/takagi/server/multi.rbs +16 -0
- data/sig/takagi/server/tcp.rbs +42 -0
- data/sig/takagi/server/udp.rbs +52 -0
- data/sig/takagi/server/udp_worker.rbs +42 -0
- data/sig/takagi/server.rbs +4 -0
- data/sig/takagi/server_registry.rbs +71 -0
- data/sig/takagi/tcp_client.rbs +23 -0
- data/sig/takagi/version.rbs +5 -0
- data/takagi.gemspec +37 -35
- metadata +204 -31
- data/.idea/.gitignore +0 -8
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/takagi.iml +0 -81
- data/.idea/vcs.xml +0 -6
- data/lib/takagi/message.rb +0 -75
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
# High-level event distribution with threaded/process async delivery.
|
|
3
|
+
# Built on top of ObserveRegistry with zero runtime dependencies.
|
|
4
|
+
#
|
|
5
|
+
# Supports both CoAP Observe style and Pub/Sub style APIs:
|
|
6
|
+
#
|
|
7
|
+
# @example CoAP Observe style
|
|
8
|
+
# EventBus.observe('sensor.temperature.room1') { |msg| puts msg.body }
|
|
9
|
+
# EventBus.notify('sensor.temperature.room1', { value: 25.5 })
|
|
10
|
+
#
|
|
11
|
+
# @example Pub/Sub style (aliases)
|
|
12
|
+
# EventBus.consumer('sensor.temperature.room1') { |msg| puts msg.body }
|
|
13
|
+
# EventBus.publish('sensor.temperature.room1', { value: 25.5 })
|
|
14
|
+
#
|
|
15
|
+
# @example Request-Reply pattern
|
|
16
|
+
# reply = EventBus.send_sync('cache.query', { key: 'user:123' }, timeout: 1.0)
|
|
17
|
+
class EventBus
|
|
18
|
+
self.@executor: untyped
|
|
19
|
+
|
|
20
|
+
self.@handlers: untyped
|
|
21
|
+
|
|
22
|
+
self.@consumers: untyped
|
|
23
|
+
|
|
24
|
+
self.@handler_store: untyped
|
|
25
|
+
|
|
26
|
+
self.@mutex: untyped
|
|
27
|
+
|
|
28
|
+
self.@current_states: untyped
|
|
29
|
+
|
|
30
|
+
self.@cleanup: untyped
|
|
31
|
+
|
|
32
|
+
self.@last_index: untyped
|
|
33
|
+
|
|
34
|
+
self.@message_store: untyped
|
|
35
|
+
|
|
36
|
+
class Error < StandardError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Event message wrapper (shareable for Ractor)
|
|
40
|
+
class Message
|
|
41
|
+
@address: untyped
|
|
42
|
+
|
|
43
|
+
@body: untyped
|
|
44
|
+
|
|
45
|
+
@headers: untyped
|
|
46
|
+
|
|
47
|
+
@reply_address: untyped
|
|
48
|
+
|
|
49
|
+
@timestamp: untyped
|
|
50
|
+
|
|
51
|
+
attr_reader address: untyped
|
|
52
|
+
|
|
53
|
+
attr_reader body: untyped
|
|
54
|
+
|
|
55
|
+
attr_reader headers: untyped
|
|
56
|
+
|
|
57
|
+
attr_reader reply_address: untyped
|
|
58
|
+
|
|
59
|
+
attr_reader timestamp: untyped
|
|
60
|
+
|
|
61
|
+
def initialize: (untyped address, untyped body, ?headers: ::Hash[untyped, untyped], ?reply_address: untyped?) -> void
|
|
62
|
+
|
|
63
|
+
# Reply to this message (request-reply pattern)
|
|
64
|
+
def reply: (untyped body, ?headers: ::Hash[untyped, untyped]) -> (nil | untyped)
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def deep_freeze: (untyped obj) -> untyped
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Event handler wrapper
|
|
72
|
+
class Handler
|
|
73
|
+
@address: untyped
|
|
74
|
+
|
|
75
|
+
@block: untyped
|
|
76
|
+
|
|
77
|
+
@options: untyped
|
|
78
|
+
|
|
79
|
+
@local_only: untyped
|
|
80
|
+
|
|
81
|
+
@pool_id: untyped
|
|
82
|
+
|
|
83
|
+
attr_reader address: untyped
|
|
84
|
+
|
|
85
|
+
attr_reader block: untyped
|
|
86
|
+
|
|
87
|
+
attr_reader options: untyped
|
|
88
|
+
|
|
89
|
+
attr_reader pool_id: untyped
|
|
90
|
+
|
|
91
|
+
def initialize: (untyped address, ?::Hash[untyped, untyped] options) { (?) -> untyped } -> void
|
|
92
|
+
|
|
93
|
+
def call: (untyped message) -> untyped
|
|
94
|
+
|
|
95
|
+
def local_only?: () -> untyped
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Helper method to get configuration with fallback to ENV
|
|
99
|
+
def self.config_value: (untyped config_key, untyped env_key, untyped default) -> untyped
|
|
100
|
+
|
|
101
|
+
# Publish message to all subscribers (pub/sub pattern)
|
|
102
|
+
# @param address [String] Event address (e.g., "sensor.temperature.room1")
|
|
103
|
+
# @param body [Object] Message body (must be shareable for Ractors)
|
|
104
|
+
# @param headers [Hash] Optional message headers
|
|
105
|
+
# @return [Message] Published message
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# EventBus.publish('sensor.temperature.room1', { value: 25.5, unit: 'C' })
|
|
109
|
+
def self.publish: (untyped address, ?untyped? body, ?headers: ::Hash[untyped, untyped]) -> untyped
|
|
110
|
+
|
|
111
|
+
# Send message to single consumer (point-to-point pattern)
|
|
112
|
+
# Uses round-robin if multiple consumers registered
|
|
113
|
+
# @param address [String] Event address
|
|
114
|
+
# @param body [Object] Message body
|
|
115
|
+
# @param headers [Hash] Optional headers
|
|
116
|
+
# @yield [reply_message] Optional reply handler (request-reply pattern)
|
|
117
|
+
# @return [Message] Sent message
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# EventBus.send('cache.query', { key: 'user:123' }) do |reply|
|
|
121
|
+
# puts "Cache value: #{reply.body[:value]}"
|
|
122
|
+
# end
|
|
123
|
+
def self.send: (untyped address, ?untyped? body, ?headers: ::Hash[untyped, untyped]) ?{ (?) -> untyped } -> untyped
|
|
124
|
+
|
|
125
|
+
# Synchronous send with timeout (blocking)
|
|
126
|
+
# @param address [String] Event address
|
|
127
|
+
# @param body [Object] Message body
|
|
128
|
+
# @param headers [Hash] Optional headers
|
|
129
|
+
# @param timeout [Float] Timeout in seconds (default: 1.0)
|
|
130
|
+
# @return [Message] Reply message
|
|
131
|
+
# @raise [Timeout::Error] If no reply received within timeout
|
|
132
|
+
#
|
|
133
|
+
# @example
|
|
134
|
+
# reply = EventBus.send_sync('cache.query', { key: 'user:123' }, timeout: 1.0)
|
|
135
|
+
# puts "Cache hit: #{reply.body[:hit]}"
|
|
136
|
+
def self.send_sync: (untyped address, ?untyped? body, ?headers: ::Hash[untyped, untyped], ?timeout: ::Float) -> untyped
|
|
137
|
+
|
|
138
|
+
# Asynchronous send with Future (non-blocking)
|
|
139
|
+
# @param address [String] Event address
|
|
140
|
+
# @param body [Object] Message body
|
|
141
|
+
# @param headers [Hash] Optional headers
|
|
142
|
+
# @return [Future] Future that will contain reply
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# future = EventBus.send_async('cache.query', { key: 'user:123' })
|
|
146
|
+
# # ... do other work ...
|
|
147
|
+
# reply = future.value(timeout: 1.0)
|
|
148
|
+
def self.send_async: (untyped address, ?untyped? body, ?headers: ::Hash[untyped, untyped]) -> untyped
|
|
149
|
+
|
|
150
|
+
# Register consumer for address (point-to-point or pub/sub)
|
|
151
|
+
# @param address [String] Event address or pattern
|
|
152
|
+
# @param options [Hash] Handler options
|
|
153
|
+
# @option options [Boolean] :local_only Only receive local messages
|
|
154
|
+
# @yield [message] Block called when message received
|
|
155
|
+
# @return [String] Consumer ID (for unregistering)
|
|
156
|
+
#
|
|
157
|
+
# @example
|
|
158
|
+
# id = EventBus.consumer('sensor.temperature.room1') do |message|
|
|
159
|
+
# puts "Temp: #{message.body[:value]}"
|
|
160
|
+
# end
|
|
161
|
+
# EventBus.unregister(id)
|
|
162
|
+
def self.consumer: (untyped address, ?::Hash[untyped, untyped] options) ?{ (?) -> untyped } -> untyped
|
|
163
|
+
|
|
164
|
+
# Unregister a consumer
|
|
165
|
+
# @param consumer_id [String] Consumer ID returned from consumer()
|
|
166
|
+
def self.unregister: (untyped consumer_id) -> (nil | untyped)
|
|
167
|
+
|
|
168
|
+
# Subscribe to remote CoAP observable
|
|
169
|
+
# @param address [String] Event address
|
|
170
|
+
# @param node_url [String] Remote node URL (e.g., 'coap://building-a:5683')
|
|
171
|
+
# @yield [message] Block called when remote notification received
|
|
172
|
+
# @return [String] Subscription ID
|
|
173
|
+
#
|
|
174
|
+
# @example
|
|
175
|
+
# id = EventBus.subscribe_remote('sensor.temp.buildingA', 'coap://building-a:5683') do |msg|
|
|
176
|
+
# puts "Remote temp: #{msg.body[:value]}"
|
|
177
|
+
# end
|
|
178
|
+
def self.subscribe_remote: (untyped address, untyped node_url) { (?) -> untyped } -> untyped
|
|
179
|
+
|
|
180
|
+
alias self.observe self.consumer
|
|
181
|
+
|
|
182
|
+
alias self.on self.consumer
|
|
183
|
+
|
|
184
|
+
alias self.notify self.publish
|
|
185
|
+
|
|
186
|
+
alias self.emit self.publish
|
|
187
|
+
|
|
188
|
+
alias self.cancel self.unregister
|
|
189
|
+
|
|
190
|
+
alias self.subscribe self.subscribe_remote
|
|
191
|
+
|
|
192
|
+
alias self.unsubscribe self.unregister
|
|
193
|
+
|
|
194
|
+
# Configure message store (for buffering/replay)
|
|
195
|
+
# @param store [MessageBuffer, Object] Message store instance (nil to disable)
|
|
196
|
+
# @return [MessageBuffer, Object, nil] Configured store
|
|
197
|
+
#
|
|
198
|
+
# @example Enable default buffering
|
|
199
|
+
# EventBus.enable_message_buffering
|
|
200
|
+
#
|
|
201
|
+
# @example Custom configuration
|
|
202
|
+
# EventBus.configure_message_store(
|
|
203
|
+
# MessageBuffer.new(max_messages: 200, ttl: 600)
|
|
204
|
+
# )
|
|
205
|
+
#
|
|
206
|
+
# @example Custom plugin store
|
|
207
|
+
# EventBus.configure_message_store(RedisMessageStore.new)
|
|
208
|
+
def self.configure_message_store: (untyped store) -> untyped
|
|
209
|
+
|
|
210
|
+
# Enable default message buffering
|
|
211
|
+
# @param max_messages [Integer] Max messages per address
|
|
212
|
+
# @param ttl [Integer] Time-to-live in seconds
|
|
213
|
+
# @return [MessageBuffer] Configured buffer
|
|
214
|
+
def self.enable_message_buffering: (?max_messages: ::Integer, ?ttl: ::Integer) -> untyped
|
|
215
|
+
|
|
216
|
+
# Disable message buffering
|
|
217
|
+
def self.disable_message_buffering: () -> untyped
|
|
218
|
+
|
|
219
|
+
# Get current message store
|
|
220
|
+
# @return [MessageBuffer, Object, nil] Current message store
|
|
221
|
+
attr_reader self.message_store: untyped
|
|
222
|
+
|
|
223
|
+
# Replay buffered messages for an address
|
|
224
|
+
# @param address [String] Event address
|
|
225
|
+
# @param since [Time, nil] Return messages since this time (nil = all)
|
|
226
|
+
# @return [Array<Message>] Buffered messages
|
|
227
|
+
#
|
|
228
|
+
# @example Replay all buffered messages
|
|
229
|
+
# EventBus.replay('sensor.temperature.room1')
|
|
230
|
+
#
|
|
231
|
+
# @example Replay last 60 seconds
|
|
232
|
+
# EventBus.replay('sensor.temperature.room1', since: Time.now - 60)
|
|
233
|
+
def self.replay: (untyped address, ?since: untyped?) -> (::Array[untyped] | untyped)
|
|
234
|
+
|
|
235
|
+
# Replay buffered messages to a consumer
|
|
236
|
+
# Useful for late joiners or reconnecting nodes
|
|
237
|
+
# @param address [String] Event address
|
|
238
|
+
# @param since [Time, nil] Replay messages since this time
|
|
239
|
+
# @yield [message] Block called for each buffered message
|
|
240
|
+
#
|
|
241
|
+
# @example Replay to new subscriber
|
|
242
|
+
# EventBus.consumer('sensor.temperature.room1') do |msg|
|
|
243
|
+
# puts "Temp: #{msg.body[:value]}"
|
|
244
|
+
# end
|
|
245
|
+
# # Catch up on last 5 minutes
|
|
246
|
+
# EventBus.replay_to('sensor.temperature.room1', since: Time.now - 300) do |msg|
|
|
247
|
+
# puts "Missed: #{msg.body[:value]}"
|
|
248
|
+
# end
|
|
249
|
+
def self.replay_to: (untyped address, ?since: untyped?) ?{ (?) -> untyped } -> untyped
|
|
250
|
+
|
|
251
|
+
# Check if address is distributed via CoAP
|
|
252
|
+
# Uses AddressPrefix registry for extensibility
|
|
253
|
+
# @param address [String] Event address
|
|
254
|
+
# @return [Boolean]
|
|
255
|
+
def self.distributed?: (untyped address) -> untyped
|
|
256
|
+
|
|
257
|
+
# Check if address is local-only
|
|
258
|
+
# Uses AddressPrefix registry for extensibility
|
|
259
|
+
# @param address [String] Event address
|
|
260
|
+
# @return [Boolean]
|
|
261
|
+
def self.local_only?: (untyped address) -> untyped
|
|
262
|
+
|
|
263
|
+
# Get current state for address
|
|
264
|
+
# @param address [String] Event address
|
|
265
|
+
# @return [Object, nil] Current state or nil
|
|
266
|
+
def self.current_state: (untyped address) -> untyped
|
|
267
|
+
|
|
268
|
+
# List all registered addresses
|
|
269
|
+
# @return [Array<String>] Event addresses
|
|
270
|
+
def self.addresses: () -> untyped
|
|
271
|
+
|
|
272
|
+
# Get handler count for address
|
|
273
|
+
# @param address [String] Event address
|
|
274
|
+
# @return [Integer] Number of handlers
|
|
275
|
+
def self.handler_count: (untyped address) -> untyped
|
|
276
|
+
|
|
277
|
+
# Get EventBus statistics
|
|
278
|
+
# @return [Hash] Statistics
|
|
279
|
+
def self.stats: () -> untyped
|
|
280
|
+
|
|
281
|
+
# Start background cleanup
|
|
282
|
+
def self.start_cleanup: () -> untyped
|
|
283
|
+
|
|
284
|
+
# Stop background cleanup
|
|
285
|
+
def self.stop_cleanup: () -> untyped
|
|
286
|
+
|
|
287
|
+
# Shutdown EventBus (cleanup resources)
|
|
288
|
+
def self.shutdown: () -> untyped
|
|
289
|
+
|
|
290
|
+
private
|
|
291
|
+
|
|
292
|
+
# Get all handlers for exact address match
|
|
293
|
+
def self.handlers_for: (untyped address) -> untyped
|
|
294
|
+
|
|
295
|
+
# Get handlers matching wildcard patterns
|
|
296
|
+
# Supports: "sensor.*", "sensor.*.room1"
|
|
297
|
+
def self.wildcard_handlers: (untyped address) -> untyped
|
|
298
|
+
|
|
299
|
+
# Match address parts against pattern
|
|
300
|
+
def self.match_pattern?: (untyped parts, untyped pattern_parts) -> (false | untyped)
|
|
301
|
+
|
|
302
|
+
# Round-robin selection of next handler
|
|
303
|
+
def self.next_handler_for: (untyped address) -> (nil | untyped)
|
|
304
|
+
|
|
305
|
+
public
|
|
306
|
+
|
|
307
|
+
def self.handler_for_pool_id: (untyped pool_id) -> untyped
|
|
308
|
+
|
|
309
|
+
private
|
|
310
|
+
|
|
311
|
+
# Deliver message asynchronously via Ractor pool
|
|
312
|
+
def self.deliver_async: (untyped handler, untyped message) -> untyped
|
|
313
|
+
|
|
314
|
+
# Generate unique reply address
|
|
315
|
+
def self.generate_reply_address: () -> ::String
|
|
316
|
+
|
|
317
|
+
# Log debug message
|
|
318
|
+
def self.log_debug: (untyped message) -> (nil | untyped)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
# Helper methods for route handlers to improve DX
|
|
3
|
+
#
|
|
4
|
+
# Dynamically generates response helper methods from CoAP::Registries::Response registry:
|
|
5
|
+
# - Success methods (2.xx): created(data = {}), changed(data = {}), etc.
|
|
6
|
+
# - Error methods (4.xx, 5.xx): bad_request(message = ...), not_found(message = ...), etc.
|
|
7
|
+
#
|
|
8
|
+
# @example Success response
|
|
9
|
+
# created({ id: 123, name: 'Resource' })
|
|
10
|
+
#
|
|
11
|
+
# @example Error response
|
|
12
|
+
# bad_request('Invalid input')
|
|
13
|
+
# unauthorized({ error: 'Token expired' })
|
|
14
|
+
module Helpers
|
|
15
|
+
# Returns a JSON response with 2.05 Content status and sets Content-Format to application/json
|
|
16
|
+
# Similar to Sinatra's json helper, automatically sets the Content-Format option
|
|
17
|
+
# @param data [Hash] The data to return as JSON
|
|
18
|
+
# @return [Takagi::Message::Outbound] The response with JSON content-format
|
|
19
|
+
def json: (?::Hash[untyped, untyped] data) -> Message::Outbound
|
|
20
|
+
|
|
21
|
+
# Validates that required parameters are present
|
|
22
|
+
# @param required_params [Array<Symbol>] List of required parameter names
|
|
23
|
+
# @raise [StandardError] If any required parameter is missing
|
|
24
|
+
def validate_params: (*untyped required_params) -> (nil | untyped)
|
|
25
|
+
|
|
26
|
+
# Halts execution and returns the given response
|
|
27
|
+
# Useful for early returns
|
|
28
|
+
# @param response [Takagi::Message::Outbound, Hash] The response to return
|
|
29
|
+
def halt: (untyped response) -> untyped
|
|
30
|
+
|
|
31
|
+
# Alias for internal_server_error (more concise)
|
|
32
|
+
alias server_error internal_server_error
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class Logger
|
|
3
|
+
@logger: untyped
|
|
4
|
+
|
|
5
|
+
def initialize: (?log_output: untyped, ?level: untyped) -> void
|
|
6
|
+
|
|
7
|
+
def set_level: (untyped level) -> untyped
|
|
8
|
+
|
|
9
|
+
def info: (untyped message) -> untyped
|
|
10
|
+
|
|
11
|
+
def warn: (untyped message) -> untyped
|
|
12
|
+
|
|
13
|
+
def debug: (untyped message) -> untyped
|
|
14
|
+
|
|
15
|
+
def error: (untyped message) -> untyped
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
module Message
|
|
3
|
+
# Base class for message
|
|
4
|
+
class Base
|
|
5
|
+
@data: untyped
|
|
6
|
+
|
|
7
|
+
@logger: untyped
|
|
8
|
+
|
|
9
|
+
@version: untyped
|
|
10
|
+
|
|
11
|
+
@type: untyped
|
|
12
|
+
|
|
13
|
+
@code: untyped
|
|
14
|
+
|
|
15
|
+
@message_id: untyped
|
|
16
|
+
|
|
17
|
+
@token: untyped
|
|
18
|
+
|
|
19
|
+
@options: untyped
|
|
20
|
+
|
|
21
|
+
@payload: untyped
|
|
22
|
+
|
|
23
|
+
attr_reader version: untyped
|
|
24
|
+
|
|
25
|
+
attr_reader type: untyped
|
|
26
|
+
|
|
27
|
+
attr_reader token: untyped
|
|
28
|
+
|
|
29
|
+
attr_reader message_id: untyped
|
|
30
|
+
|
|
31
|
+
attr_reader payload: untyped
|
|
32
|
+
|
|
33
|
+
attr_reader options: untyped
|
|
34
|
+
|
|
35
|
+
attr_reader code: untyped
|
|
36
|
+
|
|
37
|
+
def initialize: (?untyped? data) -> void
|
|
38
|
+
|
|
39
|
+
# Convert CoAP code number to method name using registry
|
|
40
|
+
# @param code [Integer] CoAP code number
|
|
41
|
+
# @return [String] Method name (e.g., 'GET', 'OBSERVE')
|
|
42
|
+
def coap_code_to_method: (untyped code) -> ("OBSERVE" | untyped)
|
|
43
|
+
|
|
44
|
+
# Convert method name to CoAP code number using registry
|
|
45
|
+
# @param method [String, Symbol] Method name (e.g., 'GET', :post)
|
|
46
|
+
# @return [Integer] CoAP code number
|
|
47
|
+
def coap_method_to_code: (untyped method) -> untyped
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def parse: (untyped data) -> untyped
|
|
52
|
+
|
|
53
|
+
def parse_options: (untyped bytes) -> untyped
|
|
54
|
+
|
|
55
|
+
def extract_payload: (untyped data) -> (nil | untyped)
|
|
56
|
+
|
|
57
|
+
def decode_extended_value: (untyped bytes, untyped position, untyped raw_value) -> untyped
|
|
58
|
+
|
|
59
|
+
def store_option: (untyped options, untyped option_number, untyped value) -> untyped
|
|
60
|
+
|
|
61
|
+
def coerce_option_value: (untyped value) -> untyped
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
module Message
|
|
3
|
+
# Implements message deduplication as per RFC 7252 Section 4.4
|
|
4
|
+
#
|
|
5
|
+
# The server MUST detect duplicates by matching both Message ID and source endpoint.
|
|
6
|
+
# When a duplicate CON message is received, the server MUST resend the cached response.
|
|
7
|
+
#
|
|
8
|
+
# Cache entries expire after EXCHANGE_LIFETIME (247 seconds per RFC 7252 §4.8.2)
|
|
9
|
+
class DeduplicationCache
|
|
10
|
+
@cache: untyped
|
|
11
|
+
|
|
12
|
+
@mutex: untyped
|
|
13
|
+
|
|
14
|
+
# RFC 7252 §4.8.2: EXCHANGE_LIFETIME = 247 seconds
|
|
15
|
+
EXCHANGE_LIFETIME: 247
|
|
16
|
+
|
|
17
|
+
# Entry contains the cached response and metadata
|
|
18
|
+
CacheEntry: untyped
|
|
19
|
+
|
|
20
|
+
def initialize: () -> void
|
|
21
|
+
|
|
22
|
+
# Check if message is a duplicate and return cached response if available
|
|
23
|
+
# @param message_id [Integer] The CoAP Message ID
|
|
24
|
+
# @param source_endpoint [String] Source IP:Port identifier
|
|
25
|
+
# @return [String, nil] Cached response data or nil if not a duplicate
|
|
26
|
+
def check_duplicate: (untyped message_id, untyped source_endpoint) -> untyped
|
|
27
|
+
|
|
28
|
+
# Store a response for future duplicate detection
|
|
29
|
+
# @param message_id [Integer] The CoAP Message ID
|
|
30
|
+
# @param source_endpoint [String] Source IP:Port identifier
|
|
31
|
+
# @param response_data [String] The serialized response to cache
|
|
32
|
+
def store_response: (untyped message_id, untyped source_endpoint, untyped response_data) -> untyped
|
|
33
|
+
|
|
34
|
+
# Clear all cache entries (useful for testing)
|
|
35
|
+
def clear: () -> untyped
|
|
36
|
+
|
|
37
|
+
# Get cache statistics
|
|
38
|
+
def stats: () -> untyped
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def cache_key: (untyped message_id, untyped source_endpoint) -> ::String
|
|
43
|
+
|
|
44
|
+
# Remove expired entries to prevent unbounded memory growth
|
|
45
|
+
# RFC 7252 §4.8.2: entries older than EXCHANGE_LIFETIME should be discarded
|
|
46
|
+
def cleanup_expired_entries: () -> untyped
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
module Message
|
|
3
|
+
# Class for inbound message that is coming to server
|
|
4
|
+
class Inbound < Base
|
|
5
|
+
@method: untyped
|
|
6
|
+
|
|
7
|
+
@response_code: untyped
|
|
8
|
+
|
|
9
|
+
@uri: untyped
|
|
10
|
+
|
|
11
|
+
attr_reader method: untyped
|
|
12
|
+
|
|
13
|
+
attr_reader uri: untyped
|
|
14
|
+
|
|
15
|
+
attr_reader response_code: untyped
|
|
16
|
+
|
|
17
|
+
def initialize: (untyped data) -> void
|
|
18
|
+
|
|
19
|
+
def to_response: (untyped code, untyped payload, ?options: ::Hash[untyped, untyped]) -> untyped
|
|
20
|
+
|
|
21
|
+
def parse_coap_uri: () -> untyped
|
|
22
|
+
|
|
23
|
+
# Get CoAP option by number
|
|
24
|
+
# @param option_number [Integer] The CoAP option number
|
|
25
|
+
# @return [Object, nil] The option value
|
|
26
|
+
def option: (untyped option_number) -> untyped
|
|
27
|
+
|
|
28
|
+
# Check if request has a specific CoAP option
|
|
29
|
+
# @param option_number [Integer] The CoAP option number
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def option?: (untyped option_number) -> untyped
|
|
32
|
+
|
|
33
|
+
# Get Accept option
|
|
34
|
+
# @return [Integer, nil] The Accept content format
|
|
35
|
+
def accept: () -> untyped
|
|
36
|
+
|
|
37
|
+
# Check if request accepts a specific content format
|
|
38
|
+
# @param format [String, Integer] Format name or number
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def accept?: (untyped format) -> (false | untyped)
|
|
41
|
+
|
|
42
|
+
# Get Content-Format option
|
|
43
|
+
# @return [Integer, nil] The Content-Format
|
|
44
|
+
def content_format: () -> untyped
|
|
45
|
+
|
|
46
|
+
# Get query parameters as a hash
|
|
47
|
+
# @return [Hash<String, String>]
|
|
48
|
+
def query_params: () -> (::Hash[untyped, untyped] | untyped)
|
|
49
|
+
|
|
50
|
+
# Check if request is a GET
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def get?: () -> untyped
|
|
53
|
+
|
|
54
|
+
# Check if request is a POST
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def post?: () -> untyped
|
|
57
|
+
|
|
58
|
+
# Check if request is a PUT
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def put?: () -> untyped
|
|
61
|
+
|
|
62
|
+
# Check if request is a DELETE
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def delete?: () -> untyped
|
|
65
|
+
|
|
66
|
+
# Check if request is an OBSERVE
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def observe?: () -> untyped
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Convert content format name to number using CoAP registry
|
|
73
|
+
def content_format_to_number: (untyped format) -> untyped
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
module Message
|
|
3
|
+
# Class for outbound message that is coming from server
|
|
4
|
+
class Outbound < Base
|
|
5
|
+
@code: untyped
|
|
6
|
+
|
|
7
|
+
@token: untyped
|
|
8
|
+
|
|
9
|
+
@message_id: untyped
|
|
10
|
+
|
|
11
|
+
@type: untyped
|
|
12
|
+
|
|
13
|
+
@options: untyped
|
|
14
|
+
|
|
15
|
+
@payload: untyped
|
|
16
|
+
|
|
17
|
+
def initialize: (code: untyped, payload: untyped, ?token: untyped?, ?message_id: untyped?, ?type: untyped, ?options: ::Hash[untyped, untyped]) -> void
|
|
18
|
+
|
|
19
|
+
def to_bytes: () -> untyped
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def with_error_handling: () { () -> untyped } -> untyped
|
|
24
|
+
|
|
25
|
+
def log_generation: () -> untyped
|
|
26
|
+
|
|
27
|
+
def build_header: () -> untyped
|
|
28
|
+
|
|
29
|
+
def token_bytes: () -> untyped
|
|
30
|
+
|
|
31
|
+
def build_options_section: () -> untyped
|
|
32
|
+
|
|
33
|
+
def build_payload_section: () -> untyped
|
|
34
|
+
|
|
35
|
+
def log_final_packet: (untyped packet) -> untyped
|
|
36
|
+
|
|
37
|
+
def normalize_options: (untyped options) -> untyped
|
|
38
|
+
|
|
39
|
+
def flattened_options: () -> untyped
|
|
40
|
+
|
|
41
|
+
def encode_option_value: (untyped value) -> untyped
|
|
42
|
+
|
|
43
|
+
def encode_integer_option_value: (untyped value) -> untyped
|
|
44
|
+
|
|
45
|
+
def encode_option_header_value: (untyped value) -> untyped
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
module Message
|
|
3
|
+
# Encodes outbound CoAP request envelopes used when observing remote peers.
|
|
4
|
+
class Request < Base
|
|
5
|
+
@method: untyped
|
|
6
|
+
|
|
7
|
+
@uri: untyped
|
|
8
|
+
|
|
9
|
+
@token: untyped
|
|
10
|
+
|
|
11
|
+
@observe: untyped
|
|
12
|
+
|
|
13
|
+
@message_id: untyped
|
|
14
|
+
|
|
15
|
+
@payload: untyped
|
|
16
|
+
|
|
17
|
+
attr_reader message_id: untyped
|
|
18
|
+
|
|
19
|
+
def initialize: (method: untyped, uri: untyped, ?payload: untyped?, ?token: untyped?, **untyped options) -> void
|
|
20
|
+
|
|
21
|
+
def to_bytes: () -> untyped
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def encode_options: () -> untyped
|
|
26
|
+
|
|
27
|
+
def encode_option: (untyped option_number, untyped value, untyped last_option_number) -> untyped
|
|
28
|
+
|
|
29
|
+
def encode_extended: (untyped val) -> untyped
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|