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,50 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
module Discovery
|
|
3
|
+
# Builds CoRE Link Format payloads as defined in RFC 6690.
|
|
4
|
+
class CoreLinkFormat
|
|
5
|
+
@router: untyped
|
|
6
|
+
|
|
7
|
+
@query: untyped
|
|
8
|
+
|
|
9
|
+
CONTENT_FORMAT: 40
|
|
10
|
+
|
|
11
|
+
def self.generate: (router: untyped, ?request: untyped?) -> untyped
|
|
12
|
+
|
|
13
|
+
def initialize: (untyped router, untyped raw_query) -> void
|
|
14
|
+
|
|
15
|
+
def generate: () -> untyped
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader router: untyped
|
|
20
|
+
|
|
21
|
+
attr_reader query: untyped
|
|
22
|
+
|
|
23
|
+
def parse_query: (untyped raw_query) -> (::Hash[untyped, untyped] | untyped)
|
|
24
|
+
|
|
25
|
+
def filter_resources: (untyped resources) -> untyped
|
|
26
|
+
|
|
27
|
+
def matches_filter?: (untyped entry, untyped key, untyped values) -> untyped
|
|
28
|
+
|
|
29
|
+
def matches_list_attribute?: (untyped entry, untyped attribute, untyped values) -> untyped
|
|
30
|
+
|
|
31
|
+
def matches_numeric_attribute?: (untyped entry, untyped attribute, untyped values) -> untyped
|
|
32
|
+
|
|
33
|
+
def matches_string_attribute?: (untyped entry, untyped attribute, untyped values) -> untyped
|
|
34
|
+
|
|
35
|
+
def format_entry: (untyped entry) -> ::String
|
|
36
|
+
|
|
37
|
+
def normalize_path: (untyped path) -> untyped
|
|
38
|
+
|
|
39
|
+
def link_attributes_for: (untyped entry) -> untyped
|
|
40
|
+
|
|
41
|
+
def append_attribute: (untyped attrs, untyped name, untyped value) -> (nil | untyped)
|
|
42
|
+
|
|
43
|
+
def append_numeric_attribute: (untyped attrs, untyped name, untyped value) -> (nil | untyped)
|
|
44
|
+
|
|
45
|
+
def join_attributes: (untyped attrs) -> untyped
|
|
46
|
+
|
|
47
|
+
def server_link_entry: () -> (nil | untyped)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class EventBus
|
|
3
|
+
# EventBus Address Prefix Registry
|
|
4
|
+
#
|
|
5
|
+
# Defines which event address prefixes are distributed via CoAP
|
|
6
|
+
# and which remain local-only.
|
|
7
|
+
#
|
|
8
|
+
# Extensible registry allows plugins to register custom prefixes
|
|
9
|
+
# without modifying core code.
|
|
10
|
+
#
|
|
11
|
+
# @example Using predefined prefixes
|
|
12
|
+
# AddressPrefix.distributed?('sensor.temperature.room1') # => true
|
|
13
|
+
# AddressPrefix.distributed?('system.startup') # => false
|
|
14
|
+
#
|
|
15
|
+
# @example Registering a custom distributed prefix
|
|
16
|
+
# AddressPrefix.register_distributed('custom.', 'Custom Events')
|
|
17
|
+
#
|
|
18
|
+
# @example Registering a custom local prefix
|
|
19
|
+
# AddressPrefix.register_local('internal.', 'Internal Events')
|
|
20
|
+
class AddressPrefix
|
|
21
|
+
self.@distributed: untyped
|
|
22
|
+
|
|
23
|
+
self.@local: untyped
|
|
24
|
+
|
|
25
|
+
self.@mutex: untyped
|
|
26
|
+
|
|
27
|
+
# Register a distributed prefix (events are published via CoAP)
|
|
28
|
+
# @param prefix [String] Address prefix (e.g., 'sensor.')
|
|
29
|
+
# @param description [String] Human-readable description
|
|
30
|
+
# @param rfc [String, nil] Optional RFC reference
|
|
31
|
+
def self.register_distributed: (untyped prefix, untyped description, ?rfc: untyped?) -> untyped
|
|
32
|
+
|
|
33
|
+
# Register a local-only prefix (events stay in-process)
|
|
34
|
+
# @param prefix [String] Address prefix (e.g., 'system.')
|
|
35
|
+
# @param description [String] Human-readable description
|
|
36
|
+
# @param rfc [String, nil] Optional RFC reference
|
|
37
|
+
def self.register_local: (untyped prefix, untyped description, ?rfc: untyped?) -> untyped
|
|
38
|
+
|
|
39
|
+
# Check if address matches a distributed prefix
|
|
40
|
+
# @param address [String] Event address
|
|
41
|
+
# @return [Boolean] true if distributed
|
|
42
|
+
def self.distributed?: (untyped address) -> (false | untyped)
|
|
43
|
+
|
|
44
|
+
# Check if address matches a local-only prefix
|
|
45
|
+
# @param address [String] Event address
|
|
46
|
+
# @return [Boolean] true if local-only
|
|
47
|
+
def self.local?: (untyped address) -> untyped
|
|
48
|
+
|
|
49
|
+
# Get all distributed prefixes
|
|
50
|
+
# @return [Hash] Map of prefix => metadata
|
|
51
|
+
def self.distributed_prefixes: () -> untyped
|
|
52
|
+
|
|
53
|
+
# Get all local prefixes
|
|
54
|
+
# @return [Hash] Map of prefix => metadata
|
|
55
|
+
def self.local_prefixes: () -> untyped
|
|
56
|
+
|
|
57
|
+
# Get all registered prefixes
|
|
58
|
+
# @return [Hash] Combined map of all prefixes
|
|
59
|
+
def self.all: () -> untyped
|
|
60
|
+
|
|
61
|
+
# Get metadata for a specific prefix
|
|
62
|
+
# @param prefix [String] The prefix to look up
|
|
63
|
+
# @return [Hash, nil] Prefix metadata
|
|
64
|
+
def self.metadata_for: (untyped prefix) -> untyped
|
|
65
|
+
|
|
66
|
+
# Unregister a prefix (useful for testing/plugins)
|
|
67
|
+
# @param prefix [String] The prefix to remove
|
|
68
|
+
# @return [Boolean] true if was registered
|
|
69
|
+
def self.unregister: (untyped prefix) -> untyped
|
|
70
|
+
|
|
71
|
+
# Clear all registrations (useful for testing)
|
|
72
|
+
def self.clear!: () -> untyped
|
|
73
|
+
|
|
74
|
+
# Initialize default prefixes
|
|
75
|
+
def self.initialize_defaults!: () -> untyped
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class EventBus
|
|
3
|
+
module AsyncExecutor
|
|
4
|
+
# Thread-based executor (default fallback)
|
|
5
|
+
class ThreadExecutor
|
|
6
|
+
@size: untyped
|
|
7
|
+
|
|
8
|
+
@queue: untyped
|
|
9
|
+
|
|
10
|
+
@threads: untyped
|
|
11
|
+
|
|
12
|
+
@shutdown: untyped
|
|
13
|
+
|
|
14
|
+
attr_reader size: untyped
|
|
15
|
+
|
|
16
|
+
def initialize: (size: untyped) -> void
|
|
17
|
+
|
|
18
|
+
def post: (untyped handler, untyped message) -> untyped
|
|
19
|
+
|
|
20
|
+
def register_handler: (untyped _handler) -> nil
|
|
21
|
+
|
|
22
|
+
def unregister_handler: (untyped _handler) -> nil
|
|
23
|
+
|
|
24
|
+
def shutdown: () -> (nil | untyped)
|
|
25
|
+
|
|
26
|
+
def running?: () -> untyped
|
|
27
|
+
|
|
28
|
+
def stats: () -> { mode: :threads, size: untyped }
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def start_workers: () -> untyped
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Process-based executor for multi-reactor workloads
|
|
36
|
+
class ProcessExecutor
|
|
37
|
+
@processes: untyped
|
|
38
|
+
|
|
39
|
+
@threads: untyped
|
|
40
|
+
|
|
41
|
+
@mutex: untyped
|
|
42
|
+
|
|
43
|
+
@jobs: untyped
|
|
44
|
+
|
|
45
|
+
@next_index: untyped
|
|
46
|
+
|
|
47
|
+
@needs_restart: untyped
|
|
48
|
+
|
|
49
|
+
Job: untyped
|
|
50
|
+
|
|
51
|
+
def initialize: (processes: untyped, threads: untyped) -> void
|
|
52
|
+
|
|
53
|
+
def post: (untyped handler, untyped message) -> untyped
|
|
54
|
+
|
|
55
|
+
# Mark for restart so new handlers are visible in workers
|
|
56
|
+
def register_handler: (untyped _handler) -> untyped
|
|
57
|
+
|
|
58
|
+
def unregister_handler: (untyped _handler) -> untyped
|
|
59
|
+
|
|
60
|
+
def shutdown: () -> untyped
|
|
61
|
+
|
|
62
|
+
def stats: () -> { mode: :processes, size: untyped }
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def ensure_running: () -> (nil | untyped)
|
|
67
|
+
|
|
68
|
+
def mark_restart_needed: () -> untyped
|
|
69
|
+
|
|
70
|
+
def restart_workers_locked: () -> untyped
|
|
71
|
+
|
|
72
|
+
def shutdown_workers: () -> untyped
|
|
73
|
+
|
|
74
|
+
def spawn_workers_locked: () -> (nil | untyped)
|
|
75
|
+
|
|
76
|
+
def fork_worker: (untyped index) -> untyped
|
|
77
|
+
|
|
78
|
+
def run_worker: (untyped reader, untyped index) -> untyped
|
|
79
|
+
|
|
80
|
+
def dispatch: (untyped handler, untyped message) -> (nil | untyped)
|
|
81
|
+
|
|
82
|
+
def select_job: () -> untyped
|
|
83
|
+
|
|
84
|
+
def reopen_worker: (untyped index) -> untyped
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class EventBus
|
|
3
|
+
# CoAP Observe integration - maps events to CoAP resources
|
|
4
|
+
# Thread-safe with Mutex for resource registration
|
|
5
|
+
#
|
|
6
|
+
# Maps EventBus addresses to CoAP observable resources:
|
|
7
|
+
# - "sensor.temperature.room1" -> "/events/sensor/temperature/room1"
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# CoAPBridge.register_observable_resource('sensor.temp.room1', app)
|
|
11
|
+
# CoAPBridge.publish_to_observers('sensor.temp.room1', message)
|
|
12
|
+
class CoAPBridge
|
|
13
|
+
self.@registered_resources: untyped
|
|
14
|
+
|
|
15
|
+
self.@mutex: untyped
|
|
16
|
+
|
|
17
|
+
# Convert event address to CoAP path
|
|
18
|
+
# @param address [String] Event address (e.g., "sensor.temperature.room1")
|
|
19
|
+
# @return [String] CoAP path (e.g., "/events/sensor/temperature/room1")
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# CoAPBridge.address_to_path('sensor.temperature.room1')
|
|
23
|
+
# # => "/events/sensor/temperature/room1"
|
|
24
|
+
def self.address_to_path: (untyped address) -> ::String
|
|
25
|
+
|
|
26
|
+
# Convert CoAP path to event address
|
|
27
|
+
# @param path [String] CoAP path (e.g., "/events/sensor/temperature/room1")
|
|
28
|
+
# @return [String] Event address (e.g., "sensor.temperature.room1")
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# CoAPBridge.path_to_address('/events/sensor/temperature/room1')
|
|
32
|
+
# # => "sensor.temperature.room1"
|
|
33
|
+
def self.path_to_address: (untyped path) -> untyped
|
|
34
|
+
|
|
35
|
+
# Auto-register observable CoAP resource (thread-safe)
|
|
36
|
+
# Creates a CoAP observable endpoint that returns current state
|
|
37
|
+
#
|
|
38
|
+
# Uses AddressPrefix registry to determine if address should be distributed
|
|
39
|
+
#
|
|
40
|
+
# @param address [String] Event address
|
|
41
|
+
# @param app [Class] Application class (must respond to #observable)
|
|
42
|
+
# @return [Boolean] True if registered, false if already exists
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# CoAPBridge.register_observable_resource('sensor.temp.room1', MyApp)
|
|
46
|
+
def self.register_observable_resource: (untyped address, untyped app) -> (false | untyped)
|
|
47
|
+
|
|
48
|
+
# Publish event to all CoAP observers
|
|
49
|
+
# Notifies all observers subscribed via CoAP Observe
|
|
50
|
+
#
|
|
51
|
+
# @param address [String] Event address
|
|
52
|
+
# @param message [EventBus::Message] Event message
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# message = EventBus::Message.new('sensor.temp.room1', { value: 25.5 })
|
|
56
|
+
# CoAPBridge.publish_to_observers('sensor.temp.room1', message)
|
|
57
|
+
def self.publish_to_observers: (untyped address, untyped message) -> untyped
|
|
58
|
+
|
|
59
|
+
# Subscribe to remote event via CoAP Observe
|
|
60
|
+
# @param address [String] Event address
|
|
61
|
+
# @param node_url [String] Remote node URL (e.g., 'coap://building-a:5683')
|
|
62
|
+
# @yield [message] Block called when remote notification received
|
|
63
|
+
# @return [String] Subscription ID
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# id = CoAPBridge.subscribe_remote('sensor.temp.buildingA', 'coap://building-a:5683') do |msg|
|
|
67
|
+
# puts "Remote temp: #{msg.body[:value]}"
|
|
68
|
+
# end
|
|
69
|
+
def self.subscribe_remote: (untyped address, untyped node_url) -> untyped
|
|
70
|
+
|
|
71
|
+
# Check if resource is registered
|
|
72
|
+
# @param address [String] Event address
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def self.registered?: (untyped address) -> untyped
|
|
75
|
+
|
|
76
|
+
# Get all registered resource addresses
|
|
77
|
+
# @return [Array<String>]
|
|
78
|
+
def self.registered_addresses: () -> untyped
|
|
79
|
+
|
|
80
|
+
# Get count of registered resources
|
|
81
|
+
# @return [Integer]
|
|
82
|
+
def self.registered_count: () -> untyped
|
|
83
|
+
|
|
84
|
+
# Unregister a resource (for testing)
|
|
85
|
+
# @param address [String] Event address
|
|
86
|
+
# @return [Boolean] True if was registered, false otherwise
|
|
87
|
+
def self.unregister: (untyped address) -> untyped
|
|
88
|
+
|
|
89
|
+
# Clear all registrations (for testing)
|
|
90
|
+
def self.clear: () -> untyped
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class EventBus
|
|
3
|
+
# Future for sync/async request-reply pattern (pure Ruby)
|
|
4
|
+
# Uses ConditionVariable for efficient waiting
|
|
5
|
+
#
|
|
6
|
+
# @example Blocking wait
|
|
7
|
+
# future = Future.new
|
|
8
|
+
# Thread.new { future.set_value(42) }
|
|
9
|
+
# value = future.value(timeout: 1.0) # => 42
|
|
10
|
+
#
|
|
11
|
+
# @example Non-blocking check
|
|
12
|
+
# future = Future.new
|
|
13
|
+
# future.completed? # => false
|
|
14
|
+
# future.set_value(42)
|
|
15
|
+
# future.completed? # => true
|
|
16
|
+
#
|
|
17
|
+
# @example Error propagation
|
|
18
|
+
# future = Future.new
|
|
19
|
+
# future.set_error(StandardError.new("Failed"))
|
|
20
|
+
# future.value # raises StandardError
|
|
21
|
+
class Future
|
|
22
|
+
@mutex: untyped
|
|
23
|
+
|
|
24
|
+
@condition: untyped
|
|
25
|
+
|
|
26
|
+
@value: untyped
|
|
27
|
+
|
|
28
|
+
@completed: untyped
|
|
29
|
+
|
|
30
|
+
@error: untyped
|
|
31
|
+
|
|
32
|
+
# Initialize a new Future
|
|
33
|
+
def initialize: () -> void
|
|
34
|
+
|
|
35
|
+
# Set the successful value of the Future
|
|
36
|
+
# @param value [Object] The value to set
|
|
37
|
+
# @raise [RuntimeError] If already completed
|
|
38
|
+
def set_value: (untyped value) -> untyped
|
|
39
|
+
|
|
40
|
+
# Set the error state of the Future
|
|
41
|
+
# @param error [Exception] The error to set
|
|
42
|
+
# @raise [RuntimeError] If already completed
|
|
43
|
+
def set_error: (untyped error) -> untyped
|
|
44
|
+
|
|
45
|
+
# Get the value of the Future (blocking)
|
|
46
|
+
# @param timeout [Float, nil] Timeout in seconds (nil = wait forever)
|
|
47
|
+
# @return [Object] The value
|
|
48
|
+
# @raise [Timeout::Error] If timeout expires before completion
|
|
49
|
+
# @raise [Exception] The error if Future completed with error
|
|
50
|
+
def value: (?timeout: untyped?) -> untyped
|
|
51
|
+
|
|
52
|
+
# Check if Future is completed
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def completed?: () -> untyped
|
|
55
|
+
|
|
56
|
+
# Check if Future completed with error
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def error?: () -> untyped
|
|
59
|
+
|
|
60
|
+
# Check if Future completed successfully
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def success?: () -> untyped
|
|
63
|
+
|
|
64
|
+
# Get the error if present
|
|
65
|
+
# @return [Exception, nil]
|
|
66
|
+
def error: () -> untyped
|
|
67
|
+
|
|
68
|
+
# Try to get value without blocking
|
|
69
|
+
# @return [Object, nil] Value if completed, nil otherwise
|
|
70
|
+
def try_value: () -> untyped
|
|
71
|
+
|
|
72
|
+
# Wait for completion without returning value
|
|
73
|
+
# @param timeout [Float, nil] Timeout in seconds
|
|
74
|
+
# @return [Boolean] True if completed, false if timeout
|
|
75
|
+
def wait: (?timeout: untyped?) -> untyped
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class EventBus
|
|
3
|
+
# Thread-safe LRU cache with TTL (pure Ruby)
|
|
4
|
+
# Zero runtime dependencies
|
|
5
|
+
#
|
|
6
|
+
# Features:
|
|
7
|
+
# - Least Recently Used eviction when at capacity
|
|
8
|
+
# - Time-To-Live (TTL) based expiration
|
|
9
|
+
# - Thread-safe with Mutex
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# cache = LRUCache.new(max_size: 1000, ttl: 3600)
|
|
13
|
+
# cache.set('key', 'value')
|
|
14
|
+
# cache.get('key') # => 'value'
|
|
15
|
+
# cache.size # => 1
|
|
16
|
+
class LRUCache
|
|
17
|
+
@max_size: untyped
|
|
18
|
+
|
|
19
|
+
@ttl: untyped
|
|
20
|
+
|
|
21
|
+
@cache: untyped
|
|
22
|
+
|
|
23
|
+
@access_order: untyped
|
|
24
|
+
|
|
25
|
+
@timestamps: untyped
|
|
26
|
+
|
|
27
|
+
@mutex: untyped
|
|
28
|
+
|
|
29
|
+
attr_reader max_size: untyped
|
|
30
|
+
|
|
31
|
+
attr_reader ttl: untyped
|
|
32
|
+
|
|
33
|
+
# Initialize LRU cache
|
|
34
|
+
# @param max_size [Integer] Maximum number of entries (default: 1000)
|
|
35
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 3600)
|
|
36
|
+
def initialize: (?::Integer max_size, ?::Integer ttl) -> void
|
|
37
|
+
|
|
38
|
+
# Get value from cache
|
|
39
|
+
# @param key [Object] Cache key
|
|
40
|
+
# @return [Object, nil] Cached value or nil if not found/expired
|
|
41
|
+
def get: (untyped key) -> untyped
|
|
42
|
+
|
|
43
|
+
# Set value in cache
|
|
44
|
+
# @param key [Object] Cache key
|
|
45
|
+
# @param value [Object] Value to cache
|
|
46
|
+
def set: (untyped key, untyped value) -> untyped
|
|
47
|
+
|
|
48
|
+
# Delete entry from cache
|
|
49
|
+
# @param key [Object] Cache key
|
|
50
|
+
# @return [Object, nil] Deleted value or nil
|
|
51
|
+
def delete: (untyped key) -> untyped
|
|
52
|
+
|
|
53
|
+
# Clear all entries
|
|
54
|
+
def clear: () -> untyped
|
|
55
|
+
|
|
56
|
+
# Get number of entries in cache
|
|
57
|
+
# @return [Integer] Cache size
|
|
58
|
+
def size: () -> untyped
|
|
59
|
+
|
|
60
|
+
# Check if cache is empty
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def empty?: () -> untyped
|
|
63
|
+
|
|
64
|
+
# Check if key exists in cache
|
|
65
|
+
# @param key [Object] Cache key
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def key?: (untyped key) -> untyped
|
|
68
|
+
|
|
69
|
+
# Get all keys in cache
|
|
70
|
+
# @return [Array] Cache keys
|
|
71
|
+
def keys: () -> untyped
|
|
72
|
+
|
|
73
|
+
# Get cache statistics
|
|
74
|
+
# @return [Hash] Statistics
|
|
75
|
+
def stats: () -> untyped
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Remove expired entries based on TTL
|
|
80
|
+
def cleanup_expired: () -> untyped
|
|
81
|
+
|
|
82
|
+
# Evict least recently used entry
|
|
83
|
+
def evict_oldest: () -> (nil | untyped)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class EventBus
|
|
3
|
+
# Bounded message buffer for distributed addresses
|
|
4
|
+
# Stores recent messages in a ring buffer per address for replay on reconnection
|
|
5
|
+
#
|
|
6
|
+
# Features:
|
|
7
|
+
# - Bounded memory (max messages per address)
|
|
8
|
+
# - TTL-based expiration
|
|
9
|
+
# - Thread-safe
|
|
10
|
+
# - Zero external dependencies
|
|
11
|
+
# - Only buffers distributed addresses
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# buffer = MessageBuffer.new(max_messages: 100, ttl: 300)
|
|
15
|
+
# buffer.store(address, message)
|
|
16
|
+
# messages = buffer.replay(address, since: Time.now - 60)
|
|
17
|
+
class MessageBuffer
|
|
18
|
+
@max_messages: untyped
|
|
19
|
+
|
|
20
|
+
@ttl: untyped
|
|
21
|
+
|
|
22
|
+
@buffers: untyped
|
|
23
|
+
|
|
24
|
+
@mutex: untyped
|
|
25
|
+
|
|
26
|
+
@enabled: untyped
|
|
27
|
+
|
|
28
|
+
@cleanup_thread: untyped
|
|
29
|
+
|
|
30
|
+
# Ring buffer for a single address
|
|
31
|
+
class RingBuffer
|
|
32
|
+
@max_size: untyped
|
|
33
|
+
|
|
34
|
+
@messages: untyped
|
|
35
|
+
|
|
36
|
+
@mutex: untyped
|
|
37
|
+
|
|
38
|
+
def initialize: (untyped max_size) -> void
|
|
39
|
+
|
|
40
|
+
def push: (untyped message) -> untyped
|
|
41
|
+
|
|
42
|
+
def messages_since: (?untyped? timestamp) -> untyped
|
|
43
|
+
|
|
44
|
+
def all_messages: () -> untyped
|
|
45
|
+
|
|
46
|
+
def size: () -> untyped
|
|
47
|
+
|
|
48
|
+
def clear: () -> untyped
|
|
49
|
+
|
|
50
|
+
def empty?: () -> untyped
|
|
51
|
+
|
|
52
|
+
# Clean expired messages based on TTL
|
|
53
|
+
def clean_expired: (untyped ttl) -> untyped
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param max_messages [Integer] Maximum messages per address (default: 100)
|
|
57
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 300 = 5 minutes)
|
|
58
|
+
def initialize: (?max_messages: ::Integer, ?ttl: ::Integer) -> void
|
|
59
|
+
|
|
60
|
+
# Store a message in the buffer
|
|
61
|
+
# Only stores distributed addresses to conserve memory
|
|
62
|
+
#
|
|
63
|
+
# @param address [String] Event address
|
|
64
|
+
# @param message [Message] Event message
|
|
65
|
+
def store: (untyped address, untyped message) -> (nil | untyped)
|
|
66
|
+
|
|
67
|
+
# Store a failed delivery for retry
|
|
68
|
+
# Used by CoAPBridge when network delivery fails
|
|
69
|
+
#
|
|
70
|
+
# @param address [String] Event address
|
|
71
|
+
# @param message [Message] Event message
|
|
72
|
+
# @param destination [String] Failed destination (for logging)
|
|
73
|
+
def store_failed: (untyped address, untyped message, ?untyped? destination) -> (nil | untyped)
|
|
74
|
+
|
|
75
|
+
# Replay messages for an address since a given timestamp
|
|
76
|
+
#
|
|
77
|
+
# @param address [String] Event address
|
|
78
|
+
# @param since [Time, nil] Return messages since this time (nil = all messages)
|
|
79
|
+
# @return [Array<Message>] Buffered messages
|
|
80
|
+
#
|
|
81
|
+
# @example Replay last 60 seconds
|
|
82
|
+
# messages = buffer.replay('sensor.temperature.room1', since: Time.now - 60)
|
|
83
|
+
def replay: (untyped address, ?since: untyped?) -> untyped
|
|
84
|
+
|
|
85
|
+
# Get all buffered messages for an address
|
|
86
|
+
#
|
|
87
|
+
# @param address [String] Event address
|
|
88
|
+
# @return [Array<Message>] All buffered messages
|
|
89
|
+
def all: (untyped address) -> untyped
|
|
90
|
+
|
|
91
|
+
# Get buffer size for an address
|
|
92
|
+
#
|
|
93
|
+
# @param address [String] Event address
|
|
94
|
+
# @return [Integer] Number of buffered messages
|
|
95
|
+
def size: (untyped address) -> untyped
|
|
96
|
+
|
|
97
|
+
# Get total number of buffered messages across all addresses
|
|
98
|
+
#
|
|
99
|
+
# @return [Integer] Total buffered messages
|
|
100
|
+
def total_size: () -> untyped
|
|
101
|
+
|
|
102
|
+
# Clear buffer for an address
|
|
103
|
+
#
|
|
104
|
+
# @param address [String] Event address
|
|
105
|
+
def clear: (untyped address) -> untyped
|
|
106
|
+
|
|
107
|
+
# Clear all buffers
|
|
108
|
+
def clear_all: () -> untyped
|
|
109
|
+
|
|
110
|
+
# Enable message buffering
|
|
111
|
+
def enable: () -> untyped
|
|
112
|
+
|
|
113
|
+
# Disable message buffering
|
|
114
|
+
def disable: () -> untyped
|
|
115
|
+
|
|
116
|
+
# Check if buffering is enabled
|
|
117
|
+
def enabled?: () -> untyped
|
|
118
|
+
|
|
119
|
+
# Get statistics about buffering
|
|
120
|
+
#
|
|
121
|
+
# @return [Hash] Buffer statistics
|
|
122
|
+
def stats: () -> untyped
|
|
123
|
+
|
|
124
|
+
# Shutdown cleanup thread
|
|
125
|
+
def shutdown: () -> untyped
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# Start background thread to clean expired messages
|
|
130
|
+
def start_cleanup_thread: () -> untyped
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Takagi
|
|
2
|
+
class EventBus
|
|
3
|
+
# Background thread for stale observer cleanup
|
|
4
|
+
#
|
|
5
|
+
# Periodically cleans up stale observers from the ObserveRegistry.
|
|
6
|
+
# Observers are considered stale if they haven't received notifications
|
|
7
|
+
# for longer than max_age seconds.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# cleanup = ObserverCleanup.new(interval: 60, max_age: 600)
|
|
11
|
+
# cleanup.start
|
|
12
|
+
# # ... cleanup runs in background ...
|
|
13
|
+
# cleanup.stop
|
|
14
|
+
class ObserverCleanup
|
|
15
|
+
@interval: untyped
|
|
16
|
+
|
|
17
|
+
@max_age: untyped
|
|
18
|
+
|
|
19
|
+
@running: untyped
|
|
20
|
+
|
|
21
|
+
@thread: untyped
|
|
22
|
+
|
|
23
|
+
@mutex: untyped
|
|
24
|
+
|
|
25
|
+
@stats: untyped
|
|
26
|
+
|
|
27
|
+
attr_reader interval: untyped
|
|
28
|
+
|
|
29
|
+
attr_reader max_age: untyped
|
|
30
|
+
|
|
31
|
+
# Initialize observer cleanup
|
|
32
|
+
# @param interval [Integer] Cleanup interval in seconds (default: 60)
|
|
33
|
+
# @param max_age [Integer] Max observer age in seconds (default: 600)
|
|
34
|
+
def initialize: (?interval: ::Integer, ?max_age: ::Integer) -> void
|
|
35
|
+
|
|
36
|
+
# Start the cleanup thread
|
|
37
|
+
def start: () -> (nil | untyped)
|
|
38
|
+
|
|
39
|
+
# Stop the cleanup thread
|
|
40
|
+
def stop: () -> (nil | untyped)
|
|
41
|
+
|
|
42
|
+
# Check if cleanup is running
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def running?: () -> untyped
|
|
45
|
+
|
|
46
|
+
# Get cleanup statistics
|
|
47
|
+
# @return [Hash] Statistics
|
|
48
|
+
def stats: () -> untyped
|
|
49
|
+
|
|
50
|
+
# Force a cleanup run (for testing)
|
|
51
|
+
def cleanup_now: () -> untyped
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Main cleanup loop
|
|
56
|
+
def run_cleanup_loop: () -> untyped
|
|
57
|
+
|
|
58
|
+
# Cleanup stale observers from ObserveRegistry
|
|
59
|
+
def cleanup_stale_observers: () -> untyped
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|