@uistate/core 3.1.2 → 4.0.1

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/package.json CHANGED
@@ -1,22 +1,21 @@
1
1
  {
2
2
  "name": "@uistate/core",
3
- "version": "3.1.2",
3
+ "version": "4.0.1",
4
+ "type": "module",
4
5
  "description": "High-performance UI state management using CSS custom properties and ADSI (Attribute-Driven State Inheritance)",
5
6
  "main": "src/index.js",
6
7
  "module": "src/index.js",
7
8
  "files": [
8
9
  "src"
9
10
  ],
10
- "scripts": {
11
- "test": "jest",
12
- "lint": "eslint src"
13
- },
11
+ "scripts": {},
14
12
  "keywords": [
15
13
  "state-management",
16
14
  "ui",
17
- "react",
18
15
  "css",
19
- "performance"
16
+ "performance",
17
+ "framework-agnostic",
18
+ "vanilla-js"
20
19
  ],
21
20
  "author": "Ajdin Imsirovic <ajdika@live.com> (GitHub)",
22
21
  "contributors": [
@@ -31,13 +30,5 @@
31
30
  "url": "https://github.com/ImsirovicAjdin/uistate/issues"
32
31
  },
33
32
  "homepage": "https://github.com/ImsirovicAjdin/uistate#readme",
34
- "peerDependencies": {
35
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
36
- },
37
- "devDependencies": {
38
- "eslint": "^8.0.0",
39
- "jest": "^29.0.0",
40
- "jest-environment-jsdom": "^29.0.0"
41
- },
42
33
  "sideEffects": false
43
34
  }
package/src/index.js CHANGED
@@ -1,353 +1,18 @@
1
1
  /**
2
- * UIstate - Declarative state management for the web
3
- *
4
- * A unified system that combines CSS and Event-based state management with templating
5
- * Integrates CSS variables, event-based state, and template management for optimal performance
6
- * Provides a simple, declarative API for state management
7
- * Uses the DOM as the source of truth for state
8
- * Supports plugins through a clean composition system
2
+ * UIstate - Simple barrel file for core modules
3
+ *
4
+ * Exports the four core UIstate modules:
5
+ * - cssState: CSS custom properties state management
6
+ * - eventState: Event-based state management
7
+ * - stateSerializer: State serialization utilities
8
+ * - templateManager: Declarative template management
9
9
  */
10
- import createCssState from './cssState.js';
11
- import { createEventState } from './eventState.js';
12
- import { createTemplateManager } from './templateManager.js';
13
10
 
