quantum-framework 0.9.0__py3-none-any.whl

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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,585 @@
1
+ """
2
+ WebSocket Service - Connection Management and Messaging
3
+
4
+ Manages WebSocket connections for Quantum components, providing:
5
+ - Connection lifecycle management
6
+ - Message sending/receiving
7
+ - Event dispatching
8
+ - Reconnection logic
9
+ - Connection state tracking
10
+
11
+ Server-side counterpart to client WebSocket connections.
12
+
13
+ Usage:
14
+ from quantum.runtime.websocket_service import get_websocket_service
15
+
16
+ ws_service = get_websocket_service()
17
+
18
+ # Register connection handler
19
+ ws_service.register_handler("chat", on_message=handle_message)
20
+
21
+ # Send message to connected clients
22
+ ws_service.broadcast("chat", {"type": "notification", "text": "Hello!"})
23
+ """
24
+
25
+ import json
26
+ import logging
27
+ import threading
28
+ import time
29
+ from typing import Dict, List, Any, Optional, Callable, Set
30
+ from dataclasses import dataclass, field
31
+ from datetime import datetime
32
+ from enum import IntEnum
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class WebSocketState(IntEnum):
38
+ """WebSocket connection states (matches JavaScript WebSocket.readyState)."""
39
+ CONNECTING = 0
40
+ OPEN = 1
41
+ CLOSING = 2
42
+ CLOSED = 3
43
+
44
+
45
+ @dataclass
46
+ class WebSocketConnection:
47
+ """Represents a WebSocket connection."""
48
+ id: str
49
+ name: str
50
+ url: str
51
+ state: WebSocketState = WebSocketState.CLOSED
52
+ connected_at: Optional[datetime] = None
53
+ last_message_at: Optional[datetime] = None
54
+ message_count: int = 0
55
+ error: Optional[str] = None
56
+ metadata: Dict[str, Any] = field(default_factory=dict)
57
+ # "websockets" once a real socket was opened for this record; "none"
58
+ # while it is only bookkeeping.
59
+ transport: str = "none"
60
+
61
+ def to_dict(self) -> Dict[str, Any]:
62
+ """Convert to dictionary for template access."""
63
+ result = {
64
+ "id": self.id,
65
+ "name": self.name,
66
+ "url": self.url,
67
+ "connected": self.state == WebSocketState.OPEN,
68
+ "readyState": int(self.state),
69
+ "connectedAt": self.connected_at.isoformat() if self.connected_at else None,
70
+ "lastMessageAt": self.last_message_at.isoformat() if self.last_message_at else None,
71
+ "messageCount": self.message_count,
72
+ "error": self.error,
73
+ "transport": self.transport,
74
+ }
75
+ if self.transport == "none":
76
+ # Registered but never opened. A template checking {ws.connected}
77
+ # already gets False; this says WHY, so a record cannot be
78
+ # mistaken for a live socket.
79
+ result["note"] = (
80
+ "no socket was opened for this connection — nothing sent "
81
+ "through it is delivered"
82
+ )
83
+ return result
84
+
85
+
86
+ @dataclass
87
+ class WebSocketMessage:
88
+ """Represents a WebSocket message."""
89
+ connection_id: str
90
+ data: Any
91
+ type: str = "text" # text, json, binary
92
+ timestamp: datetime = field(default_factory=datetime.now)
93
+ direction: str = "incoming" # incoming, outgoing
94
+
95
+ def to_dict(self) -> Dict[str, Any]:
96
+ """Convert to dictionary."""
97
+ return {
98
+ "connectionId": self.connection_id,
99
+ "data": self.data,
100
+ "type": self.type,
101
+ "timestamp": self.timestamp.isoformat(),
102
+ "direction": self.direction
103
+ }
104
+
105
+
106
+ class WebSocketError(Exception):
107
+ """WebSocket-related error."""
108
+ pass
109
+
110
+
111
+ class WebSocketService:
112
+ """
113
+ Manages WebSocket connections and messaging.
114
+
115
+ This service handles:
116
+ - Connection registration and lifecycle
117
+ - Message routing to handlers
118
+ - Broadcasting to multiple connections
119
+ - Connection state tracking
120
+ """
121
+
122
+ def __init__(self):
123
+ self._connections: Dict[str, WebSocketConnection] = {}
124
+ self._handlers: Dict[str, Dict[str, List[Callable]]] = {}
125
+ self._message_queue: Dict[str, List[WebSocketMessage]] = {}
126
+ self._lock = threading.RLock()
127
+ self._connection_counter = 0
128
+
129
+ def _generate_connection_id(self) -> str:
130
+ """Generate unique connection ID."""
131
+ self._connection_counter += 1
132
+ return f"ws_{self._connection_counter}_{int(time.time() * 1000)}"
133
+
134
+ def register_connection(
135
+ self,
136
+ name: str,
137
+ url: str,
138
+ metadata: Optional[Dict[str, Any]] = None,
139
+ connect: Optional[bool] = None,
140
+ ) -> WebSocketConnection:
141
+ """
142
+ Register a WebSocket connection, and open it when asked to.
143
+
144
+ Args:
145
+ name: Connection name (used for referencing)
146
+ url: WebSocket URL
147
+ metadata: Additional connection metadata
148
+ connect: Open the socket now. Defaults to the metadata's
149
+ ``auto_connect`` (which is what q:websocket autoConnect
150
+ sets), i.e. registration alone opens nothing.
151
+
152
+ Returns:
153
+ WebSocketConnection object — check ``.state``/``.transport``, a
154
+ record is not proof of a socket.
155
+ """
156
+ with self._lock:
157
+ conn_id = self._generate_connection_id()
158
+ connection = WebSocketConnection(
159
+ id=conn_id,
160
+ name=name,
161
+ url=url,
162
+ state=WebSocketState.CONNECTING,
163
+ metadata=metadata or {}
164
+ )
165
+ self._connections[conn_id] = connection
166
+ self._message_queue[conn_id] = []
167
+
168
+ if connect is None:
169
+ connect = bool((metadata or {}).get('auto_connect'))
170
+
171
+ if connect:
172
+ # Deliberately outside the lock: open() blocks until the socket
173
+ # is up, and the transport calls back into set_connection_state
174
+ # from its own thread. Holding the lock here would deadlock.
175
+ self.open(conn_id)
176
+ else:
177
+ logger.info(
178
+ "q:websocket '%s' -> %s registered without connecting "
179
+ "(autoConnect off); nothing sent through it is delivered "
180
+ "until it is opened.", name, url
181
+ )
182
+ return connection
183
+
184
+ def open(self, name_or_id: str, timeout: float = 10.0) -> bool:
185
+ """Open a real socket for a registered connection.
186
+
187
+ Returns True only when the socket reached OPEN. On failure the
188
+ connection goes to CLOSED carrying the reason, rather than sitting
189
+ in CONNECTING forever the way it used to.
190
+ """
191
+ conn = self.get_connection(name_or_id)
192
+ if conn is None:
193
+ logger.warning("q:websocket: no connection named %r", name_or_id)
194
+ return False
195
+
196
+ try:
197
+ from quantum.runtime.websocket_transport import (
198
+ get_transport, HAS_WEBSOCKETS,
199
+ )
200
+ except ImportError: # pragma: no cover
201
+ HAS_WEBSOCKETS = False
202
+
203
+ if not HAS_WEBSOCKETS:
204
+ msg = ("no websocket transport available — install the "
205
+ "'websockets' package (pip install quantum[websocket])")
206
+ logger.warning("q:websocket '%s' -> %s: %s", conn.name, conn.url, msg)
207
+ self.set_connection_state(conn.id, WebSocketState.CLOSED, msg)
208
+ return False
209
+
210
+ conn_id = conn.id
211
+ transport = get_transport()
212
+ opened, error = transport.connect(
213
+ conn_id,
214
+ conn.url,
215
+ on_open=lambda: self.set_connection_state(conn_id, WebSocketState.OPEN),
216
+ on_message=lambda payload: self.receive_message(
217
+ conn_id, payload,
218
+ 'json' if isinstance(payload, (dict, list)) else 'text'),
219
+ on_close=lambda: self.set_connection_state(conn_id, WebSocketState.CLOSED),
220
+ on_error=lambda exc: self.set_connection_state(
221
+ conn_id, WebSocketState.CLOSED, str(exc)),
222
+ timeout=timeout,
223
+ )
224
+
225
+ # The callbacks run on the transport's handler thread, so they may not
226
+ # have landed yet. Settle the state here too — whichever arrives
227
+ # second is a no-op — so the caller never sees an open socket reported
228
+ # as CONNECTING, nor a refused one still "connecting" forever.
229
+ if opened:
230
+ with self._lock:
231
+ live = self._connections.get(conn_id)
232
+ if live is not None:
233
+ live.transport = "websockets"
234
+ self.set_connection_state(conn_id, WebSocketState.OPEN)
235
+ else:
236
+ self.set_connection_state(conn_id, WebSocketState.CLOSED, error)
237
+ return opened
238
+
239
+ def get_connection(self, name_or_id: str) -> Optional[WebSocketConnection]:
240
+ """Get connection by name or ID."""
241
+ with self._lock:
242
+ # Try by ID first
243
+ if name_or_id in self._connections:
244
+ return self._connections[name_or_id]
245
+
246
+ # Try by name
247
+ for conn in self._connections.values():
248
+ if conn.name == name_or_id:
249
+ return conn
250
+
251
+ return None
252
+
253
+ def get_connections_by_name(self, name: str) -> List[WebSocketConnection]:
254
+ """Get all connections with the given name."""
255
+ with self._lock:
256
+ return [c for c in self._connections.values() if c.name == name]
257
+
258
+ def set_connection_state(
259
+ self,
260
+ connection_id: str,
261
+ state: WebSocketState,
262
+ error: Optional[str] = None
263
+ ):
264
+ """Update connection state."""
265
+ # O handler roda DEPOIS de soltar o lock (ver _dispatch_event).
266
+ evento = None
267
+ with self._lock:
268
+ if connection_id in self._connections:
269
+ conn = self._connections[connection_id]
270
+ old_state = conn.state
271
+ conn.state = state
272
+ conn.error = error
273
+
274
+ if state == WebSocketState.OPEN and old_state != WebSocketState.OPEN:
275
+ conn.connected_at = datetime.now()
276
+ evento = ("connect", {})
277
+
278
+ elif (state in (WebSocketState.CLOSING, WebSocketState.CLOSED)
279
+ and old_state not in (WebSocketState.CLOSING,
280
+ WebSocketState.CLOSED)):
281
+ # Only on the transition. A failed connect settles the
282
+ # state from the caller AND from on_error AND from
283
+ # on_close; without this the app's on-close body would
284
+ # run three times for one failure.
285
+ evento = ("close", {"error": error})
286
+
287
+ logger.debug(f"Connection {connection_id} state: {old_state.name} -> {state.name}")
288
+
289
+ if evento is not None:
290
+ self._dispatch_event(connection_id, evento[0], evento[1])
291
+
292
+ def remove_connection(self, connection_id: str):
293
+ """Remove a connection."""
294
+ with self._lock:
295
+ if connection_id in self._connections:
296
+ del self._connections[connection_id]
297
+ if connection_id in self._message_queue:
298
+ del self._message_queue[connection_id]
299
+ if connection_id in self._handlers:
300
+ del self._handlers[connection_id]
301
+
302
+ logger.info(f"Removed WebSocket connection: {connection_id}")
303
+
304
+ def register_handler(
305
+ self,
306
+ connection_name: str,
307
+ event: str,
308
+ handler: Callable[[Dict[str, Any]], None]
309
+ ):
310
+ """
311
+ Register an event handler for a connection.
312
+
313
+ Args:
314
+ connection_name: Name of the connection
315
+ event: Event type (connect, message, error, close)
316
+ handler: Callback function
317
+ """
318
+ with self._lock:
319
+ if connection_name not in self._handlers:
320
+ self._handlers[connection_name] = {}
321
+ if event not in self._handlers[connection_name]:
322
+ self._handlers[connection_name][event] = []
323
+
324
+ self._handlers[connection_name][event].append(handler)
325
+ logger.debug(f"Registered handler for {connection_name}:{event}")
326
+
327
+ def _dispatch_event(
328
+ self,
329
+ connection_id: str,
330
+ event: str,
331
+ data: Dict[str, Any]
332
+ ):
333
+ """Dispatch event to registered handlers.
334
+
335
+ Os handlers rodam FORA do `self._lock`.
336
+
337
+ Antes eram chamados de dentro dele — `receive_message` embrulhava o
338
+ corpo inteiro num `with self._lock` e so entao despachava. O lock e do
339
+ servico, nao da conexao, entao um on-message lento de UMA conexao
340
+ parava tudo: nenhum outro send_message, nenhum outro receive_message,
341
+ nenhuma mudanca de estado de nenhuma outra conexao andava enquanto o
342
+ handler nao voltasse. Um <q:query> de dez segundos dentro de um
343
+ on-message congelava o servico de websocket inteiro por dez segundos.
344
+ (Isolar os workers de handler por conexao no transporte nao resolvia
345
+ isso sozinho: eles disputavam o mesmo lock.)
346
+
347
+ Aqui o lock so protege a LEITURA da lista de handlers.
348
+ """
349
+ with self._lock:
350
+ conn = self._connections.get(connection_id)
351
+ if not conn:
352
+ return
353
+ nome = conn.name
354
+ # Copia: um handler pode registrar outro, e a lista viva mudaria
355
+ # debaixo da iteracao.
356
+ handlers = list(self._handlers.get(nome, {}).get(event, []))
357
+
358
+ for handler in handlers:
359
+ try:
360
+ handler(data)
361
+ except Exception as e:
362
+ logger.error(f"Error in WebSocket handler {nome}:{event}: {e}")
363
+
364
+ def receive_message(
365
+ self,
366
+ connection_id: str,
367
+ data: Any,
368
+ msg_type: str = "text"
369
+ ):
370
+ """
371
+ Process an incoming message.
372
+
373
+ Args:
374
+ connection_id: Connection that received the message
375
+ data: Message data
376
+ msg_type: Message type (text, json, binary)
377
+ """
378
+ with self._lock:
379
+ if connection_id not in self._connections:
380
+ logger.warning(f"Message for unknown connection: {connection_id}")
381
+ return
382
+
383
+ conn = self._connections[connection_id]
384
+ conn.message_count += 1
385
+ conn.last_message_at = datetime.now()
386
+
387
+ # Parse JSON if needed
388
+ parsed_data = data
389
+ if msg_type == "json" and isinstance(data, str):
390
+ try:
391
+ parsed_data = json.loads(data)
392
+ except json.JSONDecodeError:
393
+ logger.warning(f"Failed to parse JSON message: {data[:100]}")
394
+
395
+ message = WebSocketMessage(
396
+ connection_id=connection_id,
397
+ data=parsed_data,
398
+ type=msg_type,
399
+ direction="incoming"
400
+ )
401
+
402
+ # Store in queue
403
+ self._message_queue[connection_id].append(message)
404
+
405
+ # Keep only last 100 messages per connection
406
+ if len(self._message_queue[connection_id]) > 100:
407
+ self._message_queue[connection_id] = self._message_queue[connection_id][-100:]
408
+
409
+ payload = {
410
+ "data": parsed_data,
411
+ "type": msg_type,
412
+ "raw": data,
413
+ "timestamp": message.timestamp.isoformat()
414
+ }
415
+
416
+ # Fora do lock: o handler e codigo do app e pode demorar o que quiser
417
+ # sem parar as outras conexoes.
418
+ self._dispatch_event(connection_id, "message", payload)
419
+
420
+ def send_message(
421
+ self,
422
+ connection_name: str,
423
+ data: Any,
424
+ msg_type: str = "text"
425
+ ) -> bool:
426
+ """
427
+ Send a message to a connection.
428
+
429
+ Args:
430
+ connection_name: Name of the connection
431
+ data: Message data
432
+ msg_type: Message type (text, json)
433
+
434
+ Returns:
435
+ True if the message was written to a live socket, or recorded
436
+ for a connection that has no transport.
437
+ """
438
+ connections = self.get_connections_by_name(connection_name)
439
+ if not connections:
440
+ logger.warning(f"No connections found for: {connection_name}")
441
+ return False
442
+
443
+ success = False
444
+ for conn in connections:
445
+ if self._send_to_connection(conn, data, msg_type):
446
+ success = True
447
+
448
+ return success
449
+
450
+ def _send_to_connection(
451
+ self,
452
+ conn: WebSocketConnection,
453
+ data: Any,
454
+ msg_type: str
455
+ ) -> bool:
456
+ """Write to exactly ONE connection. Returns True if it was written.
457
+
458
+ Existe porque `broadcast` chamava `send_message(conn.name, ...)` dentro
459
+ de um laco sobre as conexoes com aquele nome: send_message ja percorre
460
+ TODAS as conexoes do nome, entao N conexoes recebiam N frames cada —
461
+ N*N no total — e a contagem devolvida era N. Com 50 clientes num
462
+ broadcast eram 2500 frames e cada cliente via a mesma mensagem 50
463
+ vezes. O laco de fora precisa de um envio para UMA conexao.
464
+ """
465
+ if conn.state != WebSocketState.OPEN:
466
+ return False
467
+
468
+ # Serialize if needed
469
+ send_data = data
470
+ if msg_type == "json" and not isinstance(data, str):
471
+ send_data = json.dumps(data, default=str)
472
+
473
+ if conn.transport == "websockets":
474
+ from quantum.runtime.websocket_transport import get_transport
475
+ if not get_transport().send(conn.id, send_data):
476
+ logger.warning(
477
+ "q:websocket send to '%s' failed — socket not "
478
+ "writable", conn.name)
479
+ return False
480
+
481
+ message = WebSocketMessage(
482
+ connection_id=conn.id,
483
+ data=send_data,
484
+ type=msg_type,
485
+ direction="outgoing"
486
+ )
487
+ with self._lock:
488
+ self._message_queue.setdefault(conn.id, []).append(message)
489
+
490
+ logger.debug(f"Message sent to {conn.name}: {str(data)[:100]}")
491
+ return True
492
+
493
+ def broadcast(
494
+ self,
495
+ connection_name: str,
496
+ data: Any,
497
+ msg_type: str = "json"
498
+ ) -> int:
499
+ """
500
+ Broadcast message to all connections with the given name.
501
+
502
+ Args:
503
+ connection_name: Name of connections to broadcast to
504
+ data: Message data
505
+ msg_type: Message type
506
+
507
+ Returns:
508
+ Number of connections messaged
509
+ """
510
+ connections = self.get_connections_by_name(connection_name)
511
+ sent = 0
512
+
513
+ for conn in connections:
514
+ # UMA conexao por volta. Chamar send_message(conn.name) aqui
515
+ # reenviava para o grupo inteiro a cada volta.
516
+ if self._send_to_connection(conn, data, msg_type):
517
+ sent += 1
518
+
519
+ return sent
520
+
521
+ def get_pending_messages(self, connection_id: str) -> List[WebSocketMessage]:
522
+ """Get pending outgoing messages for a connection."""
523
+ with self._lock:
524
+ messages = [
525
+ m for m in self._message_queue.get(connection_id, [])
526
+ if m.direction == "outgoing"
527
+ ]
528
+ # Clear sent messages
529
+ self._message_queue[connection_id] = [
530
+ m for m in self._message_queue.get(connection_id, [])
531
+ if m.direction == "incoming"
532
+ ]
533
+ return messages
534
+
535
+ def get_connection_status(self, name: str) -> Dict[str, Any]:
536
+ """Get status object for a connection."""
537
+ conn = self.get_connection(name)
538
+ if conn:
539
+ return conn.to_dict()
540
+ return {
541
+ "connected": False,
542
+ "readyState": WebSocketState.CLOSED,
543
+ "error": "Connection not found"
544
+ }
545
+
546
+ def get_all_connections(self) -> List[Dict[str, Any]]:
547
+ """Get all connections as dictionaries."""
548
+ with self._lock:
549
+ return [c.to_dict() for c in self._connections.values()]
550
+
551
+ def close_connection(
552
+ self,
553
+ connection_name: str,
554
+ code: int = 1000,
555
+ reason: str = ""
556
+ ):
557
+ """Close the connection — the socket too, when there is one."""
558
+ connections = self.get_connections_by_name(connection_name)
559
+ for conn in connections:
560
+ self.set_connection_state(
561
+ conn.id,
562
+ WebSocketState.CLOSING,
563
+ reason or f"Close requested (code: {code})"
564
+ )
565
+ if conn.transport == "websockets":
566
+ from quantum.runtime.websocket_transport import get_transport
567
+ get_transport().close(conn.id)
568
+
569
+
570
+ # Global singleton
571
+ _websocket_service: Optional[WebSocketService] = None
572
+
573
+
574
+ def get_websocket_service() -> WebSocketService:
575
+ """Get the global WebSocket service instance."""
576
+ global _websocket_service
577
+ if _websocket_service is None:
578
+ _websocket_service = WebSocketService()
579
+ return _websocket_service
580
+
581
+
582
+ def reset_websocket_service():
583
+ """Reset the global WebSocket service (for testing)."""
584
+ global _websocket_service
585
+ _websocket_service = None