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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Hot Reload System
|
|
3
|
+
|
|
4
|
+
Provides file watching and WebSocket-based live reload for development.
|
|
5
|
+
|
|
6
|
+
Components:
|
|
7
|
+
- HotReloadWatcher: File watcher using watchdog
|
|
8
|
+
- HotReloadServer: WebSocket server to push updates to browsers
|
|
9
|
+
- HotReloadManager: Coordinates file watching and WebSocket broadcasting
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from quantum.cli.hot_reload import HotReloadManager
|
|
13
|
+
|
|
14
|
+
manager = HotReloadManager(
|
|
15
|
+
watch_paths=[Path('./components')],
|
|
16
|
+
port=35729
|
|
17
|
+
)
|
|
18
|
+
manager.start()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
import hashlib
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Callable, Dict, List, Optional, Set
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from enum import Enum
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ReloadType(Enum):
|
|
34
|
+
"""Type of reload to perform."""
|
|
35
|
+
FULL = "full" # Full page reload (structure changes)
|
|
36
|
+
CSS = "css" # CSS-only reload (style changes)
|
|
37
|
+
COMPONENT = "component" # Partial component reload
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class FileChange:
|
|
42
|
+
"""Represents a file change event."""
|
|
43
|
+
path: Path
|
|
44
|
+
change_type: str # 'modified', 'created', 'deleted'
|
|
45
|
+
timestamp: float = field(default_factory=time.time)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def extension(self) -> str:
|
|
49
|
+
return self.path.suffix.lower()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def reload_type(self) -> ReloadType:
|
|
53
|
+
"""Determine reload type based on file extension."""
|
|
54
|
+
if self.extension in ('.css', '.scss', '.sass', '.less'):
|
|
55
|
+
return ReloadType.CSS
|
|
56
|
+
return ReloadType.FULL
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class HotReloadWatcher:
|
|
60
|
+
"""
|
|
61
|
+
File watcher using watchdog library.
|
|
62
|
+
|
|
63
|
+
Watches specified directories for changes and triggers callbacks.
|
|
64
|
+
Includes debouncing to handle rapid file changes.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
paths: List[Path],
|
|
70
|
+
extensions: List[str] = None,
|
|
71
|
+
debounce_ms: int = 100,
|
|
72
|
+
on_change: Callable[[List[FileChange]], None] = None
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Initialize the file watcher.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
paths: Directories or files to watch
|
|
79
|
+
extensions: File extensions to watch (e.g., ['.q', '.css'])
|
|
80
|
+
debounce_ms: Debounce delay in milliseconds
|
|
81
|
+
on_change: Callback when files change
|
|
82
|
+
"""
|
|
83
|
+
self.paths = [Path(p).resolve() for p in paths]
|
|
84
|
+
self.extensions = extensions or ['.q', '.yaml', '.yml', '.css', '.js', '.html']
|
|
85
|
+
self.debounce_ms = debounce_ms
|
|
86
|
+
self.on_change = on_change
|
|
87
|
+
|
|
88
|
+
self._running = False
|
|
89
|
+
self._thread: Optional[threading.Thread] = None
|
|
90
|
+
self._observer = None
|
|
91
|
+
self._pending_changes: Dict[Path, FileChange] = {}
|
|
92
|
+
self._debounce_timer: Optional[threading.Timer] = None
|
|
93
|
+
self._lock = threading.Lock()
|
|
94
|
+
|
|
95
|
+
# File content hashes for detecting actual changes
|
|
96
|
+
self._file_hashes: Dict[Path, str] = {}
|
|
97
|
+
|
|
98
|
+
def start(self):
|
|
99
|
+
"""Start watching files."""
|
|
100
|
+
if self._running:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
self._running = True
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
from watchdog.observers import Observer
|
|
107
|
+
from watchdog.events import FileSystemEventHandler
|
|
108
|
+
|
|
109
|
+
watcher = self
|
|
110
|
+
|
|
111
|
+
class ChangeHandler(FileSystemEventHandler):
|
|
112
|
+
def on_modified(self, event):
|
|
113
|
+
if not event.is_directory:
|
|
114
|
+
watcher._handle_change(Path(event.src_path), 'modified')
|
|
115
|
+
|
|
116
|
+
def on_created(self, event):
|
|
117
|
+
if not event.is_directory:
|
|
118
|
+
watcher._handle_change(Path(event.src_path), 'created')
|
|
119
|
+
|
|
120
|
+
def on_deleted(self, event):
|
|
121
|
+
if not event.is_directory:
|
|
122
|
+
watcher._handle_change(Path(event.src_path), 'deleted')
|
|
123
|
+
|
|
124
|
+
self._observer = Observer()
|
|
125
|
+
handler = ChangeHandler()
|
|
126
|
+
|
|
127
|
+
for path in self.paths:
|
|
128
|
+
if path.exists():
|
|
129
|
+
if path.is_dir():
|
|
130
|
+
self._observer.schedule(handler, str(path), recursive=True)
|
|
131
|
+
else:
|
|
132
|
+
self._observer.schedule(handler, str(path.parent), recursive=False)
|
|
133
|
+
|
|
134
|
+
self._observer.start()
|
|
135
|
+
|
|
136
|
+
except ImportError:
|
|
137
|
+
# Fallback to polling if watchdog not available
|
|
138
|
+
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
|
139
|
+
self._thread.start()
|
|
140
|
+
|
|
141
|
+
def stop(self):
|
|
142
|
+
"""Stop watching files."""
|
|
143
|
+
self._running = False
|
|
144
|
+
|
|
145
|
+
if self._observer:
|
|
146
|
+
self._observer.stop()
|
|
147
|
+
self._observer.join(timeout=2)
|
|
148
|
+
self._observer = None
|
|
149
|
+
|
|
150
|
+
if self._thread:
|
|
151
|
+
self._thread.join(timeout=2)
|
|
152
|
+
self._thread = None
|
|
153
|
+
|
|
154
|
+
if self._debounce_timer:
|
|
155
|
+
self._debounce_timer.cancel()
|
|
156
|
+
self._debounce_timer = None
|
|
157
|
+
|
|
158
|
+
def _handle_change(self, path: Path, change_type: str):
|
|
159
|
+
"""Handle a file change event."""
|
|
160
|
+
path = path.resolve()
|
|
161
|
+
|
|
162
|
+
# Check extension filter
|
|
163
|
+
if self.extensions and path.suffix.lower() not in self.extensions:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Check if content actually changed (for modified events)
|
|
167
|
+
if change_type == 'modified':
|
|
168
|
+
try:
|
|
169
|
+
current_hash = self._compute_hash(path)
|
|
170
|
+
if path in self._file_hashes and self._file_hashes[path] == current_hash:
|
|
171
|
+
return # Content unchanged, skip
|
|
172
|
+
self._file_hashes[path] = current_hash
|
|
173
|
+
except (IOError, OSError):
|
|
174
|
+
pass # File might be locked or deleted
|
|
175
|
+
|
|
176
|
+
# Add to pending changes
|
|
177
|
+
with self._lock:
|
|
178
|
+
self._pending_changes[path] = FileChange(path, change_type)
|
|
179
|
+
|
|
180
|
+
# Reset debounce timer
|
|
181
|
+
if self._debounce_timer:
|
|
182
|
+
self._debounce_timer.cancel()
|
|
183
|
+
|
|
184
|
+
self._debounce_timer = threading.Timer(
|
|
185
|
+
self.debounce_ms / 1000.0,
|
|
186
|
+
self._flush_changes
|
|
187
|
+
)
|
|
188
|
+
self._debounce_timer.start()
|
|
189
|
+
|
|
190
|
+
def _flush_changes(self):
|
|
191
|
+
"""Flush pending changes to callback."""
|
|
192
|
+
with self._lock:
|
|
193
|
+
if not self._pending_changes:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
changes = list(self._pending_changes.values())
|
|
197
|
+
self._pending_changes.clear()
|
|
198
|
+
|
|
199
|
+
if self.on_change and changes:
|
|
200
|
+
try:
|
|
201
|
+
self.on_change(changes)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
print(f"[Hot Reload] Error in change callback: {e}")
|
|
204
|
+
|
|
205
|
+
def _compute_hash(self, path: Path) -> str:
|
|
206
|
+
"""Compute MD5 hash of file content."""
|
|
207
|
+
try:
|
|
208
|
+
content = path.read_bytes()
|
|
209
|
+
return hashlib.md5(content).hexdigest()
|
|
210
|
+
except (IOError, OSError):
|
|
211
|
+
return ""
|
|
212
|
+
|
|
213
|
+
def _poll_loop(self):
|
|
214
|
+
"""Polling fallback when watchdog is not available."""
|
|
215
|
+
last_mtimes: Dict[Path, float] = {}
|
|
216
|
+
|
|
217
|
+
while self._running:
|
|
218
|
+
try:
|
|
219
|
+
current_files: Set[Path] = set()
|
|
220
|
+
|
|
221
|
+
for base_path in self.paths:
|
|
222
|
+
if not base_path.exists():
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
if base_path.is_file():
|
|
226
|
+
files = [base_path]
|
|
227
|
+
else:
|
|
228
|
+
files = []
|
|
229
|
+
for ext in self.extensions:
|
|
230
|
+
files.extend(base_path.rglob(f'*{ext}'))
|
|
231
|
+
|
|
232
|
+
for file_path in files:
|
|
233
|
+
file_path = file_path.resolve()
|
|
234
|
+
current_files.add(file_path)
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
mtime = file_path.stat().st_mtime
|
|
238
|
+
|
|
239
|
+
if file_path in last_mtimes:
|
|
240
|
+
if mtime > last_mtimes[file_path]:
|
|
241
|
+
self._handle_change(file_path, 'modified')
|
|
242
|
+
|
|
243
|
+
last_mtimes[file_path] = mtime
|
|
244
|
+
except (IOError, OSError):
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
# Check for deleted files
|
|
248
|
+
for old_file in list(last_mtimes.keys()):
|
|
249
|
+
if old_file not in current_files:
|
|
250
|
+
self._handle_change(old_file, 'deleted')
|
|
251
|
+
del last_mtimes[old_file]
|
|
252
|
+
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
time.sleep(0.5)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class HotReloadServer:
|
|
260
|
+
"""
|
|
261
|
+
WebSocket server for pushing reload events to browsers.
|
|
262
|
+
|
|
263
|
+
Uses asyncio and the websockets library.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, host: str = 'localhost', port: int = 35729):
|
|
267
|
+
"""
|
|
268
|
+
Initialize the WebSocket server.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
host: Host to bind to
|
|
272
|
+
port: Port for WebSocket server
|
|
273
|
+
"""
|
|
274
|
+
self.host = host
|
|
275
|
+
self.port = port
|
|
276
|
+
self._clients: Set = set()
|
|
277
|
+
self._server = None
|
|
278
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
279
|
+
self._thread: Optional[threading.Thread] = None
|
|
280
|
+
self._running = False
|
|
281
|
+
|
|
282
|
+
def start(self):
|
|
283
|
+
"""Start the WebSocket server in a background thread."""
|
|
284
|
+
if self._running:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
self._running = True
|
|
288
|
+
self._thread = threading.Thread(target=self._run_server, daemon=True)
|
|
289
|
+
self._thread.start()
|
|
290
|
+
|
|
291
|
+
# Wait a bit for server to start
|
|
292
|
+
time.sleep(0.2)
|
|
293
|
+
|
|
294
|
+
def stop(self):
|
|
295
|
+
"""Stop the WebSocket server."""
|
|
296
|
+
self._running = False
|
|
297
|
+
|
|
298
|
+
if self._loop:
|
|
299
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
300
|
+
|
|
301
|
+
if self._thread:
|
|
302
|
+
self._thread.join(timeout=2)
|
|
303
|
+
self._thread = None
|
|
304
|
+
|
|
305
|
+
def _run_server(self):
|
|
306
|
+
"""Run the WebSocket server (in background thread)."""
|
|
307
|
+
try:
|
|
308
|
+
import websockets
|
|
309
|
+
import websockets.server
|
|
310
|
+
except ImportError:
|
|
311
|
+
print("[Hot Reload] websockets library not found. Using fallback polling.")
|
|
312
|
+
print("[Hot Reload] Install with: pip install websockets")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
async def handler(websocket):
|
|
316
|
+
"""Handle WebSocket connections."""
|
|
317
|
+
self._clients.add(websocket)
|
|
318
|
+
try:
|
|
319
|
+
# Send initial connection message
|
|
320
|
+
await websocket.send(json.dumps({
|
|
321
|
+
'type': 'connected',
|
|
322
|
+
'timestamp': time.time()
|
|
323
|
+
}))
|
|
324
|
+
|
|
325
|
+
# Keep connection alive
|
|
326
|
+
async for message in websocket:
|
|
327
|
+
# Handle ping/pong or other messages
|
|
328
|
+
try:
|
|
329
|
+
data = json.loads(message)
|
|
330
|
+
if data.get('type') == 'ping':
|
|
331
|
+
await websocket.send(json.dumps({'type': 'pong'}))
|
|
332
|
+
except json.JSONDecodeError:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
except websockets.exceptions.ConnectionClosed:
|
|
336
|
+
pass
|
|
337
|
+
finally:
|
|
338
|
+
self._clients.discard(websocket)
|
|
339
|
+
|
|
340
|
+
async def main():
|
|
341
|
+
async with websockets.server.serve(handler, self.host, self.port):
|
|
342
|
+
while self._running:
|
|
343
|
+
await asyncio.sleep(0.1)
|
|
344
|
+
|
|
345
|
+
self._loop = asyncio.new_event_loop()
|
|
346
|
+
asyncio.set_event_loop(self._loop)
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
self._loop.run_until_complete(main())
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
finally:
|
|
353
|
+
self._loop.close()
|
|
354
|
+
|
|
355
|
+
def broadcast(self, message: dict):
|
|
356
|
+
"""
|
|
357
|
+
Broadcast a message to all connected clients.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
message: Message dict to send as JSON
|
|
361
|
+
"""
|
|
362
|
+
if not self._clients or not self._loop:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
async def send_to_all():
|
|
366
|
+
json_msg = json.dumps(message)
|
|
367
|
+
disconnected = set()
|
|
368
|
+
|
|
369
|
+
for client in self._clients.copy():
|
|
370
|
+
try:
|
|
371
|
+
await client.send(json_msg)
|
|
372
|
+
except Exception:
|
|
373
|
+
disconnected.add(client)
|
|
374
|
+
|
|
375
|
+
self._clients -= disconnected
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
asyncio.run_coroutine_threadsafe(send_to_all(), self._loop)
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def client_count(self) -> int:
|
|
384
|
+
"""Number of connected clients."""
|
|
385
|
+
return len(self._clients)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class HotReloadManager:
|
|
389
|
+
"""
|
|
390
|
+
Coordinates file watching and WebSocket broadcasting.
|
|
391
|
+
|
|
392
|
+
Main interface for the hot reload system.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
def __init__(
|
|
396
|
+
self,
|
|
397
|
+
watch_paths: List[Path] = None,
|
|
398
|
+
extensions: List[str] = None,
|
|
399
|
+
ws_host: str = 'localhost',
|
|
400
|
+
ws_port: int = 35729,
|
|
401
|
+
debounce_ms: int = 100,
|
|
402
|
+
on_reload: Callable[[List[FileChange], ReloadType], None] = None,
|
|
403
|
+
console = None
|
|
404
|
+
):
|
|
405
|
+
"""
|
|
406
|
+
Initialize the hot reload manager.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
watch_paths: Paths to watch for changes
|
|
410
|
+
extensions: File extensions to watch
|
|
411
|
+
ws_host: WebSocket server host
|
|
412
|
+
ws_port: WebSocket server port
|
|
413
|
+
debounce_ms: Debounce delay in milliseconds
|
|
414
|
+
on_reload: Optional callback when reload is triggered
|
|
415
|
+
console: Optional console for logging (from cli.utils)
|
|
416
|
+
"""
|
|
417
|
+
self.watch_paths = watch_paths or [Path('.')]
|
|
418
|
+
self.extensions = extensions or ['.q', '.yaml', '.yml', '.css', '.js', '.html']
|
|
419
|
+
self.ws_host = ws_host
|
|
420
|
+
self.ws_port = ws_port
|
|
421
|
+
self.debounce_ms = debounce_ms
|
|
422
|
+
self.on_reload = on_reload
|
|
423
|
+
self.console = console
|
|
424
|
+
|
|
425
|
+
self._watcher: Optional[HotReloadWatcher] = None
|
|
426
|
+
self._server: Optional[HotReloadServer] = None
|
|
427
|
+
self._running = False
|
|
428
|
+
self._reload_count = 0
|
|
429
|
+
|
|
430
|
+
# Error tracking for overlay
|
|
431
|
+
self._last_error: Optional[dict] = None
|
|
432
|
+
|
|
433
|
+
def start(self):
|
|
434
|
+
"""Start the hot reload system."""
|
|
435
|
+
if self._running:
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
self._running = True
|
|
439
|
+
|
|
440
|
+
# Start WebSocket server
|
|
441
|
+
self._server = HotReloadServer(self.ws_host, self.ws_port)
|
|
442
|
+
self._server.start()
|
|
443
|
+
|
|
444
|
+
# Start file watcher
|
|
445
|
+
self._watcher = HotReloadWatcher(
|
|
446
|
+
paths=self.watch_paths,
|
|
447
|
+
extensions=self.extensions,
|
|
448
|
+
debounce_ms=self.debounce_ms,
|
|
449
|
+
on_change=self._handle_changes
|
|
450
|
+
)
|
|
451
|
+
self._watcher.start()
|
|
452
|
+
|
|
453
|
+
self._log(f"Hot reload started on ws://{self.ws_host}:{self.ws_port}")
|
|
454
|
+
|
|
455
|
+
def stop(self):
|
|
456
|
+
"""Stop the hot reload system."""
|
|
457
|
+
self._running = False
|
|
458
|
+
|
|
459
|
+
if self._watcher:
|
|
460
|
+
self._watcher.stop()
|
|
461
|
+
self._watcher = None
|
|
462
|
+
|
|
463
|
+
if self._server:
|
|
464
|
+
self._server.stop()
|
|
465
|
+
self._server = None
|
|
466
|
+
|
|
467
|
+
self._log("Hot reload stopped")
|
|
468
|
+
|
|
469
|
+
def _handle_changes(self, changes: List[FileChange]):
|
|
470
|
+
"""Handle file changes from watcher."""
|
|
471
|
+
if not changes:
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
self._reload_count += 1
|
|
475
|
+
|
|
476
|
+
# Determine reload type
|
|
477
|
+
reload_type = ReloadType.FULL
|
|
478
|
+
css_only = all(c.reload_type == ReloadType.CSS for c in changes)
|
|
479
|
+
if css_only:
|
|
480
|
+
reload_type = ReloadType.CSS
|
|
481
|
+
|
|
482
|
+
# Log changes
|
|
483
|
+
for change in changes:
|
|
484
|
+
rel_path = change.path
|
|
485
|
+
try:
|
|
486
|
+
rel_path = change.path.relative_to(Path.cwd())
|
|
487
|
+
except ValueError:
|
|
488
|
+
pass
|
|
489
|
+
self._log(f"[{datetime.now().strftime('%H:%M:%S')}] {change.change_type}: {rel_path}")
|
|
490
|
+
|
|
491
|
+
# Clear any previous error
|
|
492
|
+
self._last_error = None
|
|
493
|
+
|
|
494
|
+
# Try to parse changed .q files to detect errors
|
|
495
|
+
parse_error = None
|
|
496
|
+
for change in changes:
|
|
497
|
+
if change.extension == '.q' and change.change_type != 'deleted':
|
|
498
|
+
error = self._check_parse_error(change.path)
|
|
499
|
+
if error:
|
|
500
|
+
parse_error = error
|
|
501
|
+
break
|
|
502
|
+
|
|
503
|
+
if parse_error:
|
|
504
|
+
self._last_error = parse_error
|
|
505
|
+
self._broadcast_error(parse_error)
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# Broadcast reload to clients
|
|
509
|
+
self._broadcast_reload(changes, reload_type)
|
|
510
|
+
|
|
511
|
+
# Call optional callback
|
|
512
|
+
if self.on_reload:
|
|
513
|
+
try:
|
|
514
|
+
self.on_reload(changes, reload_type)
|
|
515
|
+
except Exception as e:
|
|
516
|
+
self._log(f"Error in reload callback: {e}")
|
|
517
|
+
|
|
518
|
+
def _check_parse_error(self, path: Path) -> Optional[dict]:
|
|
519
|
+
"""Check if a .q file has parse errors."""
|
|
520
|
+
try:
|
|
521
|
+
import sys
|
|
522
|
+
from quantum.core.parser import QuantumParser, QuantumParseError
|
|
523
|
+
|
|
524
|
+
parser = QuantumParser()
|
|
525
|
+
parser.parse_file(str(path))
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
except QuantumParseError as e:
|
|
529
|
+
return {
|
|
530
|
+
'type': 'parse_error',
|
|
531
|
+
'file': str(path),
|
|
532
|
+
'message': str(e),
|
|
533
|
+
'line': getattr(e, 'line', None),
|
|
534
|
+
'column': getattr(e, 'column', None)
|
|
535
|
+
}
|
|
536
|
+
except Exception as e:
|
|
537
|
+
return {
|
|
538
|
+
'type': 'error',
|
|
539
|
+
'file': str(path),
|
|
540
|
+
'message': str(e)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
def _broadcast_reload(self, changes: List[FileChange], reload_type: ReloadType):
|
|
544
|
+
"""Broadcast reload event to connected browsers."""
|
|
545
|
+
if not self._server:
|
|
546
|
+
return
|
|
547
|
+
|
|
548
|
+
message = {
|
|
549
|
+
'type': 'reload',
|
|
550
|
+
'reloadType': reload_type.value,
|
|
551
|
+
'files': [
|
|
552
|
+
{
|
|
553
|
+
'path': str(c.path),
|
|
554
|
+
'changeType': c.change_type,
|
|
555
|
+
'extension': c.extension
|
|
556
|
+
}
|
|
557
|
+
for c in changes
|
|
558
|
+
],
|
|
559
|
+
'timestamp': time.time(),
|
|
560
|
+
'reloadCount': self._reload_count
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
self._server.broadcast(message)
|
|
564
|
+
self._log(f"Reloading... ({reload_type.value})")
|
|
565
|
+
|
|
566
|
+
def _broadcast_error(self, error: dict):
|
|
567
|
+
"""Broadcast error event to connected browsers."""
|
|
568
|
+
if not self._server:
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
message = {
|
|
572
|
+
'type': 'error',
|
|
573
|
+
'error': error,
|
|
574
|
+
'timestamp': time.time()
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
self._server.broadcast(message)
|
|
578
|
+
self._log(f"Parse error in {error.get('file', 'unknown')}: {error.get('message', 'Unknown error')}")
|
|
579
|
+
|
|
580
|
+
def clear_error(self):
|
|
581
|
+
"""Clear the current error and notify clients."""
|
|
582
|
+
self._last_error = None
|
|
583
|
+
if self._server:
|
|
584
|
+
self._server.broadcast({
|
|
585
|
+
'type': 'clear_error',
|
|
586
|
+
'timestamp': time.time()
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
def force_reload(self):
|
|
590
|
+
"""Force a full page reload on all clients."""
|
|
591
|
+
if self._server:
|
|
592
|
+
self._reload_count += 1
|
|
593
|
+
self._server.broadcast({
|
|
594
|
+
'type': 'reload',
|
|
595
|
+
'reloadType': 'full',
|
|
596
|
+
'files': [],
|
|
597
|
+
'timestamp': time.time(),
|
|
598
|
+
'reloadCount': self._reload_count,
|
|
599
|
+
'forced': True
|
|
600
|
+
})
|
|
601
|
+
self._log("Forced reload")
|
|
602
|
+
|
|
603
|
+
def _log(self, message: str):
|
|
604
|
+
"""Log a message."""
|
|
605
|
+
if self.console:
|
|
606
|
+
self.console.info(message)
|
|
607
|
+
else:
|
|
608
|
+
print(f"[Hot Reload] {message}")
|
|
609
|
+
|
|
610
|
+
@property
|
|
611
|
+
def ws_url(self) -> str:
|
|
612
|
+
"""WebSocket URL for client connection."""
|
|
613
|
+
return f"ws://{self.ws_host}:{self.ws_port}"
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def client_count(self) -> int:
|
|
617
|
+
"""Number of connected browser clients."""
|
|
618
|
+
return self._server.client_count if self._server else 0
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def get_hot_reload_client_script(ws_port: int = 35729, ws_host: str = 'localhost') -> str:
|
|
622
|
+
"""
|
|
623
|
+
Get the hot reload client JavaScript code.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
ws_port: WebSocket server port
|
|
627
|
+
ws_host: WebSocket server host
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
JavaScript code to inject into HTML pages
|
|
631
|
+
"""
|
|
632
|
+
return f"""
|
|
633
|
+
<!-- Quantum Hot Reload Client -->
|
|
634
|
+
<script>
|
|
635
|
+
(function() {{
|
|
636
|
+
'use strict';
|
|
637
|
+
|
|
638
|
+
var WS_URL = 'ws://{ws_host}:{ws_port}';
|
|
639
|
+
var RECONNECT_INTERVAL = 1000;
|
|
640
|
+
var MAX_RECONNECT_ATTEMPTS = 30;
|
|
641
|
+
|
|
642
|
+
var socket = null;
|
|
643
|
+
var reconnectAttempts = 0;
|
|
644
|
+
var overlay = null;
|
|
645
|
+
var isConnected = false;
|
|
646
|
+
|
|
647
|
+
// State preservation
|
|
648
|
+
var preservedState = {{}};
|
|
649
|
+
|
|
650
|
+
function log(msg) {{
|
|
651
|
+
console.log('[Hot Reload] ' + msg);
|
|
652
|
+
}}
|
|
653
|
+
|
|
654
|
+
function connect() {{
|
|
655
|
+
if (socket && socket.readyState === WebSocket.OPEN) {{
|
|
656
|
+
return;
|
|
657
|
+
}}
|
|
658
|
+
|
|
659
|
+
try {{
|
|
660
|
+
socket = new WebSocket(WS_URL);
|
|
661
|
+
|
|
662
|
+
socket.onopen = function() {{
|
|
663
|
+
log('Connected to dev server');
|
|
664
|
+
reconnectAttempts = 0;
|
|
665
|
+
isConnected = true;
|
|
666
|
+
hideOverlay();
|
|
667
|
+
showToast('Hot reload connected', 'success');
|
|
668
|
+
}};
|
|
669
|
+
|
|
670
|
+
socket.onclose = function() {{
|
|
671
|
+
isConnected = false;
|
|
672
|
+
log('Disconnected from dev server');
|
|
673
|
+
scheduleReconnect();
|
|
674
|
+
}};
|
|
675
|
+
|
|
676
|
+
socket.onerror = function(err) {{
|
|
677
|
+
log('WebSocket error');
|
|
678
|
+
}};
|
|
679
|
+
|
|
680
|
+
socket.onmessage = function(event) {{
|
|
681
|
+
try {{
|
|
682
|
+
var data = JSON.parse(event.data);
|
|
683
|
+
handleMessage(data);
|
|
684
|
+
}} catch (e) {{
|
|
685
|
+
log('Error parsing message: ' + e);
|
|
686
|
+
}}
|
|
687
|
+
}};
|
|
688
|
+
|
|
689
|
+
}} catch (e) {{
|
|
690
|
+
log('Failed to connect: ' + e);
|
|
691
|
+
scheduleReconnect();
|
|
692
|
+
}}
|
|
693
|
+
}}
|
|
694
|
+
|
|
695
|
+
function scheduleReconnect() {{
|
|
696
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {{
|
|
697
|
+
log('Max reconnect attempts reached');
|
|
698
|
+
showOverlay({{
|
|
699
|
+
type: 'connection_lost',
|
|
700
|
+
message: 'Lost connection to dev server. Please restart the server.'
|
|
701
|
+
}});
|
|
702
|
+
return;
|
|
703
|
+
}}
|
|
704
|
+
|
|
705
|
+
reconnectAttempts++;
|
|
706
|
+
setTimeout(connect, RECONNECT_INTERVAL);
|
|
707
|
+
}}
|
|
708
|
+
|
|
709
|
+
function handleMessage(data) {{
|
|
710
|
+
switch (data.type) {{
|
|
711
|
+
case 'connected':
|
|
712
|
+
log('Server acknowledged connection');
|
|
713
|
+
break;
|
|
714
|
+
|
|
715
|
+
case 'reload':
|
|
716
|
+
handleReload(data);
|
|
717
|
+
break;
|
|
718
|
+
|
|
719
|
+
case 'error':
|
|
720
|
+
handleError(data.error);
|
|
721
|
+
break;
|
|
722
|
+
|
|
723
|
+
case 'clear_error':
|
|
724
|
+
hideOverlay();
|
|
725
|
+
break;
|
|
726
|
+
|
|
727
|
+
case 'pong':
|
|
728
|
+
// Heartbeat response
|
|
729
|
+
break;
|
|
730
|
+
}}
|
|
731
|
+
}}
|
|
732
|
+
|
|
733
|
+
function handleReload(data) {{
|
|
734
|
+
log('Reload triggered: ' + data.reloadType);
|
|
735
|
+
|
|
736
|
+
if (data.reloadType === 'css') {{
|
|
737
|
+
reloadCSS();
|
|
738
|
+
showToast('Styles updated', 'info');
|
|
739
|
+
}} else {{
|
|
740
|
+
// Preserve state before reload
|
|
741
|
+
preserveState();
|
|
742
|
+
|
|
743
|
+
// Full page reload
|
|
744
|
+
showToast('Reloading...', 'info');
|
|
745
|
+
setTimeout(function() {{
|
|
746
|
+
location.reload();
|
|
747
|
+
}}, 100);
|
|
748
|
+
}}
|
|
749
|
+
}}
|
|
750
|
+
|
|
751
|
+
function handleError(error) {{
|
|
752
|
+
log('Error received: ' + error.message);
|
|
753
|
+
showOverlay(error);
|
|
754
|
+
}}
|
|
755
|
+
|
|
756
|
+
function reloadCSS() {{
|
|
757
|
+
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
758
|
+
var timestamp = Date.now();
|
|
759
|
+
|
|
760
|
+
links.forEach(function(link) {{
|
|
761
|
+
var href = link.href.split('?')[0];
|
|
762
|
+
link.href = href + '?_hr=' + timestamp;
|
|
763
|
+
}});
|
|
764
|
+
|
|
765
|
+
log('CSS reloaded');
|
|
766
|
+
}}
|
|
767
|
+
|
|
768
|
+
function preserveState() {{
|
|
769
|
+
// Save form values
|
|
770
|
+
var forms = document.querySelectorAll('form');
|
|
771
|
+
forms.forEach(function(form, i) {{
|
|
772
|
+
var formId = form.id || 'form_' + i;
|
|
773
|
+
preservedState[formId] = {{}};
|
|
774
|
+
|
|
775
|
+
var inputs = form.querySelectorAll('input, textarea, select');
|
|
776
|
+
inputs.forEach(function(input) {{
|
|
777
|
+
if (input.name) {{
|
|
778
|
+
if (input.type === 'checkbox' || input.type === 'radio') {{
|
|
779
|
+
preservedState[formId][input.name] = input.checked;
|
|
780
|
+
}} else {{
|
|
781
|
+
preservedState[formId][input.name] = input.value;
|
|
782
|
+
}}
|
|
783
|
+
}}
|
|
784
|
+
}});
|
|
785
|
+
}});
|
|
786
|
+
|
|
787
|
+
// Save scroll position
|
|
788
|
+
preservedState._scroll = {{
|
|
789
|
+
x: window.scrollX,
|
|
790
|
+
y: window.scrollY
|
|
791
|
+
}};
|
|
792
|
+
|
|
793
|
+
// Store in sessionStorage for restoration after reload
|
|
794
|
+
try {{
|
|
795
|
+
sessionStorage.setItem('__quantum_hr_state', JSON.stringify(preservedState));
|
|
796
|
+
}} catch (e) {{
|
|
797
|
+
// Storage might be full or unavailable
|
|
798
|
+
}}
|
|
799
|
+
}}
|
|
800
|
+
|
|
801
|
+
function restoreState() {{
|
|
802
|
+
try {{
|
|
803
|
+
var saved = sessionStorage.getItem('__quantum_hr_state');
|
|
804
|
+
if (!saved) return;
|
|
805
|
+
|
|
806
|
+
var state = JSON.parse(saved);
|
|
807
|
+
sessionStorage.removeItem('__quantum_hr_state');
|
|
808
|
+
|
|
809
|
+
// Restore form values
|
|
810
|
+
Object.keys(state).forEach(function(formId) {{
|
|
811
|
+
if (formId.startsWith('_')) return;
|
|
812
|
+
|
|
813
|
+
var form = document.getElementById(formId) || document.forms[formId.replace('form_', '')];
|
|
814
|
+
if (!form) return;
|
|
815
|
+
|
|
816
|
+
var formState = state[formId];
|
|
817
|
+
Object.keys(formState).forEach(function(name) {{
|
|
818
|
+
var input = form.querySelector('[name="' + name + '"]');
|
|
819
|
+
if (input) {{
|
|
820
|
+
if (input.type === 'checkbox' || input.type === 'radio') {{
|
|
821
|
+
input.checked = formState[name];
|
|
822
|
+
}} else {{
|
|
823
|
+
input.value = formState[name];
|
|
824
|
+
}}
|
|
825
|
+
}}
|
|
826
|
+
}});
|
|
827
|
+
}});
|
|
828
|
+
|
|
829
|
+
// Restore scroll position
|
|
830
|
+
if (state._scroll) {{
|
|
831
|
+
setTimeout(function() {{
|
|
832
|
+
window.scrollTo(state._scroll.x, state._scroll.y);
|
|
833
|
+
}}, 100);
|
|
834
|
+
}}
|
|
835
|
+
|
|
836
|
+
log('State restored');
|
|
837
|
+
|
|
838
|
+
}} catch (e) {{
|
|
839
|
+
log('Error restoring state: ' + e);
|
|
840
|
+
}}
|
|
841
|
+
}}
|
|
842
|
+
|
|
843
|
+
function showOverlay(error) {{
|
|
844
|
+
hideOverlay();
|
|
845
|
+
|
|
846
|
+
overlay = document.createElement('div');
|
|
847
|
+
overlay.id = '__quantum_hr_overlay';
|
|
848
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:999999;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,sans-serif;';
|
|
849
|
+
|
|
850
|
+
var content = document.createElement('div');
|
|
851
|
+
content.style.cssText = 'max-width:800px;padding:40px;background:#1e1e1e;border-radius:8px;color:#fff;box-shadow:0 4px 24px rgba(0,0,0,0.5);';
|
|
852
|
+
|
|
853
|
+
var title = error.type === 'connection_lost' ? 'Connection Lost' : 'Parse Error';
|
|
854
|
+
var titleColor = error.type === 'connection_lost' ? '#ffc107' : '#f44336';
|
|
855
|
+
|
|
856
|
+
content.innerHTML = '<h2 style="color:' + titleColor + ';margin:0 0 16px 0;display:flex;align-items:center;gap:8px;">' +
|
|
857
|
+
'<span style="font-size:24px;">' + (error.type === 'connection_lost' ? '⚠' : '✗') + '</span>' +
|
|
858
|
+
title +
|
|
859
|
+
'</h2>' +
|
|
860
|
+
(error.file ? '<p style="color:#888;margin:0 0 12px 0;font-size:14px;">File: ' + error.file + '</p>' : '') +
|
|
861
|
+
'<pre style="background:#2d2d2d;padding:16px;border-radius:4px;overflow:auto;max-height:400px;margin:0;font-size:14px;line-height:1.5;color:#ff6b6b;">' +
|
|
862
|
+
escapeHtml(error.message) +
|
|
863
|
+
'</pre>' +
|
|
864
|
+
'<p style="color:#888;margin:16px 0 0 0;font-size:13px;">Fix the error and save the file to reload automatically.</p>';
|
|
865
|
+
|
|
866
|
+
overlay.appendChild(content);
|
|
867
|
+
document.body.appendChild(overlay);
|
|
868
|
+
|
|
869
|
+
// Click outside to dismiss (but error will reappear on next change)
|
|
870
|
+
overlay.addEventListener('click', function(e) {{
|
|
871
|
+
if (e.target === overlay) {{
|
|
872
|
+
hideOverlay();
|
|
873
|
+
}}
|
|
874
|
+
}});
|
|
875
|
+
}}
|
|
876
|
+
|
|
877
|
+
function hideOverlay() {{
|
|
878
|
+
if (overlay && overlay.parentNode) {{
|
|
879
|
+
overlay.parentNode.removeChild(overlay);
|
|
880
|
+
}}
|
|
881
|
+
overlay = null;
|
|
882
|
+
}}
|
|
883
|
+
|
|
884
|
+
function showToast(message, type) {{
|
|
885
|
+
var existing = document.getElementById('__quantum_hr_toast');
|
|
886
|
+
if (existing) {{
|
|
887
|
+
existing.parentNode.removeChild(existing);
|
|
888
|
+
}}
|
|
889
|
+
|
|
890
|
+
var toast = document.createElement('div');
|
|
891
|
+
toast.id = '__quantum_hr_toast';
|
|
892
|
+
|
|
893
|
+
var bgColor = type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3';
|
|
894
|
+
|
|
895
|
+
toast.style.cssText = 'position:fixed;bottom:20px;right:20px;padding:12px 20px;background:' + bgColor + ';color:#fff;border-radius:4px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:14px;z-index:999998;box-shadow:0 2px 8px rgba(0,0,0,0.2);animation:__qhr_fadein 0.3s ease;';
|
|
896
|
+
toast.textContent = message;
|
|
897
|
+
|
|
898
|
+
// Add animation keyframes if not present
|
|
899
|
+
if (!document.getElementById('__quantum_hr_styles')) {{
|
|
900
|
+
var style = document.createElement('style');
|
|
901
|
+
style.id = '__quantum_hr_styles';
|
|
902
|
+
style.textContent = '@keyframes __qhr_fadein {{ from {{ opacity:0;transform:translateY(10px); }} to {{ opacity:1;transform:translateY(0); }} }}';
|
|
903
|
+
document.head.appendChild(style);
|
|
904
|
+
}}
|
|
905
|
+
|
|
906
|
+
document.body.appendChild(toast);
|
|
907
|
+
|
|
908
|
+
setTimeout(function() {{
|
|
909
|
+
if (toast.parentNode) {{
|
|
910
|
+
toast.style.opacity = '0';
|
|
911
|
+
toast.style.transition = 'opacity 0.3s ease';
|
|
912
|
+
setTimeout(function() {{
|
|
913
|
+
if (toast.parentNode) {{
|
|
914
|
+
toast.parentNode.removeChild(toast);
|
|
915
|
+
}}
|
|
916
|
+
}}, 300);
|
|
917
|
+
}}
|
|
918
|
+
}}, 2000);
|
|
919
|
+
}}
|
|
920
|
+
|
|
921
|
+
function escapeHtml(text) {{
|
|
922
|
+
var div = document.createElement('div');
|
|
923
|
+
div.textContent = text;
|
|
924
|
+
return div.innerHTML;
|
|
925
|
+
}}
|
|
926
|
+
|
|
927
|
+
// Heartbeat to keep connection alive
|
|
928
|
+
setInterval(function() {{
|
|
929
|
+
if (socket && socket.readyState === WebSocket.OPEN) {{
|
|
930
|
+
socket.send(JSON.stringify({{ type: 'ping' }}));
|
|
931
|
+
}}
|
|
932
|
+
}}, 30000);
|
|
933
|
+
|
|
934
|
+
// Initialize
|
|
935
|
+
if (document.readyState === 'loading') {{
|
|
936
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
|
937
|
+
restoreState();
|
|
938
|
+
connect();
|
|
939
|
+
}});
|
|
940
|
+
}} else {{
|
|
941
|
+
restoreState();
|
|
942
|
+
connect();
|
|
943
|
+
}}
|
|
944
|
+
|
|
945
|
+
}})();
|
|
946
|
+
</script>
|
|
947
|
+
"""
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
# Export client script as a constant for easy access
|
|
951
|
+
HOT_RELOAD_CLIENT_SCRIPT = get_hot_reload_client_script()
|