14
- /**
15
- * Utility function to convert hierarchical path notation to CSS variable notation
16
- * Converts dot notation (e.g., 'user.profile.name') to dash notation (e.g., 'user-profile-name')
17
- * @param {string} path - Hierarchical path with dot notation
18
- * @returns {string} - CSS-compatible path with dash notation
19
- */
20
- function convertPathToCssPath(path) {
21
- return path.replace(/\./g, "-");
22
- }
23
-
24
- const createUnifiedState = (initialState = {}) => {
25
- // Create the CSS state manager with integrated serialization
26
- const cssState = createCssState(initialState);
27
-
28
- // Create the event state manager with the same initial state
29
- const eventState = createEventState(initialState);
30
-
31
- // Plugin system
32
- const plugins = new Map();
33
- const middlewares = [];
34
- const lifecycleHooks = {
35
- beforeStateChange: [],
36
- afterStateChange: [],
37
- onInit: [],
38
- onDestroy: []
39
- };
40
-
41
- // Create a unified API
42
- const unifiedState = {
43
- _isNotifying: false,
44
- _cssState: cssState, // Reference to cssState for direct access to advanced features
45
-
46
- // Get state with hierarchical path support
47
- getState(path) {
48
- // Always use eventState as the source of truth
49
- // This ensures consistency with the DOM state
50
- const value = eventState.get(path);
51
-
52
- // If path is undefined or value not found in eventState
53
- if (path && value === undefined) {
54
- // Fall back to CSS variable (for values set outside this API)
55
- const cssPath = convertPathToCssPath(path);
56
- return cssState.getState(cssPath);
57
- }
58
-
59
- return value;
60
- },
61
-
62
- // Set state with hierarchical path support
63
- setState(path, value) {
64
- if (!path) return this;
65
-
66
- // Run before state change middlewares
67
- let finalValue = value;
68
- let shouldContinue = true;
69
-
70
- // Apply middlewares
71
- for (const middleware of middlewares) {
72
- const result = middleware(path, finalValue, this.getState.bind(this));
73
- if (result === false) {
74
- shouldContinue = false;
75
- break;
76
- } else if (result !== undefined) {
77
- finalValue = result;
78
- }
79
- }
80
-
81
- // Run lifecycle hooks
82
- for (const hook of lifecycleHooks.beforeStateChange) {
83
- hook(path, finalValue, this.getState.bind(this));
84
- }
85
-
86
- if (!shouldContinue) return this;
87
-
88
- // Update event state (for JS access and pub/sub)
89
- eventState.set(path, finalValue);
90
-
91
- // Update CSS state (for styling and DOM representation)
92
- const cssPath = convertPathToCssPath(path);
93
- cssState.setState(cssPath, finalValue);
94
-
95
- // Run after state change hooks
96
- for (const hook of lifecycleHooks.afterStateChange) {
97
- hook(path, finalValue, this.getState.bind(this));
98
- }
99
-
100
- return this;
101
- },
102
-
103
- // Plugin system methods
104
- use(pluginName, plugin) {
105
- if (plugins.has(pluginName)) {
106
- console.warn(`Plugin '${pluginName}' is already registered. It will be replaced.`);
107
- }
108
-
109
- // Register the plugin
110
- plugins.set(pluginName, plugin);
111
-
112
- // Initialize the plugin
113
- if (typeof plugin.init === 'function') {
114
- // Pass the API to the plugin
115
- plugin.init(this);
116
- }
117
-
118
- // Register middlewares
119
- if (Array.isArray(plugin.middlewares)) {
120
- middlewares.push(...plugin.middlewares);
121
- }
122
-
123
- // Register lifecycle hooks
124
- if (plugin.hooks) {
125
- Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
126
- if (Array.isArray(lifecycleHooks[hookName])) {
127
- lifecycleHooks[hookName].push(hookFn);
128
- }
129
- });
130
- }
131
-
132
- // Add plugin methods to the API
133
- if (plugin.methods) {
134
- Object.entries(plugin.methods).forEach(([methodName, method]) => {
135
- if (typeof method === 'function') {
136
- this[methodName] = method.bind(plugin);
137
- }
138
- });
139
- }
140
-
141
- return this;
142
- },
143
-
144
- getPlugin(pluginName) {
145
- return plugins.get(pluginName);
146
- },
147
-
148
- hasPlugin(pluginName) {
149
- return plugins.has(pluginName);
150
- },
151
-
152
- removePlugin(pluginName) {
153
- const plugin = plugins.get(pluginName);
154
- if (!plugin) return false;
155
-
156
- // Run cleanup if available
157
- if (typeof plugin.destroy === 'function') {
158
- plugin.destroy();
159
- }
160
-
161
- // Remove plugin methods
162
- if (plugin.methods) {
163
- Object.keys(plugin.methods).forEach(methodName => {
164
- delete this[methodName];
165
- });
166
- }
167
-
168
- // Remove middlewares
169
- if (Array.isArray(plugin.middlewares)) {
170
- plugin.middlewares.forEach(middleware => {
171
- const index = middlewares.indexOf(middleware);
172
- if (index !== -1) {
173
- middlewares.splice(index, 1);
174
- }
175
- });
176
- }
177
-
178
- // Remove lifecycle hooks
179
- if (plugin.hooks) {
180
- Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
181
- if (Array.isArray(lifecycleHooks[hookName])) {
182
- const index = lifecycleHooks[hookName].indexOf(hookFn);
183
- if (index !== -1) {
184
- lifecycleHooks[hookName].splice(index, 1);
185
- }
186
- }
187
- });
188
- }
189
-
190
- // Remove the plugin
191
- plugins.delete(pluginName);
192
- return true;
193
- },
194
-
195
- // Add middleware
196
- addMiddleware(middleware) {
197
- if (typeof middleware === 'function') {
198
- middlewares.push(middleware);
199
- return true;
200
- }
201
- return false;
202
- },
203
-
204
- // Add lifecycle hook
205
- addHook(hookName, hookFn) {
206
- if (typeof hookFn === 'function' && Array.isArray(lifecycleHooks[hookName])) {
207
- lifecycleHooks[hookName].push(hookFn);
208
- return true;
209
- }
210
- return false;
211
- },
212
-
213
- // Subscribe to state changes with support for wildcards
214
- subscribe(path, callback) {
215
- return eventState.subscribe(path, callback);
216
- },
217
-
218
- // Observe CSS state changes (simpler API, no wildcards)
219
- observe(key, callback) {
220
- return cssState.observe(key, callback);
221
- },
222
-
223
- // Configure serialization options
224
- configureSerializer(config) {
225
- cssState.configureSerializer(config);
226
- return this;
227
- },
228
-
229
- // Get current serializer configuration
230
- getSerializerConfig() {
231
- return cssState._serializer?.config || null;
232
- },
233
-
234
- // Set serialization mode ('hybrid', 'json', or 'escape')
235
- setSerializationMode(mode) {
236
- return this.configureSerializer({ mode });
237
- },
238
-
239
- // Clean up resources
240
- destroy() {
241
- // Run onDestroy hooks
242
- for (const hook of lifecycleHooks.onDestroy) {
243
- hook();
244
- }
245
-
246
- // Destroy all plugins
247
- for (const [pluginName, plugin] of plugins.entries()) {
248
- if (typeof plugin.destroy === 'function') {
249
- plugin.destroy();
250
- }
251
- }
252
-
253
- // Clear plugins and middlewares
254
- plugins.clear();
255
- middlewares.length = 0;
256
- Object.keys(lifecycleHooks).forEach(key => {
257
- lifecycleHooks[key].length = 0;
258
- });
259
-
260
- // Destroy core state management
261
- cssState.destroy();
262
- eventState.destroy();
263
- }
264
- };
265
-
266
- return unifiedState;
267
- };
268
-
269
- // Create a singleton instance of the unified state
270
- const UnifiedState = createUnifiedState();
271
-
272
- // Create a template manager plugin
273
- const templateManagerPlugin = {
274
- name: 'templateManager',
275
- instance: null,
276
-
277
- init(api) {
278
- this.instance = createTemplateManager(api);
279
- return this;
280
- },
281
-
282
- destroy() {
283
- // Any cleanup needed for the template manager
284
- },
285
-
286
- // Plugin methods that will be added to the UIstate API
287
- methods: {
288
- onAction(action, handler) {
289
- return this.instance.onAction(action, handler);
290
- },
291
-
292
- registerActions(actionsMap) {
293
- return this.instance.registerActions(actionsMap);
294
- },
295
-
296
- attachDelegation(root = document.body) {
297
- return this.instance.attachDelegation(root);
298
- },
299
-
300
- mount(componentName, container) {
301
- return this.instance.mount(componentName, container);
302
- },
303
-
304
- renderTemplateFromCss(templateName, data) {
305
- return this.instance.renderTemplateFromCss(templateName, data);
306
- },
307
-
308
- createComponent(name, renderFn, stateKeys) {
309
- return this.instance.createComponent(name, renderFn, stateKeys);
310
- },
311
-
312
- applyClassesFromState(element, stateKey, options) {
313
- return this.instance.applyClassesFromState(element, stateKey, options);
314
- }
315
- },
316
-
317
- // Expose handlers property
318
- get handlers() {
319
- return this.instance.handlers;
320
- }
321
- };
322
-
323
- // Create a combined API with plugin support
324
- const UIstate = {
325
- ...UnifiedState,
326
-
327
- // Initialize the system with plugins
328
- init() {
329
- // Register the template manager plugin
330
- this.use('templateManager', templateManagerPlugin);
331
-
332
- // Run onInit hooks
333
- for (const hook of this.getPlugin('templateManager').instance.lifecycleHooks?.onInit || []) {
334
- // for (const hook of lifecycleHooks.onInit) {
335
- hook();
336
- }
337
-
338
- // Attach event delegation to document body
339
- this.attachDelegation(document.body);
340
-
341
- return this;
342
- }
343
- };
11
+ // Export the four core modules
12
+ export { createCssState } from './cssState.js';
13
+ export { createEventState } from './eventState.js';
14
+ export { default as stateSerializer } from './stateSerializer.js';
15
+ export { createTemplateManager } from './templateManager.js';
344
16
 
