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.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +70 -7
  3. data/.yard/templates/default/layout/html/layout.erb +34 -0
  4. data/AGENTS.md +16 -0
  5. data/CHANGELOG.md +158 -1
  6. data/CODE_OF_CONDUCT.md +1 -1
  7. data/README.md +590 -23
  8. data/ROADMAP.md +55 -0
  9. data/Rakefile +4 -4
  10. data/Steepfile +39 -0
  11. data/bin/takagi-dev +159 -0
  12. data/docs/FIRST_PLUGIN_GUIDE.md +224 -0
  13. data/docs/HOOKS.md +31 -0
  14. data/examples/client_lifecycle_example.rb +118 -0
  15. data/examples/cloud_gateway_app.rb +217 -0
  16. data/examples/nested_api_app.rb +258 -0
  17. data/examples/simple_device_app.rb +71 -0
  18. data/examples/takagi.yml +138 -0
  19. data/lib/takagi/application.rb +256 -0
  20. data/lib/takagi/base/middleware_management.rb +39 -0
  21. data/lib/takagi/base/plugin_management.rb +75 -0
  22. data/lib/takagi/base/reactor_management.rb +104 -0
  23. data/lib/takagi/base/server_lifecycle.rb +156 -0
  24. data/lib/takagi/base.rb +103 -11
  25. data/lib/takagi/branding.rb +88 -0
  26. data/lib/takagi/cbor/decoder.rb +385 -0
  27. data/lib/takagi/cbor/encoder.rb +260 -0
  28. data/lib/takagi/cbor/error.rb +17 -0
  29. data/lib/takagi/cbor/version.rb +9 -0
  30. data/lib/takagi/client/response.rb +236 -0
  31. data/lib/takagi/client.rb +265 -0
  32. data/lib/takagi/client_base.rb +204 -0
  33. data/lib/takagi/coap/code_helpers.rb +190 -0
  34. data/lib/takagi/coap/registries/base.rb +165 -0
  35. data/lib/takagi/coap/registries/content_format.rb +71 -0
  36. data/lib/takagi/coap/registries/message_type.rb +69 -0
  37. data/lib/takagi/coap/registries/method.rb +38 -0
  38. data/lib/takagi/coap/registries/option.rb +71 -0
  39. data/lib/takagi/coap/registries/response.rb +93 -0
  40. data/lib/takagi/coap/registries/signaling.rb +34 -0
  41. data/lib/takagi/coap/signaling.rb +10 -0
  42. data/lib/takagi/coap.rb +37 -0
  43. data/lib/takagi/composite_router.rb +186 -0
  44. data/lib/takagi/config.rb +337 -0
  45. data/lib/takagi/controller/resource_allocator.rb +164 -0
  46. data/lib/takagi/controller/thread_pool.rb +144 -0
  47. data/lib/takagi/controller.rb +319 -0
  48. data/lib/takagi/core/attribute_set.rb +128 -0
  49. data/lib/takagi/discovery/core_link_format.rb +137 -0
  50. data/lib/takagi/errors.rb +536 -0
  51. data/lib/takagi/event_bus/address_prefix.rb +142 -0
  52. data/lib/takagi/event_bus/async_executor.rb +235 -0
  53. data/lib/takagi/event_bus/coap_bridge.rb +208 -0
  54. data/lib/takagi/event_bus/future.rb +153 -0
  55. data/lib/takagi/event_bus/lru_cache.rb +157 -0
  56. data/lib/takagi/event_bus/message_buffer.rb +237 -0
  57. data/lib/takagi/event_bus/observer_cleanup.rb +110 -0
  58. data/lib/takagi/event_bus/scope.rb +74 -0
  59. data/lib/takagi/event_bus.rb +594 -0
  60. data/lib/takagi/helpers.rb +88 -0
  61. data/lib/takagi/hooks.rb +82 -0
  62. data/lib/takagi/initializer.rb +18 -0
  63. data/lib/takagi/logger.rb +15 -6
  64. data/lib/takagi/message/base.rb +155 -0
  65. data/lib/takagi/message/deduplication_cache.rb +84 -0
  66. data/lib/takagi/message/inbound.rb +147 -0
  67. data/lib/takagi/message/outbound.rb +223 -0
  68. data/lib/takagi/message/request.rb +158 -0
  69. data/lib/takagi/message/retransmission_manager.rb +193 -0
  70. data/lib/takagi/middleware/authentication.rb +19 -0
  71. data/lib/takagi/middleware/caching.rb +23 -0
  72. data/lib/takagi/middleware/debugging.rb +16 -0
  73. data/lib/takagi/middleware/logging.rb +14 -0
  74. data/lib/takagi/middleware/metrics.rb +440 -0
  75. data/lib/takagi/middleware/rate_limiting.rb +24 -0
  76. data/lib/takagi/middleware_stack.rb +166 -0
  77. data/lib/takagi/network/base.rb +76 -0
  78. data/lib/takagi/network/framing/tcp.rb +222 -0
  79. data/lib/takagi/network/framing/udp.rb +110 -0
  80. data/lib/takagi/network/registry.rb +72 -0
  81. data/lib/takagi/network/tcp.rb +60 -0
  82. data/lib/takagi/network/tcp_sender.rb +21 -0
  83. data/lib/takagi/network/udp.rb +61 -0
  84. data/lib/takagi/network/udp_sender.rb +20 -0
  85. data/lib/takagi/observable/emitter.rb +62 -0
  86. data/lib/takagi/observable/reactor.rb +488 -0
  87. data/lib/takagi/observable/registry.rb +122 -0
  88. data/lib/takagi/observe_registry.rb +10 -0
  89. data/lib/takagi/observer/client.rb +68 -0
  90. data/lib/takagi/observer/registry.rb +137 -0
  91. data/lib/takagi/observer/sender.rb +39 -0
  92. data/lib/takagi/observer/watcher.rb +43 -0
  93. data/lib/takagi/plugin.rb +313 -0
  94. data/lib/takagi/profiles.rb +176 -0
  95. data/lib/takagi/reactor.rb +23 -0
  96. data/lib/takagi/reactor_registry.rb +64 -0
  97. data/lib/takagi/registry/base.rb +268 -0
  98. data/lib/takagi/response_builder.rb +141 -0
  99. data/lib/takagi/router/metadata_extractor.rb +133 -0
  100. data/lib/takagi/router/route_matcher.rb +83 -0
  101. data/lib/takagi/router.rb +284 -25
  102. data/lib/takagi/serialization/base.rb +102 -0
  103. data/lib/takagi/serialization/cbor_serializer.rb +92 -0
  104. data/lib/takagi/serialization/json_serializer.rb +96 -0
  105. data/lib/takagi/serialization/octet_stream_serializer.rb +82 -0
  106. data/lib/takagi/serialization/registry.rb +187 -0
  107. data/lib/takagi/serialization/text_serializer.rb +87 -0
  108. data/lib/takagi/serialization.rb +117 -0
  109. data/lib/takagi/server/multi.rb +41 -0
  110. data/lib/takagi/server/registry.rb +71 -0
  111. data/lib/takagi/server/tcp.rb +249 -0
  112. data/lib/takagi/server/udp.rb +139 -0
  113. data/lib/takagi/server/udp_worker.rb +174 -0
  114. data/lib/takagi/server.rb +1 -31
  115. data/lib/takagi/server_registry.rb +10 -0
  116. data/lib/takagi/tcp_client.rb +142 -0
  117. data/lib/takagi/version.rb +2 -1
  118. data/lib/takagi.rb +24 -3
  119. data/sig/takagi/application.rbs +48 -0
  120. data/sig/takagi/base/middleware_management.rbs +33 -0
  121. data/sig/takagi/base/reactor_management.rbs +52 -0
  122. data/sig/takagi/base/server_lifecycle.rbs +54 -0
  123. data/sig/takagi/base.rbs +48 -0
  124. data/sig/takagi/cbor/decoder.rbs +171 -0
  125. data/sig/takagi/cbor/encoder.rbs +146 -0
  126. data/sig/takagi/cbor/error.rbs +19 -0
  127. data/sig/takagi/cbor/version.rbs +7 -0
  128. data/sig/takagi/client/response.rbs +148 -0
  129. data/sig/takagi/client.rbs +119 -0
  130. data/sig/takagi/client_base.rbs +135 -0
  131. data/sig/takagi/coap/code_helpers.rbs +91 -0
  132. data/sig/takagi/coap/registries/base.rbs +95 -0
  133. data/sig/takagi/coap/registries/content_format.rbs +47 -0
  134. data/sig/takagi/coap/registries/message_type.rbs +53 -0
  135. data/sig/takagi/coap/registries/method.rbs +27 -0
  136. data/sig/takagi/coap/registries/option.rbs +43 -0
  137. data/sig/takagi/coap/registries/response.rbs +52 -0
  138. data/sig/takagi/coap.rbs +24 -0
  139. data/sig/takagi/composite_router.rbs +46 -0
  140. data/sig/takagi/config.rbs +134 -0
  141. data/sig/takagi/controller.rbs +73 -0
  142. data/sig/takagi/core/attribute_set.rbs +57 -0
  143. data/sig/takagi/discovery/core_link_format.rbs +50 -0
  144. data/sig/takagi/event_bus/address_prefix.rbs +78 -0
  145. data/sig/takagi/event_bus/async_executor.rbs +88 -0
  146. data/sig/takagi/event_bus/coap_bridge.rbs +93 -0
  147. data/sig/takagi/event_bus/future.rbs +78 -0
  148. data/sig/takagi/event_bus/lru_cache.rbs +86 -0
  149. data/sig/takagi/event_bus/message_buffer.rbs +133 -0
  150. data/sig/takagi/event_bus/observer_cleanup.rbs +62 -0
  151. data/sig/takagi/event_bus.rbs +320 -0
  152. data/sig/takagi/helpers.rbs +34 -0
  153. data/sig/takagi/initializer.rbs +9 -0
  154. data/sig/takagi/logger.rbs +17 -0
  155. data/sig/takagi/message/base.rbs +64 -0
  156. data/sig/takagi/message/deduplication_cache.rbs +49 -0
  157. data/sig/takagi/message/inbound.rbs +76 -0
  158. data/sig/takagi/message/outbound.rbs +48 -0
  159. data/sig/takagi/message/request.rbs +32 -0
  160. data/sig/takagi/message/retransmission_manager.rbs +76 -0
  161. data/sig/takagi/middleware/authentication.rbs +11 -0
  162. data/sig/takagi/middleware/caching.rbs +13 -0
  163. data/sig/takagi/middleware/debugging.rbs +9 -0
  164. data/sig/takagi/middleware/logging.rbs +7 -0
  165. data/sig/takagi/middleware/metrics.rbs +15 -0
  166. data/sig/takagi/middleware/rate_limiting.rbs +13 -0
  167. data/sig/takagi/middleware_stack.rbs +69 -0
  168. data/sig/takagi/network/tcp_sender.rbs +10 -0
  169. data/sig/takagi/network/udp_sender.rbs +14 -0
  170. data/sig/takagi/observe_registry.rbs +36 -0
  171. data/sig/takagi/observer/client.rbs +36 -0
  172. data/sig/takagi/observer/sender.rbs +12 -0
  173. data/sig/takagi/observer/watcher.rbs +18 -0
  174. data/sig/takagi/profiles.rbs +33 -0
  175. data/sig/takagi/reactor.rbs +20 -0
  176. data/sig/takagi/reactor_registry.rbs +14 -0
  177. data/sig/takagi/response_builder.rbs +12 -0
  178. data/sig/takagi/router/metadata_extractor.rbs +71 -0
  179. data/sig/takagi/router/route_matcher.rbs +43 -0
  180. data/sig/takagi/router.rbs +166 -0
  181. data/sig/takagi/serialization.rbs +32 -0
  182. data/sig/takagi/server/multi.rbs +16 -0
  183. data/sig/takagi/server/tcp.rbs +42 -0
  184. data/sig/takagi/server/udp.rbs +52 -0
  185. data/sig/takagi/server/udp_worker.rbs +42 -0
  186. data/sig/takagi/server.rbs +4 -0
  187. data/sig/takagi/server_registry.rbs +71 -0
  188. data/sig/takagi/tcp_client.rbs +23 -0
  189. data/sig/takagi/version.rbs +5 -0
  190. data/takagi.gemspec +37 -35
  191. metadata +204 -31
  192. data/.idea/.gitignore +0 -8
  193. data/.idea/misc.xml +0 -4
  194. data/.idea/modules.xml +0 -8
  195. data/.idea/takagi.iml +0 -81
  196. data/.idea/vcs.xml +0 -6
  197. data/lib/takagi/message.rb +0 -75
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ # Application class for modular controller-based apps
5
+ #
6
+ # Application provides a centralized way to mount and manage multiple
7
+ # controllers, auto-load controller files, and run the server.
8
+ #
9
+ # @example Simple application
10
+ # class MyApp < Takagi::Application
11
+ # configure do
12
+ # load_controllers TelemetryController, ConfigController
13
+ # end
14
+ # end
15
+ #
16
+ # MyApp.run!(port: 5683)
17
+ #
18
+ # @example With auto-loading
19
+ # class MyApp < Takagi::Application
20
+ # configure do
21
+ # auto_load 'app/controllers/**/*_controller.rb'
22
+ # end
23
+ # end
24
+ #
25
+ # MyApp.run!
26
+ class Application
27
+ # Internal controller for CoRE Link Format discovery endpoint
28
+ # Automatically mounted by Application
29
+ class DiscoveryController < Controller
30
+ @@app_router = nil
31
+
32
+ def self.app_router=(router)
33
+ @@app_router = router
34
+ end
35
+
36
+ def self.app_router
37
+ @@app_router
38
+ end
39
+
40
+ configure do
41
+ mount '/.well-known'
42
+ end
43
+
44
+ get '/core', metadata: {
45
+ rt: 'core.discovery',
46
+ if: 'core.rd',
47
+ ct: Discovery::CoreLinkFormat::CONTENT_FORMAT,
48
+ discovery: true,
49
+ title: 'Resource Discovery'
50
+ } do |req|
51
+ # Get the composite router from the application
52
+ app_router = DiscoveryController.app_router
53
+ payload = Discovery::CoreLinkFormat.generate(router: app_router, request: req)
54
+ req.to_response(
55
+ '2.05 Content',
56
+ payload,
57
+ options: { CoAP::Registries::Option::CONTENT_FORMAT => Discovery::CoreLinkFormat::CONTENT_FORMAT }
58
+ )
59
+ end
60
+ end
61
+
62
+ class << self
63
+ # Get the application's composite router
64
+ #
65
+ # @return [CompositeRouter] The application's router
66
+ def router
67
+ @router ||= CompositeRouter.new
68
+ end
69
+
70
+ # Get the application's configuration
71
+ #
72
+ # @return [Hash] Configuration hash
73
+ def config
74
+ @config ||= {
75
+ controllers: [],
76
+ auto_load_patterns: [],
77
+ allocation_mode: :automatic, # :manual or :automatic
78
+ total_threads: nil # For automatic mode
79
+ }
80
+ end
81
+
82
+ # Configure the application
83
+ #
84
+ # @yield Block for configuration DSL
85
+ #
86
+ # @example
87
+ # configure do
88
+ # load_controllers TelemetryController, ConfigController
89
+ # auto_load 'app/controllers/**/*_controller.rb'
90
+ # end
91
+ def configure(&block)
92
+ ConfigContext.new(self).instance_eval(&block) if block
93
+ end
94
+
95
+ # Load and mount all registered controllers
96
+ #
97
+ # @return [void]
98
+ def load_controllers!
99
+ # Load auto-discovered controllers
100
+ auto_load_controllers! if config[:auto_load_patterns].any?
101
+
102
+ # Mount discovery controller first (so it's available at /.well-known/core)
103
+ # Store reference to composite router for discovery endpoint
104
+ DiscoveryController.app_router = router
105
+ router.mount(DiscoveryController)
106
+
107
+ # Mount all registered controllers
108
+ config[:controllers].each do |controller_class|
109
+ router.mount(controller_class)
110
+ end
111
+ end
112
+
113
+ # Start worker thread pools for all controllers
114
+ #
115
+ # Allocates thread pool resources based on allocation_mode:
116
+ # - :manual - Controllers use their explicit thread_count or profile settings
117
+ # - :automatic - Divides total_threads proportionally by profile weights
118
+ #
119
+ # @return [void]
120
+ def start_all_workers!
121
+ # Use app config first, fall back to global config
122
+ mode = config[:allocation_mode] || Takagi.config.allocation.mode
123
+ total_threads = config[:total_threads] || Takagi.config.allocation.total_threads
124
+ controllers_to_allocate = config[:controllers] + [DiscoveryController]
125
+
126
+ # Calculate allocations based on mode
127
+ allocations = Controller::ResourceAllocator.allocate(
128
+ controllers: controllers_to_allocate,
129
+ mode: mode,
130
+ total_threads: total_threads,
131
+ protocol: :tcp # TCP uses threads, UDP uses processes
132
+ )
133
+
134
+ # Validate allocations if automatic mode with total_threads specified
135
+ if mode == :automatic && total_threads
136
+ Controller::ResourceAllocator.validate!(allocations, total_threads: total_threads)
137
+ end
138
+
139
+ # Start each controller's thread pool
140
+ allocations.each do |controller_class, allocation|
141
+ threads = allocation[:threads]
142
+ next unless threads && threads > 0
143
+
144
+ controller_class.start_workers!(
145
+ threads: threads,
146
+ name: controller_class.name.split('::').last
147
+ )
148
+
149
+ Takagi.logger.info "Allocated #{threads} threads to #{controller_class.name} (mode: #{mode})"
150
+ end
151
+ end
152
+
153
+ # Shutdown all controller worker thread pools
154
+ #
155
+ # @return [void]
156
+ def shutdown_all_workers!
157
+ all_controllers = config[:controllers] + [DiscoveryController]
158
+
159
+ all_controllers.each do |controller_class|
160
+ controller_class.shutdown_workers! if controller_class.workers_running?
161
+ end
162
+ end
163
+
164
+ # Run the application server
165
+ #
166
+ # @param options [Hash] Server options (port, protocols, etc.)
167
+ # @return [void]
168
+ #
169
+ # @example
170
+ # MyApp.run!(port: 5683, protocols: [:udp, :tcp])
171
+ def run!(**options)
172
+ # Load all controllers
173
+ load_controllers!
174
+
175
+ # Start controller worker pools
176
+ start_all_workers!
177
+
178
+ # Use the composite router instead of global singleton
179
+ options[:router] = router
180
+
181
+ # Delegate to server lifecycle (same as Takagi::Base)
182
+ Base::ServerLifecycle.run!(**options)
183
+ end
184
+
185
+ # Get all loaded controller classes
186
+ #
187
+ # @return [Array<Class>] List of controller classes
188
+ def controllers
189
+ config[:controllers]
190
+ end
191
+
192
+ private
193
+
194
+ # Auto-load controllers from file patterns
195
+ #
196
+ # @return [void]
197
+ def auto_load_controllers!
198
+ config[:auto_load_patterns].each do |pattern|
199
+ Dir.glob(pattern).each do |file|
200
+ require_relative file
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ # Configuration DSL context for Application
207
+ class ConfigContext
208
+ def initialize(app_class)
209
+ @app = app_class
210
+ end
211
+
212
+ # Load specific controller classes
213
+ #
214
+ # @param controllers [Array<Class>] Controller classes to load
215
+ #
216
+ # @example
217
+ # load_controllers TelemetryController, ConfigController
218
+ def load_controllers(*controllers)
219
+ @app.config[:controllers].concat(controllers)
220
+ end
221
+
222
+ # Auto-load controllers from file pattern
223
+ #
224
+ # @param pattern [String] Glob pattern for controller files
225
+ #
226
+ # @example
227
+ # auto_load 'app/controllers/**/*_controller.rb'
228
+ def auto_load(pattern)
229
+ @app.config[:auto_load_patterns] << pattern
230
+ end
231
+
232
+ # Configure worker allocation mode
233
+ #
234
+ # @param mode [Symbol] :manual or :automatic
235
+ # @param threads [Integer] Total threads for automatic mode
236
+ #
237
+ # @example Manual allocation (controllers specify their own thread counts)
238
+ # allocation :manual
239
+ #
240
+ # @example Automatic allocation (divide 40 threads among controllers)
241
+ # allocation :automatic, threads: 40
242
+ def allocation(mode, threads: nil)
243
+ unless [:manual, :automatic].include?(mode)
244
+ raise ArgumentError, "Invalid allocation mode: #{mode}. Use :manual or :automatic"
245
+ end
246
+
247
+ if mode == :automatic && threads.nil?
248
+ raise ArgumentError, "Automatic allocation requires threads: parameter"
249
+ end
250
+
251
+ @app.config[:allocation_mode] = mode
252
+ @app.config[:total_threads] = threads
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ class Base < Router
5
+ # Manages middleware stack configuration and request processing.
6
+ #
7
+ # Extracted from Base class to follow Single Responsibility Principle.
8
+ # Handles middleware registration and stack initialization.
9
+ module MiddlewareManagement
10
+ # Processes a request through the middleware stack
11
+ #
12
+ # @param request [Message::Inbound] Incoming CoAP request
13
+ # @return [Message::Outbound] Response after middleware processing
14
+ def call(request)
15
+ middleware_stack.call(request)
16
+ end
17
+
18
+ # Returns the middleware stack instance
19
+ #
20
+ # Lazily initializes the middleware stack with default configuration
21
+ #
22
+ # @return [MiddlewareStack] The middleware stack instance
23
+ def middleware_stack
24
+ @middleware_stack ||= Takagi::MiddlewareStack.load_from_config('', router)
25
+ end
26
+
27
+ # Adds a middleware to the stack
28
+ #
29
+ # @param middleware [Object] Middleware instance responding to #call
30
+ #
31
+ # @example
32
+ # use CustomMiddleware.new
33
+ # use Takagi::Middleware::Caching.new(ttl: 300)
34
+ def use(middleware)
35
+ middleware_stack.use(middleware)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../plugin'
4
+ require_relative '../hooks'
5
+
6
+ module Takagi
7
+ class Base < Router
8
+ # Provides a simple DSL for declaring and enabling plugins on boot.
9
+ module PluginManagement
10
+ def plugin(plugin_ref, order: 0, **options)
11
+ plugins_config << { ref: plugin_ref, options: options, order: order }
12
+ end
13
+
14
+ def enable_plugins!(app: self)
15
+ load_config_plugins
16
+ Takagi::Plugin.auto_discover! if Takagi.config.plugins.auto_discover
17
+
18
+ merged = merged_plugins
19
+ merged.each do |entry|
20
+ mod = resolve_plugin(entry[:ref])
21
+ info = Takagi::Plugin.register(mod)
22
+ Takagi::Plugin.enable(info.name, app: app, options: entry[:options])
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def plugins_config
29
+ @plugins_config ||= []
30
+ end
31
+
32
+ def load_config_plugins
33
+ config_entries = Array(Takagi.config.plugins.enabled)
34
+ config_entries.each do |entry|
35
+ if entry.is_a?(Hash)
36
+ plugins_config << {
37
+ ref: entry[:name] || entry['name'],
38
+ options: entry[:options] || entry['options'] || {},
39
+ order: entry[:order] || entry['order'] || 0
40
+ }
41
+ else
42
+ plugins_config << { ref: entry, options: {}, order: 0 }
43
+ end
44
+ end
45
+ end
46
+
47
+ def merged_plugins
48
+ seen = {}
49
+ plugins_config.reverse_each do |entry|
50
+ key = entry[:ref].to_s
51
+ seen[key] ||= entry
52
+ end
53
+ seen.values.reverse.sort_by { |entry| entry[:order] || 0 }
54
+ end
55
+
56
+ def resolve_plugin(ref)
57
+ return ref if ref.is_a?(Module)
58
+
59
+ # Try Takagi::Plugins::<Name>
60
+ if ref.is_a?(Symbol) || ref.is_a?(String)
61
+ const_name = ref.to_s.split('_').map(&:capitalize).join
62
+ begin
63
+ return Takagi::Plugins.const_get(const_name)
64
+ rescue NameError
65
+ # fall through
66
+ end
67
+ # Try fully qualified
68
+ return Object.const_get(ref.to_s)
69
+ end
70
+
71
+ raise ArgumentError, "Unknown plugin reference: #{ref.inspect}"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Takagi
4
+ class Base < Router
5
+ # Manages reactor registration and lifecycle for observable/observer patterns.
6
+ #
7
+ # Supports both inline reactor definitions and file-based reactor classes.
8
+ # Provides DSL for defining observables and observers.
9
+ module ReactorManagement
10
+ # Defines a reactor using a DSL block (inline definition)
11
+ #
12
+ # The block is evaluated in the context of a new Reactor instance,
13
+ # allowing you to define observables and observers inline.
14
+ #
15
+ # @param threads [Integer] Number of threads for the reactor (default: 2)
16
+ # @param name [String] Optional name for the reactor
17
+ # @param interval [Float] Default polling interval in seconds (default: 1.0)
18
+ # @yield Block defining reactor configuration
19
+ #
20
+ # @example Event-driven observable
21
+ # reactor threads: 4 do
22
+ # observable '/temperature' do |emitter|
23
+ # emitter.on_event('sensor.temp.changed')
24
+ # end
25
+ # end
26
+ #
27
+ # @example Polling observable
28
+ # reactor do
29
+ # observable_polling '/status', interval: 5.0 do
30
+ # { status: check_status, uptime: uptime }
31
+ # end
32
+ # end
33
+ #
34
+ # @example Remote observation
35
+ # reactor do
36
+ # observe 'coap://sensor:5683/temp' do |data|
37
+ # process_temperature(data)
38
+ # end
39
+ # end
40
+ def reactor(threads: 2, name: nil, interval: 1.0, &block)
41
+ reactor_name = name || "#{self.name.split('::').last.downcase}-reactor"
42
+ reactor_instance = Takagi::Observable::Reactor.new(
43
+ threads: threads,
44
+ name: reactor_name,
45
+ interval: interval
46
+ )
47
+ reactor_instance.instance_eval(&block)
48
+
49
+ # Register with Observable::Registry
50
+ Takagi::Observable::Registry.register(reactor_name.to_sym, reactor_instance)
51
+
52
+ reactor_instance
53
+ end
54
+
55
+ # Registers a file-based reactor class instance
56
+ #
57
+ # Use this to register reactors defined in separate files (e.g., app/reactors/).
58
+ # The reactor class should inherit from Takagi::Observable::Reactor.
59
+ #
60
+ # @param name [Symbol] Unique identifier for the reactor
61
+ # @param klass_or_instance [Class, Reactor] Reactor class or instance
62
+ #
63
+ # @example Register reactor class
64
+ # class IotReactor < Takagi::Observable::Reactor
65
+ # def initialize
66
+ # super(threads: 8, name: 'iot')
67
+ # setup_device_observables
68
+ # end
69
+ # end
70
+ #
71
+ # use_reactor :iot, IotReactor
72
+ #
73
+ # @example Register reactor instance
74
+ # use_reactor :sensors, SensorReactor.new
75
+ def use_reactor(name, klass_or_instance)
76
+ reactor_instance = if klass_or_instance.is_a?(Class)
77
+ klass_or_instance.new
78
+ else
79
+ klass_or_instance
80
+ end
81
+
82
+ Takagi::Observable::Registry.register(name.to_sym, reactor_instance)
83
+ reactor_instance
84
+ end
85
+
86
+ # Starts all registered reactors
87
+ #
88
+ # Called automatically by server lifecycle management during boot.
89
+ # Can be called manually if needed.
90
+ #
91
+ # @return [void]
92
+ def start_reactors
93
+ Takagi::Observable::Registry.start_all
94
+ end
95
+
96
+ # Stops all registered reactors
97
+ #
98
+ # @return [void]
99
+ def stop_reactors
100
+ Takagi::Observable::Registry.stop_all
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../branding'
4
+
5
+ module Takagi
6
+ class Base < Router
7
+ # Manages server lifecycle operations: booting, running, and spawning servers.
8
+ #
9
+ # Extracted from Base class to follow Single Responsibility Principle.
10
+ # Handles configuration loading, server instantiation, and process management.
11
+ module ServerLifecycle
12
+ # Boots the application by loading configuration and running initializers
13
+ #
14
+ # @param config_path [String] Path to configuration file
15
+ def boot!(config_path: 'config/takagi.yml')
16
+ Takagi.config.load_file(config_path) if File.exist?(config_path)
17
+ Takagi::Initializer.run!
18
+ end
19
+
20
+ # Runs the server in the foreground (blocking)
21
+ #
22
+ # @param port [Integer, nil] Port to bind to (uses config if nil)
23
+ # @param config_path [String] Path to configuration file
24
+ # @param protocols [Array<Symbol>, nil] Protocols to enable (uses config if nil)
25
+ # @param router [Router, CompositeRouter, nil] Custom router (uses global if nil)
26
+ # @param banner [Boolean] Show startup banner (default: true)
27
+ def run!(port: nil, config_path: 'config/takagi.yml', protocols: nil, router: nil, banner: true)
28
+ boot!(config_path: config_path)
29
+ enable_plugins!(app: self)
30
+
31
+ # Show startup banner
32
+ if banner
33
+ print_startup_banner(
34
+ port: port || Takagi.config.port,
35
+ protocols: protocols || Takagi.config.protocols,
36
+ router: router || self.router # Pass router to banner
37
+ )
38
+ end
39
+
40
+ selected_port = port || Takagi.config.port
41
+ servers = build_servers(protocols || Takagi.config.protocols, selected_port, router: router)
42
+ run_servers(servers)
43
+ end
44
+
45
+ # Spawns servers in background threads
46
+ #
47
+ # @param port [Integer] Port to bind to
48
+ # @param protocols [Array<Symbol>, nil] Protocols to enable (uses config if nil)
49
+ # @return [Server, Multi] The spawned server instance
50
+ def spawn!(port: 5683, protocols: nil)
51
+ protos = if protocols
52
+ Array(protocols)
53
+ else
54
+ Takagi.config.protocols
55
+ end.map(&:to_sym)
56
+
57
+ servers = protos.map do |proto|
58
+ # Temporary backward compatibility - will use registry after transition
59
+ proto == :tcp ? Takagi::Server::Tcp.new(port: port) : Takagi::Server::Udp.new(port: port)
60
+ end
61
+
62
+ if servers.length == 1
63
+ server = servers.first
64
+ thread = Thread.new { server.run! }
65
+ # Store thread reference on the server instance for cleanup
66
+ server.instance_variable_set(:@server_thread, thread)
67
+ server
68
+ else
69
+ multi = Takagi::Server::Multi.new(servers)
70
+ thread = Thread.new { multi.run! }
71
+ # Store thread reference on the multi instance for cleanup
72
+ multi.instance_variable_set(:@server_thread, thread)
73
+ multi
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # Prints startup banner with Takagi branding
80
+ #
81
+ # @param port [Integer] Port server will bind to
82
+ # @param protocols [Array<Symbol>] Protocols being enabled
83
+ # @param router [Router] Router instance to count routes from
84
+ def print_startup_banner(port:, protocols:, router:)
85
+ # Print banner
86
+ Branding.print_banner(compact: false)
87
+
88
+ # Version info
89
+ version = defined?(Takagi::VERSION) ? Takagi::VERSION : '0.1.0'
90
+ Branding.print_version(version)
91
+
92
+ puts
93
+ puts Branding.section('Starting Server')
94
+
95
+ # Server info
96
+ puts Branding.log("Port: #{port}")
97
+ puts Branding.log("Protocols: #{protocols.map(&:to_s).map(&:upcase).join(', ')}")
98
+ puts Branding.log("Threads: #{Takagi.config.threads}")
99
+ puts Branding.log("Processes: #{Takagi.config.processes}") if Takagi.config.processes > 0
100
+
101
+ # Route count
102
+ route_count = router.instance_variable_get(:@routes)&.size || 0
103
+ puts Branding.log("Routes registered: #{route_count}")
104
+
105
+ puts
106
+ puts Branding.success('Server starting...')
107
+ puts Branding.info('Press Ctrl+C to shutdown')
108
+ puts
109
+ puts Branding::WAVE_LINE
110
+ puts
111
+ end
112
+
113
+ # Builds server instances for the given protocols
114
+ #
115
+ # @param protocols [Array<Symbol>] Protocol identifiers
116
+ # @param port [Integer] Port to bind to
117
+ # @param router [Router, CompositeRouter, nil] Custom router (uses global if nil)
118
+ # @return [Array<Server>] Array of server instances
119
+ def build_servers(protocols, port, router: nil)
120
+ threads = Takagi.config.threads
121
+ processes = Takagi.config.processes
122
+ Array(protocols).map(&:to_sym).map do |protocol|
123
+ instantiate_server(protocol, port, threads: threads, processes: processes, router: router)
124
+ end
125
+ end
126
+
127
+ # Instantiates a server for the given protocol using ServerRegistry
128
+ #
129
+ # @param protocol [Symbol] Protocol identifier (:udp, :tcp, etc.)
130
+ # @param port [Integer] Port to bind to
131
+ # @param threads [Integer] Number of worker threads
132
+ # @param processes [Integer] Number of worker processes (UDP only)
133
+ # @param router [Router, CompositeRouter, nil] Custom router (uses global if nil)
134
+ # @return [Server] Server instance
135
+ def instantiate_server(protocol, port, threads:, processes:, router: nil)
136
+ options = { port: port }
137
+ options[:worker_threads] = threads
138
+ options[:router] = router if router
139
+
140
+ # UDP requires worker_processes, TCP doesn't use it
141
+ options[:worker_processes] = processes if protocol == :udp
142
+ Takagi.logger.debug("Instantiating server for #{protocol} on port #{port} with #{options}")
143
+ Server::Registry.build(protocol, **options)
144
+ end
145
+
146
+ # Runs servers (single or multi-protocol)
147
+ #
148
+ # @param servers [Array<Server>] Server instances to run
149
+ def run_servers(servers)
150
+ return servers.first.run! if servers.length == 1
151
+
152
+ Takagi::Server::Multi.new(servers).run!
153
+ end
154
+ end
155
+ end
156
+ end