feat: complete react fiber control loop

This commit is contained in:
SethBurkart123
2025-02-23 20:49:09 +11:00
parent f41da95f7e
commit a51049154b
4 changed files with 136 additions and 115 deletions
+13 -17
View File
@@ -92,25 +92,21 @@ async function init() {
console.info('[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.') console.info('[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.')
main() main()
waitForElm('.Viewer__Viewer___32BH-', true).then(() => { /* waitForElm('.Viewer__Viewer___32BH-', true).then(async () => {
console.log('Element exists') console.log('Element exists')
const button = document.createElement('button') await browser.runtime.sendMessage({ type: 'injectMainScript' })
button.innerHTML = 'Click me'
button.onclick = () => { const nice = ReactFiber.find(".Viewer__Viewer___32BH-", { debug: true })
// function call to run onclick
ReactFiber.find(".Viewer__Viewer___32BH-", { debug: true })
.setState(prev => ({ ...prev, selected: new Set([999999]) }))
} console.log(nice.getState())
button.style.position = 'fixed' nice.setState({ selected: new Set([999431]) })
button.style.top = '0'
button.style.right = '0'
button.style.padding = '10px' //console.log(nice)
button.style.zIndex = '9999' }) */
button.style.backgroundColor = 'red'
button.style.color = 'white'
document.body.appendChild(button)
})
} catch (error: any) { } catch (error: any) {
console.error(error) console.error(error)
} }
+6 -31
View File
@@ -13,13 +13,7 @@ function reloadSeqtaPages() {
result.then(open, console.error) result.then(open, console.error)
} }
const injectedTabs = new Set<number>(); // Keep track of injected tabs
async function injectPageState(tabId: number) { async function injectPageState(tabId: number) {
if (injectedTabs.has(tabId)) {
return; // Already injected
}
try { try {
await browser.scripting.executeScript({ await browser.scripting.executeScript({
target: { tabId }, target: { tabId },
@@ -27,18 +21,12 @@ async function injectPageState(tabId: number) {
// @ts-ignore // @ts-ignore
world: "MAIN", world: "MAIN",
}); });
injectedTabs.add(tabId); console.info(`[background] Injected pageState.js into tab ${tabId}`);
console.log(`[background] Injected pageState.js into tab ${tabId}`);
} catch (err) { } catch (err) {
console.error(`[background] Failed to inject pageState.js into tab ${tabId}:`, err); console.error(`[background] Failed to inject pageState.js into tab ${tabId}:`, err);
} }
} }
// Remove the script when a tab is closed
browser.tabs.onRemoved.addListener((tabId) => {
injectedTabs.delete(tabId);
});
// Main message listener // Main message listener
browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse: (response?: any) => void) => { browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse: (response?: any) => void) => {
@@ -88,27 +76,14 @@ browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse:
GetNews(sendResponse, url); GetNews(sendResponse, url);
return true; return true;
case 'reactFiberRequest': {
//const { tabId, /* selector, action, payload, debug */ } = request;
const tabId = sender.tab.id;
// Ensure pageState.js is injected case 'injectMainScript': {
injectPageState(tabId).then(() => { const senderTab = sender.tab ? sender.tab.id : undefined;
// Forward the request to the injected script
browser.tabs.sendMessage(tabId, { ...request, type: "reactFiberAction" }, { frameId: 0 }) // Target the main world injectPageState(senderTab!)
.then(response => { return;
sendResponse(response); // Send the response back to the content script
})
.catch(err => {
console.error(`[background] Error communicating with injected script in tab ${tabId}:`, err);
});
}).catch(err => {
console.error("[background] Failed to inject", err);
});
return true; // Keep the message channel open for the async response
} }
default: default:
console.log('Unknown request type'); console.log('Unknown request type');
} }
+64 -22
View File
@@ -1,3 +1,4 @@
// pageState.ts
class ReactFiber { class ReactFiber {
constructor(selector, options = {}) { constructor(selector, options = {}) {
this.selector = selector; this.selector = selector;
@@ -36,37 +37,62 @@ class ReactFiber {
return null; return null;
} }
getState(key = null) { getState(key) {
if (!this.components.length) return null; if (!this.components.length) return null;
const state = this.components[0]?.state || null; const state = this.components[0]?.state || null;
return key ? state?.[key] : state;
if (key === undefined) {
return state; // Return entire state
} else if (typeof key === 'string') {
return state?.[key]; // Return single key
} else if (Array.isArray(key)) {
// Return object with only specified keys
const filteredState = {};
for (const k of key) {
if (state && Object.hasOwn(state, k)) { // Use Object.hasOwn for safety
filteredState[k] = state[k];
}
}
return filteredState;
}
return null; // Invalid key type
} }
setState(updateFn) { setState(update) {
this.components.forEach(component => { this.components.forEach(component => {
if (component?.setState) { if (component?.setState) {
if (typeof update === 'function') {
// Functional update
component.setState(prevState => { component.setState(prevState => {
const newState = updateFn(prevState); const newState = update(prevState);
if (this.debug) console.log("✅ Updated State:", newState); if (this.debug) console.log("✅ Updated State (Functional):", newState);
return newState;
});
} else {
// Object update (merge with existing state)
component.setState(prevState => {
const newState = { ...prevState, ...update }; // Merge here!
if (this.debug) console.log("✅ Updated State (Object Merge):", newState);
return newState; return newState;
}); });
} }
}
}); });
return this; // Enable chaining return this;
} }
getProp(propName) { getProp(propName) {
if (!this.fibers.length) return null; if (!this.fibers.length) return null;
return this.fibers[0]?.memoizedProps?.[propName] || null; return this.fibers[0]?.memoizedProps?.[propName];
} }
setProp(propName, value) { setProp(propName) {
this.fibers.forEach(fiber => { this.fibers.forEach(fiber => {
if (fiber?.memoizedProps) { if (fiber?.memoizedProps) {
fiber.memoizedProps[propName] = value; fiber.memoizedProps[propName] = value;
} }
}); });
return this.forceUpdate(); // Apply the change and return this for chaining return this; // Enable chaining
} }
forceUpdate() { forceUpdate() {
@@ -80,37 +106,53 @@ class ReactFiber {
} }
} }
// Message listener for communication with the background script console.log("Window cat: ", window.cat);
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "reactFiberAction") {
const { selector, action, payload, debug } = request;
const fiberInstance = ReactFiber.find(selector, {debug}); // Use the class
// Listen for messages from the background script (via window.postMessage)
window.addEventListener('message', (event) => {
console.log(event)
if (event.data.type === "reactFiberRequest") {
const { selector, action, payload, debug, messageId } = event.data;
const fiberInstance = ReactFiber.find(selector, { debug });
let response;
switch (action) { switch (action) {
case "getState": case "getState":
sendResponse(fiberInstance.getState(payload.key)); response = fiberInstance.getState(payload.key);
break; break;
case "setState": case "setState":
// Very important: Eval the function string in the context of the page // Handle both function and object updates
if (payload.updateFn) {
const updateFn = eval(`(${payload.updateFn})`); const updateFn = eval(`(${payload.updateFn})`);
fiberInstance.setState(updateFn); fiberInstance.setState(updateFn);
sendResponse({}); // Send acknowledgement } else {
fiberInstance.setState(payload.updateObject);
}
response = {}; // Acknowledge
break; break;
case "getProp": case "getProp":
sendResponse(fiberInstance.getProp(payload.propName)); response = fiberInstance.getProp(payload.propName);
break; break;
case "setProp": case "setProp":
fiberInstance.setProp(payload.propName, payload.value); fiberInstance.setProp(payload.propName, payload.value);
sendResponse({}); // Send acknowledgement response = {}; // Acknowledge
break; break;
case "forceUpdate": case "forceUpdate":
fiberInstance.forceUpdate(); fiberInstance.forceUpdate();
sendResponse({}); // Send acknowledgement response = {}; // Acknowledge
break; break;
default: default:
console.warn(`[pageState] Unknown action: ${action}`); console.warn(`[pageState] Unknown action: ${action}`);
sendResponse(null); response = null;
} }
return true; // Keep message channel open (for consistency, even if not always needed)
// Send the response back to the background script using window.postMessage
window.postMessage({
type: "reactFiberResponse",
response,
messageId,
}, "*");
} }
}); });
+26 -18
View File
@@ -1,8 +1,9 @@
import browser from 'webextension-polyfill'; import browser from 'webextension-polyfill';
class ReactFiber { class ReactFiber {
private selector: string; private selector: string;
private debug: boolean; private debug: boolean;
private messageIdCounter: number = 0; // Counter for unique message IDs
constructor(selector: string, options: { debug?: boolean } = {}) { constructor(selector: string, options: { debug?: boolean } = {}) {
this.selector = selector; this.selector = selector;
@@ -14,54 +15,61 @@ private selector: string;
} }
private async sendMessage(action: string, payload: any = {}): Promise<any> { private async sendMessage(action: string, payload: any = {}): Promise<any> {
return new Promise((resolve, _) => {
const messageId = this.messageIdCounter++;
const message = { const message = {
type: "reactFiberRequest", type: "reactFiberRequest",
selector: this.selector, selector: this.selector,
action, action,
payload, payload,
debug: this.debug debug: this.debug,
messageId, // Include the unique message ID
}; };
try { const listener = (response: any) => {
const response = await browser.runtime.sendMessage(message); if (response.data?.type === 'reactFiberResponse' && response.data?.messageId === messageId) {
if (this.debug) { if (this.debug) {
console.log("Content Received Response:", response); console.log("Content Received Response:", response.data.response);
} }
return response; resolve(response.data.response);
} catch (error) { window.removeEventListener("message", listener)
console.error("Error sending message:", error);
return null; // or throw error, depending on desired behavior
} }
};
window.addEventListener('message', listener);
window.postMessage(message, "*");
});
} }
async getState(key?: string): Promise<any> { async getState(key?: string | string[]): Promise<any> { // Type change: allow string or string[]
return this.sendMessage("getState", { key }); return this.sendMessage("getState", { key });
} }
async setState(updateFn: (prevState: any) => any): Promise<ReactFiber> { async setState(update: any | ((prevState: any) => any)): Promise<ReactFiber> {
// Serialize the update function to a string. This is the tricky part. // Now async again.
// We can only send strings/JSON-serializable data through messages. const updateFnString = typeof update === 'function' ? update.toString() : null;
const updateFnString = updateFn.toString(); const updateObject = typeof update !== 'function' ? update : null;
await this.sendMessage("setState", { updateFn: updateFnString });
await this.sendMessage("setState", { updateFn: updateFnString, updateObject });
return this; return this;
} }
async getProp(propName: string): Promise<any> { async getProp(propName: string): Promise<any> {
return this.sendMessage("getProp", { propName }); return this.sendMessage("getProp", { propName });
} }
async setProp(propName: string, value: any): Promise<ReactFiber> { async setProp(propName: string, value: any): Promise<ReactFiber> {
// Now async again
await this.sendMessage("setProp", { propName, value }); await this.sendMessage("setProp", { propName, value });
return this; return this;
} }
async forceUpdate(): Promise<ReactFiber> { async forceUpdate(): Promise<ReactFiber> {
// Now async again
await this.sendMessage("forceUpdate"); await this.sendMessage("forceUpdate");
return this; return this;
} }
} }
export default ReactFiber;
export default ReactFiber