visualifyjs 2.5.3 → 3.0.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.
- package/.claude/mem/TIMELINE.md +36 -0
- package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-external-script-solution.md +24 -0
- package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-session-summary.md +43 -0
- package/.claude/mem/notes/2026-02-11-cli-fix-editor-command-alias.md +26 -0
- package/.claude/mem/notes/2026-02-11-phase-3-developer-experience-completed.md +51 -0
- package/.claude/mem/notes/2026-02-11-phase-4-web-workers-implementation-complete.md +59 -0
- package/.claude/mem/notes/2026-02-11-visualify-phase-2-3d-visualization-complete.md +50 -0
- package/.claude/mem/notes/2026-02-11-visualify-phase-2-committed-ready-for-phase-3.md +33 -0
- package/.claude/mem/notes/2026-02-11-visualify-phase-3-complete-developer-experience.md +52 -0
- package/.claude/mem/notes/2026-02-11-visualify-repository-cleanup-complete.md +28 -0
- package/.claude/mem/notes/2026-02-18-codebase-cleanup-docsify-plugin-documentation.md +37 -0
- package/.claude/mem/notes/2026-02-19-css-grid-layout-fix-displaycontents-on-vcontroller.md +18 -0
- package/.claude/mem/notes/2026-02-19-docsify-plugin-fixes-latex-and-visualify-code-bloc.md +26 -0
- package/.claude/mem/notes/2026-02-19-page-mode-docs-update-decisions.md +23 -0
- package/.claude/mem/notes/2026-02-19-react-context-infinite-re-render-loop-fix-pattern.md +31 -0
- package/.claude/mem/notes/2026-02-19-version-300-bump-and-build-fixes.md +32 -0
- package/.claude/mem/notes/2026-02-19-visualify-build-deployment-architecture-bug-fixes.md +25 -0
- package/.claude/mem/notes/2026-02-19-visualify-dist-iife-self-contained-build-config.md +30 -0
- package/.claude/mem/notes/2026-02-19-visualify-infinite-loop-i18n-fixes.md +31 -0
- package/.claude/mem/notes/2026-02-19-visualify-v3-bundle-splitting-docs-restructuring.md +32 -0
- package/.claude/mem/notes/2026-02-20-bundle-externalization-final-architecture.md +29 -0
- package/.claude/mem/notes/2026-02-20-chromium-page-fix-unstable-keys-and-double-event-b.md +27 -0
- package/.claude/mem/notes/2026-02-20-console-cleanup-bundle-optimization-commit.md +20 -0
- package/.claude/mem/notes/2026-02-20-dotbio-dot-plot-fix-useeffect-dependency.md +21 -0
- package/.claude/mem/notes/2026-02-20-public-folder-cleanup-and-readme-rewrite.md +25 -0
- package/.claude/mem/notes/2026-02-20-v300-release-and-beta-channel-strategy.md +29 -0
- package/.claude/mem/notes/2026-02-20-visium-background-image-unknown-legend-fix.md +19 -0
- package/.claude/mem/notes/2026-02-20-visualify-cdn-loader-bundle-externalization.md +34 -0
- package/.claude/mem/sessions/session-2026-02-20-031524.md +54 -0
- package/.claude/settings.local.json +21 -0
- package/.github/workflows/static.yml.bak +51 -51
- package/.sisyphus/boulder.json +65 -0
- package/.sisyphus/plans/phase-4-advanced-optimizations.md +217 -0
- package/LICENSE +674 -674
- package/README.md +94 -59
- package/config-overrides.js +31 -31
- package/dist/stats.html +4949 -0
- package/dist/visualify-3d.esm.js +1 -0
- package/dist/visualify-3d.js +1 -0
- package/dist/visualify-core.esm.js +1 -0
- package/dist/visualify-core.js +1 -0
- package/dist/visualify-docs.esm.js +1 -0
- package/dist/visualify-docs.js +1 -0
- package/dist/visualify-loader.js +1 -0
- package/dist/visualify-pages.esm.js +1 -0
- package/dist/visualify-pages.js +1 -0
- package/dist/visualify-portal.esm.js +1 -0
- package/dist/visualify-portal.js +1 -0
- package/dist/visualify-shared.js +26571 -0
- package/dist/visualify.js +1 -188
- package/docs/CHANGELOG.md +148 -0
- package/docs/cli/commands.md +513 -0
- package/docs/configuration/visualify-json.md +474 -0
- package/docs/docs/3d-visualization.md +374 -0
- package/docs/docs/CLI.md +303 -34
- package/docs/docs/README.md +65 -65
- package/docs/docs/Rechart/bar.md +190 -190
- package/docs/docs/Rechart/funnel.md +241 -193
- package/docs/docs/Rechart/line.md +355 -355
- package/docs/docs/Rechart/pie.md +225 -225
- package/docs/docs/Rechart/radar.md +253 -253
- package/docs/docs/Rechart/scatter.md +298 -0
- package/docs/docs/_404.md +51 -51
- package/docs/docs/_coverpage.md +11 -11
- package/docs/docs/_sidebar.md +54 -43
- package/docs/docs/components/dotBio.md +87 -34
- package/docs/docs/components/echart.md +171 -82
- package/docs/docs/components/html.md +61 -34
- package/docs/docs/components/macaron.md +156 -145
- package/docs/docs/components/markdown.md +42 -0
- package/docs/docs/components/more.md +183 -142
- package/docs/docs/components/plotly.md +132 -62
- package/docs/docs/components/scatterL.md +171 -70
- package/docs/docs/components/visium.md +112 -57
- package/docs/docs/configuration.md +121 -123
- package/docs/docs/deploy.md +31 -31
- package/docs/docs/docsify-plugin.md +655 -0
- package/docs/docs/hmr.md +165 -0
- package/docs/docs/i18n.md +332 -0
- package/docs/docs/log.md +30 -1
- package/docs/docs/more-pages.md +23 -23
- package/docs/docs/quickstart.md +148 -119
- package/docs/docs/rechart-attributes.md +74 -74
- package/docs/docs/rechart-basic-usage.md +160 -162
- package/docs/docs/theme.md +5 -5
- package/docs/docs/typescript.md +306 -0
- package/docs/docs/visual-editor.md +359 -0
- package/docs/index.html +85 -71
- package/docs/manifest.json +23 -23
- package/docs/migration/v3-migration.md +392 -0
- package/docs/static/css/fluff-stuff.css +169 -169
- package/docs/static/css/font-awesome.min.css +4 -4
- package/docs/static/css/visualify.css +6 -25
- package/docs/static/js/3d-viz-examples.js +181 -0
- package/docs/static/js/configuration.js +630 -448
- package/docs/static/js/visualify.js +1 -188
- package/package.json +106 -84
- package/rollup.config.mjs +766 -76
- package/src/_css/404.css +115 -115
- package/src/_css/App.css +37 -37
- package/src/_css/autoSuggestion.css +26 -26
- package/src/_css/circular-progress.css +32 -32
- package/src/_css/index.css +36 -36
- package/src/_css/modern.css +350 -25
- package/src/_media/corner.svg +8 -8
- package/src/_media/download.svg +3 -3
- package/src/_media/logo.svg +14 -14
- package/src/_test/App.test.js +15 -15
- package/src/_utils/reportWebVitals.js +13 -13
- package/src/a11y/README.md +177 -0
- package/src/a11y/aria-labels.js +339 -0
- package/src/a11y/color-contrast.js +535 -0
- package/src/a11y/index.js +197 -0
- package/src/a11y/keyboard-nav.js +523 -0
- package/src/a11y/styles.css +165 -0
- package/src/cli/commands/dev.js +214 -0
- package/src/cli/commands/docs.js +521 -0
- package/src/cli/commands/edit.js +379 -0
- package/src/cli/commands/init.js +213 -0
- package/src/cli/commands/portal.js +236 -0
- package/src/cli/dev-server.js +530 -0
- package/src/cli/hmr.js +456 -0
- package/src/cli/index.js +180 -0
- package/src/cli/utils/config.js +207 -0
- package/src/cli/utils/logger.js +241 -0
- package/src/config/defaults.ts +122 -0
- package/src/config/index.ts +72 -0
- package/src/config/loader.ts +478 -0
- package/src/config/schema.ts +227 -0
- package/src/config/validator.ts +337 -0
- package/src/core/appContext.js +34 -27
- package/src/core/components/Bar.js +383 -0
- package/src/core/components/Bar3D.js +473 -0
- package/src/core/components/LargeDatasetChart.js +296 -0
- package/src/core/components/Line3D.js +310 -0
- package/src/core/components/Scatter.js +392 -188
- package/src/core/components/Scatter3D.js +455 -0
- package/src/core/components/ScatterBio.js +601 -572
- package/src/core/components/Surface3D.js +326 -0
- package/src/core/components/ThreeCustom.js +648 -0
- package/src/core/components/ThreeScene.js +459 -0
- package/src/core/components/VisiumPlot.js +191 -165
- package/src/core/components/browser.js +42 -42
- package/src/core/components/dotplot.js +413 -413
- package/src/core/components/html.js +29 -29
- package/src/core/components/list.js +178 -178
- package/src/core/components/macaron.js +206 -201
- package/src/core/components/markdown.js +56 -56
- package/src/core/components/parser.scatterBio.js +582 -579
- package/src/core/components/ratio.js +82 -80
- package/src/core/components/scatterL.js +206 -173
- package/src/core/components/searchbar.js +156 -131
- package/src/core/components/selection.js +310 -193
- package/src/core/components/timeline.js +236 -281
- package/src/core/components/visium.js +114 -97
- package/src/core/data-processor.js +413 -0
- package/src/core/fetch/condfetch.js +82 -82
- package/src/core/fetch/fetch.js +92 -92
- package/src/core/fetch/json.js +29 -29
- package/src/core/fetch/vfetch.js +42 -42
- package/src/core/hmr-client.js +724 -0
- package/src/core/liveEditor.js +44 -44
- package/src/core/modules/codeEditorWithPreview.js +104 -104
- package/src/core/modules/echarts/common.js +20 -20
- package/src/core/modules/echarts/gl.js +228 -0
- package/src/core/modules/echarts/presetHandler.js +41 -41
- package/src/core/modules/echarts/presets/esodev.chromium.js +172 -172
- package/src/core/modules/echarts/presets/esodev.codex.js +130 -130
- package/src/core/modules/echarts/presets/esodev.visium.js +123 -123
- package/src/core/modules/echarts/presets/mmtrbc.js +186 -186
- package/src/core/modules/echarts.js +70 -71
- package/src/core/modules/echartsUtils.js +43 -43
- package/src/core/modules/echartswitcher.js +227 -152
- package/src/core/modules/replotly/presetHandler.js +24 -24
- package/src/core/modules/replotly/presets/minimum.js +18 -18
- package/src/core/modules/replotly/presets/mmtrbc.dot.js +114 -114
- package/src/core/modules/replotly/presets/mmtrbc.violin.js +100 -100
- package/src/core/modules/replotly.js +74 -71
- package/src/core/modules/threejs/Camera.js +373 -0
- package/src/core/modules/threejs/Lighting.js +459 -0
- package/src/core/modules/threejs/Renderer.js +364 -0
- package/src/core/modules/threejs/Scene.js +266 -0
- package/src/core/modules/threejs/index.js +155 -0
- package/src/core/pages/404.js +50 -50
- package/src/core/pages/error.js +27 -27
- package/src/core/pages/jsonPage.js +62 -62
- package/src/core/pages/loading.js +44 -44
- package/src/core/parser/echart.data.js +204 -183
- package/src/core/parser/echart.features.js +125 -125
- package/src/core/parser/echart.general.js +147 -143
- package/src/core/parser/echart.hilbert.js +57 -57
- package/src/core/parser/echart.parser.js +210 -210
- package/src/core/parser/echart.series.js +67 -67
- package/src/core/parser/echart.types.js +76 -76
- package/src/core/parser/plotly.config.js +10 -10
- package/src/core/parser/plotly.data.js +132 -132
- package/src/core/parser/plotly.layout.js +9 -9
- package/src/core/parser/plotly.violin.js +18 -18
- package/src/core/recharts.js +361 -62
- package/src/core/router/alias.js +49 -49
- package/src/core/router/jsonRouter.js +31 -31
- package/src/core/themes/modern.js +32 -32
- package/src/core/themes/themeSelector.js +33 -33
- package/src/core/visualify.js +213 -47
- package/src/core/widgets/circularProgress.js +23 -23
- package/src/core/widgets/controller.js +116 -83
- package/src/core/widgets/errorBoundary.js +36 -36
- package/src/core/widgets/footer.js +185 -177
- package/src/core/widgets/header.js +238 -234
- package/src/core/widgets/layout/Grid.js +31 -31
- package/src/core/widgets/layout.js +36 -36
- package/src/core/widgets/mapping.js +56 -42
- package/src/core/workers/data-worker.js +349 -0
- package/src/core/workers/worker-pool.js +396 -0
- package/src/docsify/bundle.js +215 -0
- package/src/docsify/markdown.js +271 -0
- package/src/docsify/plugin.js +268 -0
- package/src/editor/README.md +172 -0
- package/src/editor/components/ChartBuilder.jsx +341 -0
- package/src/editor/components/ChartTypeSidebar.jsx +91 -0
- package/src/editor/components/Editor.jsx +367 -0
- package/src/editor/components/Preview.jsx +446 -0
- package/src/editor/components/PropertyPanel.jsx +468 -0
- package/src/editor/components/StatusBar.jsx +85 -0
- package/src/editor/context/EditorContext.js +248 -0
- package/src/editor/hooks/useDebounce.js +32 -0
- package/src/editor/index.js +315 -0
- package/src/editor/styles/editor.css +637 -0
- package/src/editor/utils/chartValidator.js +263 -0
- package/src/entries/charts3d.js +70 -0
- package/src/entries/core.js +78 -0
- package/src/entries/docs.js +154 -0
- package/src/entries/pages.js +93 -0
- package/src/entries/portal.js +204 -0
- package/src/entries/shared.js +50 -0
- package/src/i18n/formatters.js +455 -0
- package/src/i18n/index.js +169 -0
- package/src/i18n/locales/ar.json +137 -0
- package/src/i18n/locales/de.json +137 -0
- package/src/i18n/locales/en.json +137 -0
- package/src/i18n/locales/es.json +137 -0
- package/src/i18n/locales/he.json +137 -0
- package/src/i18n/locales/zh.json +137 -0
- package/src/i18n/rtl.css +183 -0
- package/src/index.js +82 -62
- package/src/loader.js +103 -0
- package/src/setupTests.js +5 -5
- package/tsconfig.json +51 -0
- package/types/charts.d.ts +569 -0
- package/types/components.d.ts +441 -0
- package/types/config.d.ts +199 -0
- package/types/index.d.ts +353 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client-side Hot Module Replacement (HMR) handler for Visualify
|
|
3
|
+
* @module core/hmr-client
|
|
4
|
+
*
|
|
5
|
+
* Provides WebSocket client functionality for HMR, chart update logic,
|
|
6
|
+
* state preservation, and error display capabilities.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default HMR client configuration
|
|
13
|
+
* @readonly
|
|
14
|
+
* @type {Object}
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_CONFIG = {
|
|
17
|
+
wsUrl: 'ws://localhost:3000/__hmr',
|
|
18
|
+
reconnectDelay: 1000,
|
|
19
|
+
maxReconnectAttempts: 10,
|
|
20
|
+
heartbeatInterval: 30000,
|
|
21
|
+
debug: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* HMR Client class - manages WebSocket connection and updates
|
|
26
|
+
*/
|
|
27
|
+
class HMRClient {
|
|
28
|
+
/**
|
|
29
|
+
* Create an HMR client instance
|
|
30
|
+
* @param {Object} options - Configuration options
|
|
31
|
+
*/
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
34
|
+
this.ws = null;
|
|
35
|
+
this.reconnectAttempts = 0;
|
|
36
|
+
this.reconnectTimer = null;
|
|
37
|
+
this.heartbeatTimer = null;
|
|
38
|
+
this.isConnected = false;
|
|
39
|
+
this.updateHandlers = new Map();
|
|
40
|
+
this.errorHandlers = new Set();
|
|
41
|
+
this.statePreservers = new Map();
|
|
42
|
+
this.errorOverlay = null;
|
|
43
|
+
|
|
44
|
+
// Bound methods
|
|
45
|
+
this.handleOpen = this.handleOpen.bind(this);
|
|
46
|
+
this.handleMessage = this.handleMessage.bind(this);
|
|
47
|
+
this.handleClose = this.handleClose.bind(this);
|
|
48
|
+
this.handleError = this.handleError.bind(this);
|
|
49
|
+
this.reconnect = this.reconnect.bind(this);
|
|
50
|
+
this.sendHeartbeat = this.sendHeartbeat.bind(this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the HMR client and connect to WebSocket
|
|
55
|
+
* @returns {void}
|
|
56
|
+
*/
|
|
57
|
+
init() {
|
|
58
|
+
if (this.ws) {
|
|
59
|
+
this.log('HMR client already initialized');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.log('Initializing HMR client...');
|
|
64
|
+
this.connect();
|
|
65
|
+
this.createErrorOverlay();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Connect to the WebSocket server
|
|
70
|
+
* @private
|
|
71
|
+
* @returns {void}
|
|
72
|
+
*/
|
|
73
|
+
connect() {
|
|
74
|
+
try {
|
|
75
|
+
this.log(`Connecting to ${this.config.wsUrl}...`);
|
|
76
|
+
this.ws = new WebSocket(this.config.wsUrl);
|
|
77
|
+
|
|
78
|
+
this.ws.addEventListener('open', this.handleOpen);
|
|
79
|
+
this.ws.addEventListener('message', this.handleMessage);
|
|
80
|
+
this.ws.addEventListener('close', this.handleClose);
|
|
81
|
+
this.ws.addEventListener('error', this.handleError);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this.error('Failed to create WebSocket:', err);
|
|
84
|
+
this.scheduleReconnect();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Disconnect from the WebSocket server
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
disconnect() {
|
|
93
|
+
this.log('Disconnecting HMR client...');
|
|
94
|
+
|
|
95
|
+
// Clear timers
|
|
96
|
+
if (this.reconnectTimer) {
|
|
97
|
+
clearTimeout(this.reconnectTimer);
|
|
98
|
+
this.reconnectTimer = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this.heartbeatTimer) {
|
|
102
|
+
clearInterval(this.heartbeatTimer);
|
|
103
|
+
this.heartbeatTimer = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Close WebSocket
|
|
107
|
+
if (this.ws) {
|
|
108
|
+
this.ws.removeEventListener('open', this.handleOpen);
|
|
109
|
+
this.ws.removeEventListener('message', this.handleMessage);
|
|
110
|
+
this.ws.removeEventListener('close', this.handleClose);
|
|
111
|
+
this.ws.removeEventListener('error', this.handleError);
|
|
112
|
+
|
|
113
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
114
|
+
this.ws.close();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.ws = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.isConnected = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle WebSocket open event
|
|
125
|
+
* @private
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
handleOpen() {
|
|
129
|
+
this.log('Connected to HMR server');
|
|
130
|
+
this.isConnected = true;
|
|
131
|
+
this.reconnectAttempts = 0;
|
|
132
|
+
|
|
133
|
+
// Start heartbeat
|
|
134
|
+
this.heartbeatTimer = setInterval(
|
|
135
|
+
this.sendHeartbeat,
|
|
136
|
+
this.config.heartbeatInterval,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Clear any error overlay on successful connection
|
|
140
|
+
this.clearErrorOverlay();
|
|
141
|
+
|
|
142
|
+
// Send ready message
|
|
143
|
+
this.send({ type: 'ready' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handle WebSocket message event
|
|
148
|
+
* @private
|
|
149
|
+
* @param {MessageEvent} event - The message event
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
handleMessage(event) {
|
|
153
|
+
try {
|
|
154
|
+
const message = JSON.parse(event.data);
|
|
155
|
+
this.log('Received message:', message.type);
|
|
156
|
+
|
|
157
|
+
switch (message.type) {
|
|
158
|
+
case 'connected':
|
|
159
|
+
this.log('HMR connection established');
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'update':
|
|
163
|
+
this.handleUpdate(message);
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'error':
|
|
167
|
+
this.handleServerError(message);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'ping':
|
|
171
|
+
this.send({ type: 'pong' });
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
default:
|
|
175
|
+
this.log('Unknown message type:', message.type);
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
this.error('Failed to parse message:', err);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle WebSocket close event
|
|
184
|
+
* @private
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
handleClose() {
|
|
188
|
+
this.log('WebSocket connection closed');
|
|
189
|
+
this.isConnected = false;
|
|
190
|
+
|
|
191
|
+
if (this.heartbeatTimer) {
|
|
192
|
+
clearInterval(this.heartbeatTimer);
|
|
193
|
+
this.heartbeatTimer = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Attempt to reconnect
|
|
197
|
+
this.scheduleReconnect();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle WebSocket error event
|
|
202
|
+
* @private
|
|
203
|
+
* @param {Event} event - The error event
|
|
204
|
+
* @returns {void}
|
|
205
|
+
*/
|
|
206
|
+
handleError(event) {
|
|
207
|
+
this.error('WebSocket error:', event);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Schedule a reconnection attempt
|
|
212
|
+
* @private
|
|
213
|
+
* @returns {void}
|
|
214
|
+
*/
|
|
215
|
+
scheduleReconnect() {
|
|
216
|
+
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
217
|
+
this.error('Max reconnection attempts reached');
|
|
218
|
+
this.showErrorOverlay({
|
|
219
|
+
message: 'Lost connection to HMR server. Please refresh the page.',
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.reconnectAttempts++;
|
|
225
|
+
const delay = this.config.reconnectDelay * Math.min(this.reconnectAttempts, 5);
|
|
226
|
+
|
|
227
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})...`);
|
|
228
|
+
|
|
229
|
+
this.reconnectTimer = setTimeout(() => {
|
|
230
|
+
this.reconnect();
|
|
231
|
+
}, delay);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Attempt to reconnect to the server
|
|
236
|
+
* @private
|
|
237
|
+
* @returns {void}
|
|
238
|
+
*/
|
|
239
|
+
reconnect() {
|
|
240
|
+
if (this.isConnected) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.disconnect();
|
|
245
|
+
this.connect();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Send a heartbeat ping to keep connection alive
|
|
250
|
+
* @private
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
sendHeartbeat() {
|
|
254
|
+
if (this.isConnected) {
|
|
255
|
+
this.send({ type: 'ping' });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Send a message to the server
|
|
261
|
+
* @param {Object} message - The message to send
|
|
262
|
+
* @returns {void}
|
|
263
|
+
*/
|
|
264
|
+
send(message) {
|
|
265
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
266
|
+
try {
|
|
267
|
+
this.ws.send(JSON.stringify(message));
|
|
268
|
+
} catch (err) {
|
|
269
|
+
this.error('Failed to send message:', err);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Handle update message from server
|
|
276
|
+
* @private
|
|
277
|
+
* @param {Object} message - The update message
|
|
278
|
+
* @returns {void}
|
|
279
|
+
*/
|
|
280
|
+
handleUpdate(message) {
|
|
281
|
+
const { configType, file, config, changeType } = message;
|
|
282
|
+
|
|
283
|
+
this.log(`Processing ${configType} update:`, file);
|
|
284
|
+
|
|
285
|
+
// Clear error overlay on successful update
|
|
286
|
+
this.clearErrorOverlay();
|
|
287
|
+
|
|
288
|
+
// Get state preserver for this config type
|
|
289
|
+
const preserveState = this.statePreservers.get(configType);
|
|
290
|
+
let preservedState = null;
|
|
291
|
+
|
|
292
|
+
if (preserveState && changeType !== 'removed') {
|
|
293
|
+
preservedState = preserveState();
|
|
294
|
+
this.log('State preserved for update');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Call registered update handlers
|
|
298
|
+
const handlers = this.updateHandlers.get(configType);
|
|
299
|
+
if (handlers) {
|
|
300
|
+
handlers.forEach((handler) => {
|
|
301
|
+
try {
|
|
302
|
+
handler({
|
|
303
|
+
file,
|
|
304
|
+
config,
|
|
305
|
+
changeType,
|
|
306
|
+
preservedState,
|
|
307
|
+
});
|
|
308
|
+
} catch (err) {
|
|
309
|
+
this.error('Update handler failed:', err);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Notify general update handlers
|
|
315
|
+
const generalHandlers = this.updateHandlers.get('*');
|
|
316
|
+
if (generalHandlers) {
|
|
317
|
+
generalHandlers.forEach((handler) => {
|
|
318
|
+
try {
|
|
319
|
+
handler({
|
|
320
|
+
type: configType,
|
|
321
|
+
file,
|
|
322
|
+
config,
|
|
323
|
+
changeType,
|
|
324
|
+
preservedState,
|
|
325
|
+
});
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.error('General update handler failed:', err);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Handle error message from server
|
|
335
|
+
* @private
|
|
336
|
+
* @param {Object} message - The error message
|
|
337
|
+
* @returns {void}
|
|
338
|
+
*/
|
|
339
|
+
handleServerError(message) {
|
|
340
|
+
this.error('Server error:', message.message);
|
|
341
|
+
|
|
342
|
+
// Show error overlay
|
|
343
|
+
this.showErrorOverlay({
|
|
344
|
+
file: message.file,
|
|
345
|
+
message: message.message,
|
|
346
|
+
details: message.details,
|
|
347
|
+
isSyntaxError: message.isSyntaxError,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Notify error handlers
|
|
351
|
+
this.errorHandlers.forEach((handler) => {
|
|
352
|
+
try {
|
|
353
|
+
handler(message);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
this.error('Error handler failed:', err);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create the error overlay element
|
|
362
|
+
* @private
|
|
363
|
+
* @returns {void}
|
|
364
|
+
*/
|
|
365
|
+
createErrorOverlay() {
|
|
366
|
+
if (typeof document === 'undefined') return;
|
|
367
|
+
|
|
368
|
+
// Remove existing overlay if any
|
|
369
|
+
const existing = document.getElementById('visualify-hmr-error');
|
|
370
|
+
if (existing) {
|
|
371
|
+
existing.remove();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const overlay = document.createElement('div');
|
|
375
|
+
overlay.id = 'visualify-hmr-error';
|
|
376
|
+
overlay.style.cssText = `
|
|
377
|
+
position: fixed;
|
|
378
|
+
top: 0;
|
|
379
|
+
left: 0;
|
|
380
|
+
right: 0;
|
|
381
|
+
bottom: 0;
|
|
382
|
+
background: rgba(0, 0, 0, 0.85);
|
|
383
|
+
z-index: 99999;
|
|
384
|
+
display: none;
|
|
385
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
386
|
+
padding: 20px;
|
|
387
|
+
overflow: auto;
|
|
388
|
+
`;
|
|
389
|
+
|
|
390
|
+
const content = document.createElement('div');
|
|
391
|
+
content.style.cssText = `
|
|
392
|
+
background: #1e1e1e;
|
|
393
|
+
border-radius: 8px;
|
|
394
|
+
max-width: 800px;
|
|
395
|
+
margin: 40px auto;
|
|
396
|
+
padding: 24px;
|
|
397
|
+
color: #fff;
|
|
398
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
399
|
+
`;
|
|
400
|
+
|
|
401
|
+
content.innerHTML = `
|
|
402
|
+
<div style="display: flex; align-items: center; margin-bottom: 16px;">
|
|
403
|
+
<span style="
|
|
404
|
+
background: #ff5555;
|
|
405
|
+
color: white;
|
|
406
|
+
padding: 4px 12px;
|
|
407
|
+
border-radius: 4px;
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
font-weight: bold;
|
|
410
|
+
margin-right: 12px;
|
|
411
|
+
">ERROR</span>
|
|
412
|
+
<h2 style="margin: 0; font-size: 18px;">Hot Module Replacement Failed</h2>
|
|
413
|
+
</div>
|
|
414
|
+
<div id="visualify-hmr-error-message" style="
|
|
415
|
+
background: #2d2d2d;
|
|
416
|
+
padding: 16px;
|
|
417
|
+
border-radius: 4px;
|
|
418
|
+
font-family: monospace;
|
|
419
|
+
font-size: 14px;
|
|
420
|
+
margin-bottom: 16px;
|
|
421
|
+
white-space: pre-wrap;
|
|
422
|
+
word-break: break-word;
|
|
423
|
+
"></div>
|
|
424
|
+
<div id="visualify-hmr-error-file" style="
|
|
425
|
+
color: #888;
|
|
426
|
+
font-size: 12px;
|
|
427
|
+
margin-bottom: 16px;
|
|
428
|
+
"></div>
|
|
429
|
+
<button onclick="window.__VISUALIFY_HMR_CLIENT__.clearErrorOverlay()" style="
|
|
430
|
+
background: #4a9eff;
|
|
431
|
+
color: white;
|
|
432
|
+
border: none;
|
|
433
|
+
padding: 8px 16px;
|
|
434
|
+
border-radius: 4px;
|
|
435
|
+
cursor: pointer;
|
|
436
|
+
font-size: 14px;
|
|
437
|
+
">Dismiss</button>
|
|
438
|
+
`;
|
|
439
|
+
|
|
440
|
+
overlay.appendChild(content);
|
|
441
|
+
document.body.appendChild(overlay);
|
|
442
|
+
|
|
443
|
+
this.errorOverlay = overlay;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Show the error overlay
|
|
448
|
+
* @private
|
|
449
|
+
* @param {Object} error - The error details
|
|
450
|
+
* @returns {void}
|
|
451
|
+
*/
|
|
452
|
+
showErrorOverlay(error) {
|
|
453
|
+
if (!this.errorOverlay) return;
|
|
454
|
+
|
|
455
|
+
const messageEl = document.getElementById('visualify-hmr-error-message');
|
|
456
|
+
const fileEl = document.getElementById('visualify-hmr-error-file');
|
|
457
|
+
|
|
458
|
+
if (messageEl) {
|
|
459
|
+
messageEl.textContent = error.message || 'Unknown error';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (fileEl && error.file) {
|
|
463
|
+
fileEl.textContent = `File: ${error.file}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.errorOverlay.style.display = 'block';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Clear the error overlay
|
|
471
|
+
* @returns {void}
|
|
472
|
+
*/
|
|
473
|
+
clearErrorOverlay() {
|
|
474
|
+
if (this.errorOverlay) {
|
|
475
|
+
this.errorOverlay.style.display = 'none';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Register an update handler
|
|
481
|
+
* @param {string} configType - The config type to handle (e.g., 'main', 'component', '*')
|
|
482
|
+
* @param {Function} handler - The handler function
|
|
483
|
+
* @returns {Function} Unregister function
|
|
484
|
+
*/
|
|
485
|
+
onUpdate(configType, handler) {
|
|
486
|
+
if (!this.updateHandlers.has(configType)) {
|
|
487
|
+
this.updateHandlers.set(configType, new Set());
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.updateHandlers.get(configType).add(handler);
|
|
491
|
+
|
|
492
|
+
// Return unregister function
|
|
493
|
+
return () => {
|
|
494
|
+
this.updateHandlers.get(configType)?.delete(handler);
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Register an error handler
|
|
500
|
+
* @param {Function} handler - The error handler function
|
|
501
|
+
* @returns {Function} Unregister function
|
|
502
|
+
*/
|
|
503
|
+
onError(handler) {
|
|
504
|
+
this.errorHandlers.add(handler);
|
|
505
|
+
|
|
506
|
+
return () => {
|
|
507
|
+
this.errorHandlers.delete(handler);
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Register a state preserver function
|
|
513
|
+
* @param {string} configType - The config type
|
|
514
|
+
* @param {Function} preserver - Function that returns state to preserve
|
|
515
|
+
* @returns {Function} Unregister function
|
|
516
|
+
*/
|
|
517
|
+
registerStatePreserver(configType, preserver) {
|
|
518
|
+
this.statePreservers.set(configType, preserver);
|
|
519
|
+
|
|
520
|
+
return () => {
|
|
521
|
+
this.statePreservers.delete(configType);
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Log a message (if debug is enabled)
|
|
527
|
+
* @private
|
|
528
|
+
* @param {...any} args - Messages to log
|
|
529
|
+
* @returns {void}
|
|
530
|
+
*/
|
|
531
|
+
log(...args) {
|
|
532
|
+
if (this.config.debug) {
|
|
533
|
+
console.log('[HMR]', ...args);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Log an error
|
|
539
|
+
* @private
|
|
540
|
+
* @param {...any} args - Error messages
|
|
541
|
+
* @returns {void}
|
|
542
|
+
*/
|
|
543
|
+
error(...args) {
|
|
544
|
+
console.error('[HMR]', ...args);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Global HMR client instance
|
|
549
|
+
let globalClient = null;
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Initialize the global HMR client
|
|
553
|
+
* @param {Object} options - Configuration options
|
|
554
|
+
* @returns {HMRClient} The HMR client instance
|
|
555
|
+
*/
|
|
556
|
+
export function initHMR(options = {}) {
|
|
557
|
+
if (typeof window === 'undefined') {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
if (window.__VISUALIFY_HMR__?.enabled !== true) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!globalClient) {
|
|
565
|
+
// Try to get URL from global config
|
|
566
|
+
const wsUrl = window.__VISUALIFY_HMR__?.wsUrl || DEFAULT_CONFIG.wsUrl;
|
|
567
|
+
|
|
568
|
+
globalClient = new HMRClient({
|
|
569
|
+
...options,
|
|
570
|
+
wsUrl,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
globalClient.init();
|
|
574
|
+
|
|
575
|
+
// Store globally for access
|
|
576
|
+
window.__VISUALIFY_HMR_CLIENT__ = globalClient;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return globalClient;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Get the global HMR client instance
|
|
584
|
+
* @returns {HMRClient|null} The HMR client instance
|
|
585
|
+
*/
|
|
586
|
+
export function getHMRClient() {
|
|
587
|
+
return globalClient || window?.__VISUALIFY_HMR_CLIENT__ || null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* React hook for HMR integration
|
|
592
|
+
* @param {Object} options - Hook options
|
|
593
|
+
* @param {string} options.configType - The config type to listen for
|
|
594
|
+
* @param {Function} options.onUpdate - Callback when update is received
|
|
595
|
+
* @param {Function} options.onError - Callback when error is received
|
|
596
|
+
* @param {Function} options.preserveState - Function to preserve state
|
|
597
|
+
* @returns {Object} HMR status and utilities
|
|
598
|
+
*/
|
|
599
|
+
export function useHMR(options = {}) {
|
|
600
|
+
const { configType, onUpdate, onError, preserveState } = options;
|
|
601
|
+
const clientRef = useRef(null);
|
|
602
|
+
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
if (typeof window === 'undefined') return;
|
|
605
|
+
if (window.__VISUALIFY_HMR__?.enabled !== true) return;
|
|
606
|
+
|
|
607
|
+
// Initialize HMR client
|
|
608
|
+
const client = initHMR();
|
|
609
|
+
clientRef.current = client;
|
|
610
|
+
|
|
611
|
+
if (!client) return;
|
|
612
|
+
|
|
613
|
+
const unsubscribers = [];
|
|
614
|
+
|
|
615
|
+
// Register update handler
|
|
616
|
+
if (onUpdate && configType) {
|
|
617
|
+
const unsubscribe = client.onUpdate(configType, onUpdate);
|
|
618
|
+
unsubscribers.push(unsubscribe);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Register error handler
|
|
622
|
+
if (onError) {
|
|
623
|
+
const unsubscribe = client.onError(onError);
|
|
624
|
+
unsubscribers.push(unsubscribe);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Register state preserver
|
|
628
|
+
if (preserveState && configType) {
|
|
629
|
+
const unsubscribe = client.registerStatePreserver(configType, preserveState);
|
|
630
|
+
unsubscribers.push(unsubscribe);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Cleanup
|
|
634
|
+
return () => {
|
|
635
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
636
|
+
};
|
|
637
|
+
}, [configType, onUpdate, onError, preserveState]);
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
isConnected: clientRef.current?.isConnected || false,
|
|
641
|
+
client: clientRef.current,
|
|
642
|
+
clearError: () => clientRef.current?.clearErrorOverlay(),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Preserve ECharts instance state
|
|
648
|
+
* @param {Object} chartInstance - The ECharts instance
|
|
649
|
+
* @returns {Object} Preserved state
|
|
650
|
+
*/
|
|
651
|
+
export function preserveChartState(chartInstance) {
|
|
652
|
+
if (!chartInstance) return null;
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
const option = chartInstance.getOption();
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
// Preserve zoom state for dataZoom components
|
|
659
|
+
dataZoom: option.dataZoom?.map((dz) => ({
|
|
660
|
+
start: dz.start,
|
|
661
|
+
end: dz.end,
|
|
662
|
+
startValue: dz.startValue,
|
|
663
|
+
endValue: dz.endValue,
|
|
664
|
+
})),
|
|
665
|
+
|
|
666
|
+
// Preserve legend selection
|
|
667
|
+
legend: option.legend?.[0]?.selected,
|
|
668
|
+
|
|
669
|
+
// Preserve tooltip state
|
|
670
|
+
tooltip: option.tooltip?.[0],
|
|
671
|
+
|
|
672
|
+
// Preserve brush selection if any
|
|
673
|
+
brush: option.brush,
|
|
674
|
+
|
|
675
|
+
// Preserve current data view
|
|
676
|
+
series: option.series?.map((s) => ({
|
|
677
|
+
name: s.name,
|
|
678
|
+
data: s.data,
|
|
679
|
+
})),
|
|
680
|
+
};
|
|
681
|
+
} catch (err) {
|
|
682
|
+
console.error('[HMR] Failed to preserve chart state:', err);
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Restore ECharts instance state
|
|
689
|
+
* @param {Object} chartInstance - The ECharts instance
|
|
690
|
+
* @param {Object} state - The state to restore
|
|
691
|
+
*/
|
|
692
|
+
export function restoreChartState(chartInstance, state) {
|
|
693
|
+
if (!chartInstance || !state) return;
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
// Restore dataZoom state
|
|
697
|
+
if (state.dataZoom) {
|
|
698
|
+
chartInstance.setOption({
|
|
699
|
+
dataZoom: state.dataZoom,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Restore legend selection
|
|
704
|
+
if (state.legend) {
|
|
705
|
+
chartInstance.setOption({
|
|
706
|
+
legend: { selected: state.legend },
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Restore brush if any
|
|
711
|
+
if (state.brush) {
|
|
712
|
+
chartInstance.setOption({ brush: state.brush });
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
console.error('[HMR] Failed to restore chart state:', err);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Auto-initialize if in browser and enabled
|
|
720
|
+
if (typeof window !== 'undefined' && window.__VISUALIFY_HMR__?.enabled) {
|
|
721
|
+
initHMR();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export default HMRClient;
|