345
- export default UIstate;
346
- export {
347
- createUnifiedState,
348
- createCssState,
349
- createEventState,
350
- createTemplateManager,
351
- convertPathToCssPath,
352
- templateManagerPlugin
353
- };
17
+ // For convenience, also export cssState as default
18
+ export { createCssState as default } from './cssState.js';
@@ -1,140 +0,0 @@
1
- /**
2
- * StateInspector - Real-time state inspection panel for UIstate
3
- *
4
- * This module provides a configurable UI panel that displays and allows manipulation
5
- * of CSS variables and state values in UIstate applications.
6
- *
7
- * Features:
8
- * - Toggle visibility with a dedicated button
9
- * - Real-time updates of CSS variable values
10
- * - Organized display of state by categories
11
- * - Ability to filter state variables
12
- * - Direct manipulation of state values
13
- */
14
-
15
- /**
16
- * Create a configured state inspector instance
17
- * @param {Object} config - Configuration options
18
- * @returns {Object} - StateInspector instance
19
- */
20
- function createStateInspector(config = {}) {
21
- // Default configuration
22
- const defaultConfig = {
23
- target: document.body,
24
- initiallyVisible: false,
25
- categories: ['app', 'ui', 'data'],
26
- position: 'bottom-right',
27
- maxHeight: '300px',
28
- stateManager: null, // Optional reference to UIstate or CssState instance
29
- theme: 'light'
30
- };
31
-
32
- // Merge provided config with defaults
33
- const options = { ...defaultConfig, ...config };
34
-
35
- // Private state
36
- let isVisible = options.initiallyVisible;
37
- let panel = null;
38
- let toggleButton = null;
39
- let filterInput = null;
40
- let stateContainer = null;
41
- let currentFilter = '';
42
-
43
- // StateInspector instance
44
- const inspector = {
45
- // Current configuration
46
- config: options,
47
-
48
- /**
49
- * Update configuration
50
- * @param {Object} newConfig - New configuration options
51
- */
52
- configure(newConfig) {
53
- Object.assign(this.config, newConfig);
54
- this.refresh();
55
- },
56
-
57
- /**
58
- * Attach the inspector panel to the target element
59
- * @param {Element} element - Target element to attach to (defaults to config.target)
60
- * @returns {Object} - The inspector instance for chaining
61
- */
62
- attach(element = this.config.target) {
63
- // Implementation will create and attach the panel
64
- // For now, this is a placeholder
65
- console.log('StateInspector: Panel would be attached to', element);
66
- return this;
67
- },
68
-
69
- /**
70
- * Show the inspector panel
71
- * @returns {Object} - The inspector instance for chaining
72
- */
73
- show() {
74
- isVisible = true;
75
- if (panel) panel.style.display = 'block';
76
- return this;
77
- },
78
-
79
- /**
80
- * Hide the inspector panel
81
- * @returns {Object} - The inspector instance for chaining
82
- */
83
- hide() {
84
- isVisible = false;
85
- if (panel) panel.style.display = 'none';
86
- return this;
87
- },
88
-
89
- /**
90
- * Toggle the visibility of the inspector panel
91
- * @returns {Object} - The inspector instance for chaining
92
- */
93
- toggle() {
94
- return isVisible ? this.hide() : this.show();
95
- },
96
-
97
- /**
98
- * Refresh the state display
99
- * @returns {Object} - The inspector instance for chaining
100
- */
101
- refresh() {
102
- // Implementation will update the displayed state values
103
- // For now, this is a placeholder
104
- console.log('StateInspector: State display would be refreshed');
105
- return this;
106
- },
107
-
108
- /**
109
- * Filter the displayed state variables
110
- * @param {string} filterText - Text to filter by
111
- * @returns {Object} - The inspector instance for chaining
112
- */
113
- filter(filterText) {
114
- currentFilter = filterText;
115
- // Implementation will filter the displayed variables
116
- return this.refresh();
117
- },
118
-
119
- /**
120
- * Clean up resources used by the inspector
121
- */
122
- destroy() {
123
- if (panel && panel.parentNode) {
124
- panel.parentNode.removeChild(panel);
125
- }
126
- panel = null;
127
- toggleButton = null;
128
- filterInput = null;
129
- stateContainer = null;
130
- }
131
- };
132
-
133
- return inspector;
134
- }
135
-
136
- // Create a default instance
137
- const StateInspector = createStateInspector();
138
-
139
- export default StateInspector;
140
- export { createStateInspector };
@@ -1,638 +0,0 @@
1
- /**
2
- * UIstate Telemetry Plugin
3
- *
4
- * A plugin for tracking and analyzing usage patterns in UIstate applications.
5
- * Provides insights into method calls, state changes, and performance metrics.
6
- *
7
- * Features:
8
- * - Automatic tracking of API method calls
9
- * - State change monitoring
10
- * - Performance timing
11
- * - Usage statistics and reporting
12
- * - Visual dashboard integration
13
- */
14
-
15
- /**
16
- * Create a telemetry plugin instance
17
- * @param {Object} config - Configuration options
18
- * @returns {Object} - Configured telemetry plugin
19
- */
20
- const createTelemetryPlugin = (config = {}) => {
21
- // Default configuration
22
- const defaultConfig = {
23
- enabled: true,
24
- trackStateChanges: true,
25
- trackMethodCalls: true,
26
- trackPerformance: true,
27
- maxEntries: 1000,
28
- logToConsole: false,
29
- samplingRate: 1.0, // 1.0 = track everything, 0.5 = track 50% of events
30
- };
31
-
32
- // Merge provided config with defaults
33
- const options = { ...defaultConfig, ...config };
34
-
35
- // Telemetry data storage
36
- const data = {
37
- startTime: Date.now(),
38
- methodCalls: new Map(),
39
- stateChanges: new Map(),
40
- performanceMetrics: new Map(),
41
- unusedMethods: new Set(),
42
- activeTimers: new Map()
43
- };
44
-
45
- // Reference to the UIstate instance
46
- let uistateRef = null;
47
-
48
- // Original method references
49
- const originalMethods = new Map();
50
-
51
- // Methods to track (will be populated during initialization)
52
- const methodsToTrack = [
53
- 'getState', 'setState', 'subscribe', 'observe',
54
- 'configureSerializer', 'getSerializerConfig', 'setSerializationMode',
55
- 'onAction', 'registerActions', 'attachDelegation', 'mount',
56
- 'renderTemplateFromCss', 'createComponent', 'applyClassesFromState'
57
- ];
58
-
59
- // Helper function to check if we should sample this event
60
- const shouldSample = () => {
61
- return options.samplingRate >= 1.0 || Math.random() <= options.samplingRate;
62
- };
63
-
64
- // Helper function to track a method call
65
- const trackMethodCall = (methodName, args = [], result = undefined, error = null, duration = 0) => {
66
- if (!options.enabled || !options.trackMethodCalls || !shouldSample()) return;
67
-
68
- if (!data.methodCalls.has(methodName)) {
69
- data.methodCalls.set(methodName, []);
70
- }
71
-
72
- const calls = data.methodCalls.get(methodName);
73
-
74
- // Limit the number of entries to prevent memory issues
75
- if (calls.length >= options.maxEntries) {
76
- calls.shift(); // Remove oldest entry
77
- }
78
-
79
- // Create a safe copy of arguments (avoiding circular references)
80
- const safeArgs = Array.from(args).map(arg => {
81
- try {
82
- // For simple types, return as is
83
- if (arg === null || arg === undefined ||
84
- typeof arg === 'string' ||
85
- typeof arg === 'number' ||
86
- typeof arg === 'boolean') {
87
- return arg;
88
- }
89
-
90
- // For functions, just return the function name or "[Function]"
91
- if (typeof arg === 'function') {
92
- return arg.name || "[Function]";
93
- }
94
-
95
- // For objects, create a simplified representation
96
- if (typeof arg === 'object') {
97
- if (Array.isArray(arg)) {
98
- return `[Array(${arg.length})]`;
99
- }
100
- return Object.keys(arg).length > 0
101
- ? `[Object: ${Object.keys(arg).join(', ')}]`
102
- : '[Object]';
103
- }
104
-
105
- return String(arg);
106
- } catch (e) {
107
- return "[Complex Value]";
108
- }
109
- });
110
-
111
- // Create a safe copy of the result
112
- let safeResult;
113
- try {
114
- if (result === null || result === undefined ||
115
- typeof result === 'string' ||
116
- typeof result === 'number' ||
117
- typeof result === 'boolean') {
118
- safeResult = result;
119
- } else if (typeof result === 'function') {
120
- safeResult = result.name || "[Function]";
121
- } else if (typeof result === 'object') {
122
- if (Array.isArray(result)) {
123
- safeResult = `[Array(${result.length})]`;
124
- } else {
125
- safeResult = Object.keys(result).length > 0
126
- ? `[Object: ${Object.keys(result).join(', ')}]`
127
- : '[Object]';
128
- }
129
- } else {
130
- safeResult = String(result);
131
- }
132
- } catch (e) {
133
- safeResult = "[Complex Value]";
134
- }
135
-
136
- // Record the call
137
- calls.push({
138
- timestamp: Date.now(),
139
- relativeTime: Date.now() - data.startTime,
140
- args: safeArgs,
141
- result: safeResult,
142
- error: error ? error.message : null,
143
- duration
144
- });
145
-
146
- // Log to console if enabled
147
- if (options.logToConsole) {
148
- console.log(`[Telemetry] ${methodName}`, {
149
- args: safeArgs,
150
- result: safeResult,
151
- error: error ? error.message : null,
152
- duration
153
- });
154
- }
155
- };
156
-
157
- // Helper function to track state changes
158
- const trackStateChange = (path, value, previousValue) => {
159
- if (!options.enabled || !options.trackStateChanges || !shouldSample()) return;
160
-
161
- if (!data.stateChanges.has(path)) {
162
- data.stateChanges.set(path, []);
163
- }
164
-
165
- const changes = data.stateChanges.get(path);
166
-
167
- // Limit the number of entries to prevent memory issues
168
- if (changes.length >= options.maxEntries) {
169
- changes.shift(); // Remove oldest entry
170
- }
171
-
172
- // Create safe copies of values
173
- let safeValue, safePreviousValue;
174
- try {
175
- safeValue = JSON.stringify(value);
176
- } catch (e) {
177
- safeValue = String(value);
178
- }
179
-
180
- try {
181
- safePreviousValue = JSON.stringify(previousValue);
182
- } catch (e) {
183
- safePreviousValue = String(previousValue);
184
- }
185
-
186
- // Record the change
187
- changes.push({
188
- timestamp: Date.now(),
189
- relativeTime: Date.now() - data.startTime,
190
- value: safeValue,
191
- previousValue: safePreviousValue
192
- });
193
- };
194
-
195
- // Helper function to start a performance timer
196
- const startTimer = (label) => {
197
- if (!options.enabled || !options.trackPerformance) return;
198
-
199
- data.activeTimers.set(label, performance.now());
200
- };
201
-
202
- // Helper function to end a performance timer
203
- const endTimer = (label) => {
204
- if (!options.enabled || !options.trackPerformance) return 0;
205
-
206
- if (!data.activeTimers.has(label)) return 0;
207
-
208
- const startTime = data.activeTimers.get(label);
209
- const duration = performance.now() - startTime;
210
-
211
- data.activeTimers.delete(label);
212
-
213
- if (!data.performanceMetrics.has(label)) {
214
- data.performanceMetrics.set(label, {
215
- count: 0,
216
- totalDuration: 0,
217
- minDuration: Infinity,
218
- maxDuration: 0,
219
- samples: []
220
- });
221
- }
222
-
223
- const metrics = data.performanceMetrics.get(label);
224
- metrics.count++;
225
- metrics.totalDuration += duration;
226
- metrics.minDuration = Math.min(metrics.minDuration, duration);
227
- metrics.maxDuration = Math.max(metrics.maxDuration, duration);
228
-
229
- // Store sample data (limited to prevent memory issues)
230
- if (metrics.samples.length >= options.maxEntries) {
231
- metrics.samples.shift();
232
- }
233
-
234
- metrics.samples.push({
235
- timestamp: Date.now(),
236
- relativeTime: Date.now() - data.startTime,
237
- duration
238
- });
239
-
240
- return duration;
241
- };
242
-
243
- // Method proxying function
244
- const createMethodProxy = (obj, methodName) => {
245
- // Store original method
246
- const originalMethod = obj[methodName];
247
- originalMethods.set(methodName, originalMethod);
248
-
249
- // Create proxy
250
- return function(...args) {
251
- if (!options.enabled) {
252
- return originalMethod.apply(this, args);
253
- }
254
-
255
- let result, error;
256
-
257
- startTimer(`method.${methodName}`);
258
-
259
- try {
260
- // Call original method
261
- result = originalMethod.apply(this, args);
262
-
263
- // Handle promises
264
- if (result instanceof Promise) {
265
- return result
266
- .then(asyncResult => {
267
- const duration = endTimer(`method.${methodName}`);
268
- trackMethodCall(methodName, args, asyncResult, null, duration);
269
- return asyncResult;
270
- })
271
- .catch(asyncError => {
272
- const duration = endTimer(`method.${methodName}`);
273
- trackMethodCall(methodName, args, undefined, asyncError, duration);
274
- throw asyncError;
275
- });
276
- }
277
-
278
- const duration = endTimer(`method.${methodName}`);
279
- trackMethodCall(methodName, args, result, null, duration);
280
- return result;
281
-
282
- } catch (e) {
283
- error = e;
284
- const duration = endTimer(`method.${methodName}`);
285
- trackMethodCall(methodName, args, undefined, error, duration);
286
- throw error;
287
- }
288
- };
289
- };
290
-
291
- // Create the telemetry plugin
292
- const telemetryPlugin = {
293
- name: 'telemetry',
294
-
295
- // Initialize the plugin
296
- init(api) {
297
- if (!options.enabled) return this;
298
-
299
- uistateRef = api;
300
-
301
- // Proxy methods for tracking
302
- if (options.trackMethodCalls) {
303
- methodsToTrack.forEach(methodName => {
304
- if (typeof api[methodName] === 'function') {
305
- // Add to list of methods to check for usage
306
- data.unusedMethods.add(methodName);
307
-
308
- // Create proxy
309
- api[methodName] = createMethodProxy(api, methodName);
310
- }
311
- });
312
- }
313
-
314
- return this;
315
- },
316
-
317
- // Middleware for state changes
318
- middlewares: [
319
- (path, value, getState) => {
320
- if (options.enabled && options.trackStateChanges) {
321
- const previousValue = getState(path);
322
- startTimer(`stateChange.${path}`);
323
-
324
- // Return a proxy that will track after the state change
325
- return {
326
- __telemetryValue: true,
327
- value,
328
- path,
329
- previousValue,
330
- finalize() {
331
- const duration = endTimer(`stateChange.${path}`);
332
- trackStateChange(path, value, previousValue);
333
- return value;
334
- }
335
- };
336
- }
337
- return value;
338
- }
339
- ],
340
-
341
- // Lifecycle hooks
342
- hooks: {
343
- beforeStateChange: (path, value) => {
344
- // If this is our proxy value, do nothing
345
- if (value && value.__telemetryValue) {
346
- return;
347
- }
348
- },
349
-
350
- afterStateChange: (path, value) => {
351
- // If this is our proxy value, finalize it
352
- if (value && value.__telemetryValue) {
353
- return value.finalize();
354
- }
355
- }
356
- },
357
-
358
- // Plugin methods
359
- methods: {
360
- // Enable or disable telemetry
361
- setEnabled(enabled) {
362
- options.enabled = enabled;
363
- return this;
364
- },
365
-
366
- // Get current configuration
367
- getConfig() {
368
- return { ...options };
369
- },
370
-
371
- // Update configuration
372
- configure(newConfig) {
373
- Object.assign(options, newConfig);
374
- return this;
375
- },
376
-
377
- // Reset telemetry data
378
- reset() {
379
- data.startTime = Date.now();
380
- data.methodCalls.clear();
381
- data.stateChanges.clear();
382
- data.performanceMetrics.clear();
383
- data.activeTimers.clear();
384
- return this;
385
- },
386
-
387
- // Get method call statistics
388
- getMethodStats() {
389
- const stats = {};
390
-
391
- data.methodCalls.forEach((calls, methodName) => {
392
- // Mark method as used
393
- data.unusedMethods.delete(methodName);
394
-
395
- stats[methodName] = {
396
- count: calls.length,
397
- averageDuration: calls.reduce((sum, call) => sum + (call.duration || 0), 0) / calls.length,
398
- lastCall: calls[calls.length - 1],
399
- firstCall: calls[0]
400
- };
401
- });
402
-
403
- return stats;
404
- },
405
-
406
- // Get detailed method call data
407
- getMethodCalls(methodName) {
408
- if (!methodName) {
409
- const result = {};
410
- data.methodCalls.forEach((calls, method) => {
411
- result[method] = [...calls];
412
- });
413
- return result;
414
- }
415
-
416
- return data.methodCalls.has(methodName)
417
- ? [...data.methodCalls.get(methodName)]
418
- : [];
419
- },
420
-
421
- // Get state change statistics
422
- getStateStats() {
423
- const stats = {};
424
-
425
- data.stateChanges.forEach((changes, path) => {
426
- stats[path] = {
427
- count: changes.length,
428
- lastChange: changes[changes.length - 1],
429
- firstChange: changes[0]
430
- };
431
- });
432
-
433
- return stats;
434
- },
435
-
436
- // Get detailed state change data
437
- getStateChanges(path) {
438
- if (!path) {
439
- const result = {};
440
- data.stateChanges.forEach((changes, statePath) => {
441
- result[statePath] = [...changes];
442
- });
443
- return result;
444
- }
445
-
446
- return data.stateChanges.has(path)
447
- ? [...data.stateChanges.get(path)]
448
- : [];
449
- },
450
-
451
- // Get performance metrics
452
- getPerformanceMetrics() {
453
- const metrics = {};
454
-
455
- data.performanceMetrics.forEach((data, label) => {
456
- metrics[label] = {
457
- count: data.count,
458
- totalDuration: data.totalDuration,
459
- averageDuration: data.totalDuration / data.count,
460
- minDuration: data.minDuration,
461
- maxDuration: data.maxDuration
462
- };
463
- });
464
-
465
- return metrics;
466
- },
467
-
468
- // Get unused methods
469
- getUnusedMethods() {
470
- return [...data.unusedMethods];
471
- },
472
-
473
- // Get comprehensive telemetry report
474
- getReport() {
475
- return {
476
- startTime: data.startTime,
477
- duration: Date.now() - data.startTime,
478
- methodStats: this.getMethodStats(),
479
- stateStats: this.getStateStats(),
480
- performanceMetrics: this.getPerformanceMetrics(),
481
- unusedMethods: this.getUnusedMethods(),
482
- config: this.getConfig(),
483
- // Add detailed logs for download
484
- detailedLogs: {
485
- methodCalls: this.getMethodCalls(),
486
- stateChanges: this.getStateChanges()
487
- }
488
- };
489
- },
490
-
491
- // Download telemetry logs as a JSON file
492
- downloadLogs() {
493
- const report = this.getReport();
494
- const data = JSON.stringify(report, null, 2);
495
- const blob = new Blob([data], { type: 'application/json' });
496
- const url = URL.createObjectURL(blob);
497
-
498
- const a = document.createElement('a');
499
- a.href = url;
500
- a.download = `uistate-telemetry-${new Date().toISOString().replace(/:/g, '-')}.json`;
501
- document.body.appendChild(a);
502
- a.click();
503
- document.body.removeChild(a);
504
- URL.revokeObjectURL(url);
505
-
506
- return true;
507
- },
508
-
509
- // Create a visual dashboard
510
- createDashboard(container = document.body) {
511
- // Create dashboard element
512
- const dashboard = document.createElement('div');
513
- dashboard.className = 'uistate-telemetry-dashboard';
514
- dashboard.style.cssText = `
515
- position: fixed;
516
- bottom: 10px;
517
- right: 10px;
518
- width: 300px;
519
- max-height: 400px;
520
- overflow: auto;
521
- background: #fff;
522
- border: 1px solid #ccc;
523
- border-radius: 4px;
524
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
525
- padding: 10px;
526
- font-family: monospace;
527
- font-size: 12px;
528
- z-index: 9999;
529
- `;
530
-
531
- // Update function
532
- const updateDashboard = () => {
533
- const report = this.getReport();
534
-
535
- dashboard.innerHTML = `
536
- <h3 style="margin: 0 0 10px; font-size: 14px;">UIstate Telemetry</h3>
537
- <div style="margin-bottom: 5px;">
538
- <button id="telemetry-refresh">Refresh</button>
539
- <button id="telemetry-reset">Reset</button>
540
- <button id="telemetry-close">Close</button>
541
- </div>
542
- <div style="margin: 10px 0;">
543
- <strong>Duration:</strong> ${Math.round((Date.now() - report.startTime) / 1000)}s
544
- </div>
545
-
546
- <h4 style="margin: 10px 0 5px; font-size: 13px;">Method Calls</h4>
547
- <table style="width: 100%; border-collapse: collapse;">
548
- <tr>
549
- <th style="text-align: left; border-bottom: 1px solid #eee;">Method</th>
550
- <th style="text-align: right; border-bottom: 1px solid #eee;">Count</th>
551
- <th style="text-align: right; border-bottom: 1px solid #eee;">Avg Time</th>
552
- </tr>
553
- ${Object.entries(report.methodStats)
554
- .sort((a, b) => b[1].count - a[1].count)
555
- .map(([method, stats]) => `
556
- <tr>
557
- <td style="padding: 2px 0; border-bottom: 1px solid #eee;">${method}</td>
558
- <td style="text-align: right; padding: 2px 0; border-bottom: 1px solid #eee;">${stats.count}</td>
559
- <td style="text-align: right; padding: 2px 0; border-bottom: 1px solid #eee;">${stats.averageDuration ? stats.averageDuration.toFixed(2) + 'ms' : 'n/a'}</td>
560
- </tr>
561
- `).join('')}
562
- </table>
563
-
564
- <h4 style="margin: 10px 0 5px; font-size: 13px;">State Changes</h4>
565
- <table style="width: 100%; border-collapse: collapse;">
566
- <tr>
567
- <th style="text-align: left; border-bottom: 1px solid #eee;">Path</th>
568
- <th style="text-align: right; border-bottom: 1px solid #eee;">Count</th>
569
- </tr>
570
- ${Object.entries(report.stateStats)
571
- .sort((a, b) => b[1].count - a[1].count)
572
- .map(([path, stats]) => `
573
- <tr>
574
- <td style="padding: 2px 0; border-bottom: 1px solid #eee;">${path}</td>
575
- <td style="text-align: right; padding: 2px 0; border-bottom: 1px solid #eee;">${stats.count}</td>
576
- </tr>
577
- `).join('')}
578
- </table>
579
-
580
- ${report.unusedMethods.length > 0 ? `
581
- <h4 style="margin: 10px 0 5px; font-size: 13px;">Unused Methods</h4>
582
- <div style="color: #666;">
583
- ${report.unusedMethods.join(', ')}
584
- </div>
585
- ` : ''}
586
- `;
587
-
588
- // Add event listeners
589
- dashboard.querySelector('#telemetry-refresh').addEventListener('click', updateDashboard);
590
- dashboard.querySelector('#telemetry-reset').addEventListener('click', () => {
591
- this.reset();
592
- updateDashboard();
593
- });
594
- dashboard.querySelector('#telemetry-close').addEventListener('click', () => {
595
- dashboard.remove();
596
- });
597
- };
598
-
599
- // Initial update
600
- updateDashboard();
601
-
602
- // Add to container
603
- container.appendChild(dashboard);
604
-
605
- return dashboard;
606
- }
607
- },
608
-
609
- // Clean up
610
- destroy() {
611
- // Restore original methods
612
- if (uistateRef) {
613
- originalMethods.forEach((originalMethod, methodName) => {
614
- if (uistateRef[methodName]) {
615
- uistateRef[methodName] = originalMethod;
616
- }
617
- });
618
- }
619
-
620
- // Clear data
621
- data.methodCalls.clear();
622
- data.stateChanges.clear();
623
- data.performanceMetrics.clear();
624
- data.activeTimers.clear();
625
- data.unusedMethods.clear();
626
-
627
- uistateRef = null;
628
- }
629
- };
630
-
631
- return telemetryPlugin;
632
- };
633
-
634
- // Create a default instance with standard configuration
635
- const telemetryPlugin = createTelemetryPlugin();
636
-
637
- export default telemetryPlugin;
638
- export { createTelemetryPlugin };