diff --git a/src/pageState.js b/src/pageState.js index 8ee66117..9acd8c8b 100644 --- a/src/pageState.js +++ b/src/pageState.js @@ -10,6 +10,20 @@ class ReactFiber { console.log("Selected Nodes:", this.nodes); console.log("🔍 Found Fibers:", this.fibers); console.log("🛠 Found Components:", this.components); + + // Debug fiber info + this.fibers.forEach((fiber, index) => { + if (fiber) { + console.log(`Fiber ${index}:`, { + tag: fiber.tag, + type: fiber.type?.name || fiber.type, + elementType: fiber.elementType, + stateNode: fiber.stateNode, + hasState: !!fiber.stateNode?.state, + hasMemoizedState: !!fiber.memoizedState + }); + } + }); } } @@ -19,10 +33,27 @@ class ReactFiber { getFiberNode(node) { if (!node) return null; + + // Try multiple property name patterns for different React versions + const possibleKeys = [ + '__reactFiber$', // React 16+ + '__reactInternalFiber$', // React 15 + '__reactInternalInstance$', // Older versions + '__reactFiber', + '__reactInternalInstance' + ]; + + // Check for exact matches first + for (const key of possibleKeys) { + if (node[key]) return node[key]; + } + + // Fall back to pattern matching const fiberKey = Object.getOwnPropertyNames(node).find( (name) => name.startsWith("__reactFiber") || - name.startsWith("__reactInternalInstance"), + name.startsWith("__reactInternalInstance") || + name.startsWith("__reactInternalFiber") ); return fiberKey ? node[fiberKey] : null; } @@ -30,20 +61,71 @@ class ReactFiber { getOwnerComponent(fiberNode) { let current = fiberNode; while (current) { + // Use React's internal tag system to identify component types + // Based on React's WorkTags: ClassComponent = 1, FunctionComponent = 0 + if (current.tag === 1) { // ClassComponent + return current.stateNode; // For class components, stateNode is the component instance + } + + // For function components, look for hooks in memoizedState + if (current.tag === 0 || current.tag === 15) { // FunctionComponent or MemoComponent + // Function components don't have setState, but we can still track them + if (current.memoizedState && current.type) { + return { + type: 'function', + hooks: current.memoizedState, + fiber: current, + forceUpdate: () => { + // Trigger re-render by updating fiber + if (current.alternate) { + current.alternate.expirationTime = 1; + } + current.expirationTime = 1; + } + }; + } + } + + // Legacy fallback: check if stateNode has React component methods if ( current.stateNode && + current.stateNode !== null && + typeof current.stateNode === 'object' && (current.stateNode.setState || current.stateNode.forceUpdate) ) { return current.stateNode; } + current = current.return; } return null; } getState(key) { - if (!this.components.length) return null; - const state = this.components[0]?.state || null; + if (!this.components.length && !this.fibers.length) return null; + + const component = this.components[0]; + const fiber = this.fibers[0]; + let state = null; + + // Handle class components + if (component?.state) { + state = component.state; + } + // Handle function components with hooks - look directly at fiber + else if (fiber?.memoizedState) { + if (this.debug) { + console.log("🔍 Raw fiber.memoizedState:", fiber.memoizedState); + } + // Extract useState values from the hook chain + const states = this.extractStateFromHooks(fiber.memoizedState); + state = states.length === 1 ? states[0] : states; + } + // Fallback: try component hooks if available + else if (component?.type === 'function' && component?.hooks) { + const states = this.extractStateFromHooks(component.hooks); + state = states.length === 1 ? states[0] : states; + } if (key === undefined) { return state; @@ -61,8 +143,137 @@ class ReactFiber { return null; } + extractStateFromHooks(hookChain) { + const states = []; + let mainStateFound = false; + let currentHook = hookChain; + let hookIndex = 0; + + if (this.debug) { + console.log("🔍 Hook chain analysis:"); + } + + while (currentHook) { + if (this.debug) { + console.log(`Hook ${hookIndex}:`, { + type: currentHook.tag || 'unknown', + memoizedState: currentHook.memoizedState, + queue: currentHook.queue, + next: !!currentHook.next + }); + } + + // Try different approaches to extract state + if (currentHook.memoizedState !== undefined && currentHook.memoizedState !== null) { + const state = currentHook.memoizedState; + + // Priority 1: Check for useRef hooks with complex state in .current + if (!currentHook.queue && + typeof state === 'object' && + state !== null && + state.current !== undefined && + typeof state.current === 'object' && + state.current !== null) { + + // Check if this looks like a substantial state object (has multiple properties) + const currentKeys = Object.keys(state.current); + if (currentKeys.length > 2) { + states.push(state.current); + mainStateFound = true; + if (this.debug) console.log(` 🎯 Found main state in useRef:`, state.current); + } + } + // Priority 2: useState hooks with queue + else if (currentHook.queue && typeof state !== 'function') { + states.push(state); + if (this.debug) console.log(` ✅ Found useState state:`, state); + } + // Priority 3: Other potential state objects (only if we haven't found main state) + else if (!mainStateFound && !currentHook.queue && typeof state === 'object' && state !== null) { + // Skip useEffect hooks (they have tag 36) + if (!(state.tag === 36 && state.create)) { + states.push(state); + if (this.debug) console.log(` 📦 Found potential state object:`, state); + } + } + // Priority 4: Simple primitive state + else if (typeof state !== 'function' && typeof state !== 'object') { + states.push(state); + if (this.debug) console.log(` 🔹 Found primitive state:`, state); + } + } + + currentHook = currentHook.next; + hookIndex++; + } + + if (this.debug) { + console.log(`🎯 Extracted ${states.length} state values:`, states); + } + + // If we found main state objects, prioritize and deduplicate them + if (mainStateFound && states.length > 1) { + const mainStates = states.filter(state => + typeof state === 'object' && + state !== null && + Object.keys(state).length > 2 + ); + + if (mainStates.length > 1) { + // If we have multiple main state objects, find the most comprehensive one + // or merge them if they seem complementary + const largestState = mainStates.reduce((largest, current) => { + const largestKeys = Object.keys(largest).length; + const currentKeys = Object.keys(current).length; + + // Prefer the one with more properties + if (currentKeys > largestKeys) return current; + + // If same number of properties, prefer the one with more complex data + if (currentKeys === largestKeys) { + const largestComplexity = this.calculateStateComplexity(largest); + const currentComplexity = this.calculateStateComplexity(current); + return currentComplexity > largestComplexity ? current : largest; + } + + return largest; + }); + + if (this.debug) { + console.log(`🎯 Selected most comprehensive state from ${mainStates.length} candidates:`, largestState); + } + + return [largestState]; + } + + return mainStates; + } + + return states; + } + + calculateStateComplexity(state) { + if (!state || typeof state !== 'object') return 0; + + let complexity = 0; + for (const [key, value] of Object.entries(state)) { + complexity += 1; // Base point for each property + + if (Array.isArray(value)) { + complexity += value.length * 0.1; // Arrays get points based on length + } else if (typeof value === 'object' && value !== null) { + complexity += Object.keys(value).length * 0.5; // Nested objects get points + } else if (typeof value === 'function') { + complexity += 2; // Functions are valuable + } + } + + return complexity; + } + setState(update) { this.components.forEach((component) => { + // Handle class components if (component?.setState) { if (typeof update === "function") { // Functional update @@ -85,6 +296,13 @@ class ReactFiber { }); } } + // Handle function components - force re-render since we can't directly update hooks + else if (component?.type === 'function' && component?.forceUpdate) { + if (this.debug) { + console.log("⚠️ Function component detected - triggering re-render. Direct state update not possible."); + } + component.forceUpdate(); + } }); return this; } @@ -99,7 +317,7 @@ class ReactFiber { return this.fibers[0]?.memoizedProps?.[propName]; } - setProp(propName) { + setProp(propName, value) { this.fibers.forEach((fiber) => { if (fiber?.memoizedProps) { fiber.memoizedProps[propName] = value; @@ -119,38 +337,176 @@ class ReactFiber { } } -function makeSerializable(obj) { - if (typeof obj !== "object" || obj === null) { +function makeSerializable(obj, visited = new WeakSet(), depth = 0, maxDepth = 10) { + // Handle primitives first + if (obj === null || obj === undefined) { + return obj; + } + + // Catch ALL functions early + if (typeof obj === "function") { + return `[Function: ${obj.name || 'anonymous'}]`; + } + + if (typeof obj !== "object") { + // Handle other primitives + if (typeof obj === "symbol") return obj.toString(); + if (typeof obj === "bigint") return obj.toString() + "n"; return obj; } - if (Array.isArray(obj)) { - return obj.map((item) => makeSerializable(item)); + // Prevent infinite recursion - depth limit + if (depth > maxDepth) { + return "[Max Depth Reached]"; } - const serializableObj = {}; - for (const key in obj) { - if (Object.hasOwn(obj, key)) { - let value = obj[key]; + // Prevent circular references + if (visited.has(obj)) { + return "[Circular Reference]"; + } + visited.add(obj); - if (typeof value === "function") { - value = "[Function]"; - } else if (value instanceof HTMLElement) { - value = { - type: "HTMLElement", - id: value.id, - tagName: value.tagName, - }; // Replace DOM node with ID/tag info - } else if (typeof value === "symbol") { - value = value.toString(); - } else if (typeof value === "object" && value !== null) { - value = makeSerializable(value); + try { + // Handle special objects first + if (obj instanceof HTMLElement) { + return { + type: "HTMLElement", + tagName: obj.tagName, + id: obj.id || null, + className: obj.className || null, + attributes: obj.attributes ? Array.from(obj.attributes).map(attr => ({ name: attr.name, value: attr.value })) : [] + }; + } + + if (obj instanceof Event) { + return { + type: "Event", + eventType: obj.type, + target: obj.target?.tagName || null + }; + } + + if (obj instanceof Date) { + return { type: "Date", value: obj.toISOString() }; + } + + if (obj instanceof RegExp) { + return { type: "RegExp", source: obj.source, flags: obj.flags }; + } + + if (obj instanceof Error) { + return { type: "Error", message: obj.message, name: obj.name }; + } + + // Handle React Fiber nodes - these are super circular + if (obj.tag !== undefined && obj.elementType !== undefined) { + return { + type: "ReactFiber", + tag: obj.tag, + elementType: typeof obj.elementType === 'function' ? obj.elementType.name || 'AnonymousComponent' : String(obj.elementType), + key: obj.key, + hasState: !!obj.stateNode?.state, + hasMemoizedState: !!obj.memoizedState, + hasProps: !!obj.memoizedProps + }; + } + + // Handle arrays + if (Array.isArray(obj)) { + return obj.slice(0, 50).map((item, index) => { + if (index >= 25) return "[...truncated]"; // Smaller limit + return makeSerializable(item, visited, depth + 1, maxDepth); + }); + } + + // Handle regular objects + const serializableObj = {}; + + // Get own enumerable properties only to avoid prototype pollution + const ownKeys = Object.getOwnPropertyNames(obj).filter(key => { + try { + return obj.propertyIsEnumerable(key); + } catch { + return false; } + }); + + // Limit number of properties to avoid huge objects + const maxKeys = 30; // Smaller limit for safety + const processedKeys = ownKeys.slice(0, maxKeys); + + for (const key of processedKeys) { + try { + // Skip problematic keys early + const dangerousKeys = [ + 'parentNode', 'parentElement', 'ownerDocument', 'children', 'childNodes', + 'return', 'child', 'sibling', 'alternate', 'ref', // React Fiber circular refs + '_owner', '_source', '_self', '_debugOwner', '_debugSource', // React internals + 'window', 'document', 'global', 'self', 'top', 'parent', // Global objects + 'constructor', 'prototype', '__proto__', // Constructor/prototype chains + 'addEventListener', 'removeEventListener', // Event handlers + 'setState', 'forceUpdate', 'render' // React methods that might be functions + ]; + + if (dangerousKeys.includes(key)) { + serializableObj[key] = `[Skipped: ${key}]`; + continue; + } - serializableObj[key] = value; + const descriptor = Object.getOwnPropertyDescriptor(obj, key); + if (descriptor && (descriptor.get || descriptor.set)) { + serializableObj[key] = "[Getter/Setter]"; + continue; + } + + let value = obj[key]; + + // Handle symbols specifically (React context symbols) + if (typeof value === "symbol") { + value = `[Symbol: ${value.description || 'anonymous'}]`; + } + // Extra function check + else if (typeof value === "function") { + value = `[Function: ${value.name || 'anonymous'}]`; + } else if (value && typeof value === "object") { + value = makeSerializable(value, visited, depth + 1, maxDepth); + } + + serializableObj[key] = value; + } catch (error) { + serializableObj[key] = `[Error: ${error.message}]`; + } + } + + if (ownKeys.length > maxKeys) { + serializableObj['...'] = `[${ownKeys.length - maxKeys} more properties]`; + } + + return serializableObj; + } catch (error) { + return `[Serialization Error: ${error.message}]`; + } finally { + visited.delete(obj); // Clean up for potential reuse + } +} + +// Final safety check - recursively scan for any remaining functions +function deepFunctionCheck(obj, path = "") { + if (typeof obj === "function") { + throw new Error(`Found function at path: ${path}`); + } + + if (obj && typeof obj === "object") { + if (Array.isArray(obj)) { + obj.forEach((item, index) => { + deepFunctionCheck(item, `${path}[${index}]`); + }); + } else { + Object.keys(obj).forEach(key => { + deepFunctionCheck(obj[key], path ? `${path}.${key}` : key); + }); } } - return serializableObj; } window.addEventListener("message", (event) => { @@ -196,6 +552,28 @@ window.addEventListener("message", (event) => { response = makeSerializable(response); } + // Final safety check before postMessage + try { + deepFunctionCheck(response); + } catch (functionError) { + console.warn("[pageState] Function detected in response, cleaning:", functionError.message); + response = `[Cleaned Response - Function found at: ${functionError.message}]`; + } + + // Additional structured clone test + try { + // Test if the object can be cloned (same algorithm as postMessage) + if (typeof structuredClone === 'function') { + structuredClone(response); + } else { + // Fallback for older browsers - try JSON round-trip + JSON.parse(JSON.stringify(response)); + } + } catch (cloneError) { + console.warn("[pageState] Response not cloneable, fallback:", cloneError.message); + response = `[Uncloneable Response: ${cloneError.message}]`; + } + window.postMessage( { type: "reactFiberResponse", diff --git a/src/seqta/utils/listeners/ClickListeners.ts b/src/seqta/utils/listeners/ClickListeners.ts index 1b9c3a95..bd317450 100644 --- a/src/seqta/utils/listeners/ClickListeners.ts +++ b/src/seqta/utils/listeners/ClickListeners.ts @@ -17,7 +17,7 @@ const handleNotificationClick = async (target: HTMLElement) => { if (!buttonId) return; const matchingNotification = - notificationList.storeState.notifications.items.find( + notificationList.items.find( (item: any) => item.notificationID === parseInt(buttonId), );