feat: use Session to track IPCs and improve UI#268
Conversation
| "postcss": "^8.5.4", | ||
| "postcss-loader": "^8.1.1", | ||
| "postcss-preset-env": "^10.2.1", | ||
| "prettier": "^3.6.2", |
There was a problem hiding this comment.
With Prettier 3.6, we can enable the --experimental-cli flag for a performance boost.
| /* ------------------------------------------------------ */ | ||
| /** | ||
| * This is used to keep the background script alive. More testing is needed to determine whether it is needed or not | ||
| * since other KEEP_ALIVE methods are already implemented in the content script and panel script. | ||
| * Code copied from: https://stackoverflow.com/questions/66618136/persistent-service-worker-in-chrome-extension | ||
| */ | ||
| const keepAlive = () => setInterval(chrome.runtime.getPlatformInfo, 20e3); | ||
| chrome.runtime.onStartup.addListener(keepAlive); | ||
| keepAlive(); | ||
| /* ------------------------------------------------------ */ | ||
|
|
There was a problem hiding this comment.
It would be good to put an explainer in the PR body to explain why we don't need to keep the background script alive using this hack anymore.
There was a problem hiding this comment.
Added a bullet point explaining the change.
src/lib/service-worker-preload.ts
Outdated
| // @ts-expect-error: addIpcEvent is available in the background service-worker | ||
| addIpcEvent(data); |
There was a problem hiding this comment.
nit: we might be able to add it to the global scope in TypeScript using https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html
There was a problem hiding this comment.
Done, I declared it globally in global.d.ts - 4387be5
src/index.ts
Outdated
| const sw = await ses.serviceWorkers.startWorkerForScope(extension.url); | ||
| sw.startTask(); | ||
| registerIpcListeners(ses, sw); | ||
| } catch (error_1) { |
There was a problem hiding this comment.
nit: any reason we're calling this error_1?
There was a problem hiding this comment.
No particular reason tbh. I renamed error_1 and error_2 to error - 2425951
src/index.ts
Outdated
| if (isInstalled) { | ||
| throw new Error( | ||
| 'Devtron has already been installed. Please avoid calling devtron.install() more than once.', | ||
| ); | ||
| } |
There was a problem hiding this comment.
Wondering what everyone's thoughts are on silently returning vs. throwing an error. I think throwing might be a bit disruptive for the end user?
There was a problem hiding this comment.
I agree, throwing might be a bit too much here. I replaced it with a simple return;. Let me know if you’d prefer I also add a console.warn() to display a warning. - 9e62744
| export default function DevtronProvider({ children }: DevtronProviderProps) { | ||
| // Theme | ||
| const [theme, setThemeState] = useState<Theme>('light'); | ||
| const setTheme = (newTheme: Theme) => { |
There was a problem hiding this comment.
One of the gotchas with using react context is that when the provider value changes, React will re-render all components that call useContext(DevtronContext), or useDevtronContext() in this case. Right now, the setters and value object are getting recalculated on every render. It's important to avoid this performance footgun by making sure the provider value object is stable. To do this:
- Memoize all of the setX callbacks using
useCallbackso that they aren't recalculated every render - Memoize the object you're passing to
DevtronContext.Provider'svalueparameter usinguseMemo
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [], |
There was a problem hiding this comment.
You should be able to safely add setLockToBottom to this deps array after you make the DevtronProvider value stable! See comment below
| const lockToBottomRef = useRef(lockToBottom); | ||
| const gridRef = useRef<AgGridReact<IpcEventDataIndexed> | null>(null); | ||
|
|
||
| const portRef = useRef<chrome.runtime.Port | null>(null); |
There was a problem hiding this comment.
Is portRef being read anywhere?
There was a problem hiding this comment.
It wasn't being used anywhere before but I ended up using it in clearEvents callback in this commit - f7b8436
| * Comment out the useEffect below if you want to test the UI in dev mode on localhost | ||
| * and use JSON data from test_data/test_data.js for testing. | ||
| * and use JSON data from test_data/test_data.ts for testing. |
There was a problem hiding this comment.
It might be easier to set a local debug value at the top of the file so that developers don't need to comment out this entire block
const DEBUG = false
function Panel() {
...
useEffect (() => {
if (DEBUG === true) {
return
}
...
}
...
}
There was a problem hiding this comment.
Done, I added const isDev = process.env.NODE_ENV === 'development'; instead. The app now loads test_data dynamically during development mode. - de1c472
|
|
||
| const portRef = useRef<chrome.runtime.Port | null>(null); | ||
| const pingIntervalRef = useRef<NodeJS.Timeout | null>(null); | ||
| const clearEventsRef = useRef(() => {}); |
There was a problem hiding this comment.
Using a useCallback is preferred for functions, it's a bit non-standard to have a callback conditionally set with a ref. If you want this callback to do nothing in dev mode, you can use the local debug value in the callback definition (see comment below). If you want this callback to do nothing until the first render useEffect finishes, you can track isPortReady in a useState, which you can set as the last step in the useEffect
07d40cb to
544bc34
Compare
| /** | ||
| * Comment out the useEffect below if you want to test the UI in dev mode on localhost | ||
| * and use JSON data from test_data/test_data.ts for testing. | ||
| */ |
There was a problem hiding this comment.
Nit: Is this comment still relevant?
| const lockToBottomRef = useRef(lockToBottom); | ||
| const gridRef = useRef<AgGridReact<IpcEventDataIndexed> | null>(null); | ||
|
|
||
| const [isPortReady, setIsPortReady] = useState(false); |
There was a problem hiding this comment.
nit (non-blocking): I would co-locate this useState with the useStates higher up in the component for better organization
| if (isDev) { | ||
| setEvents([]); | ||
| return; | ||
| } | ||
| if (!isPortReady || !portRef.current) return; | ||
| try { | ||
| portRef.current.postMessage({ type: MSG_TYPE.CLEAR_EVENTS } satisfies MessagePanel); | ||
| setEvents([]); | ||
| } catch (error) { | ||
| console.error('Devtron - Error clearing events:', error); | ||
| } |
There was a problem hiding this comment.
nit (non-blocking): it'd be nice to add newlines to break up these statements for better readability
samuelmaddock
left a comment
There was a problem hiding this comment.
Sorry for the late review, great progress!
| moduleType, | ||
| preloadFileName, | ||
| ), | ||
| type: 'service-worker', |
There was a problem hiding this comment.
Not needed, but an ID might be useful for folks who see this in ses.getPreloadScripts()
| type: 'service-worker', | |
| type: 'service-worker', | |
| id: 'devtron-preload', |
src/index.ts
Outdated
| path.resolve('node_modules', '@electron', 'devtron', 'dist', 'extension'), | ||
| { allowFileAccess: true }, | ||
| ); | ||
| startServiceWorker(ses, devtron); |
There was a problem hiding this comment.
| startServiceWorker(ses, devtron); | |
| await startServiceWorker(ses, devtron); |
src/index.ts
Outdated
| } | ||
| isInstalled = true; | ||
|
|
||
| app.on('session-created', async (ses) => { |
There was a problem hiding this comment.
Maybe this should explicitly install to session.defaultSession initially in case it's already been created at the point install() is called.
There was a problem hiding this comment.
I tried doing that here - 44a8f47
If this approach doesn't look right to you, I'm happy to explore other options if needed.
There was a problem hiding this comment.
src/index.ts
Outdated
| ses.registerPreloadScript({ | ||
| filePath: path.resolve( | ||
| 'node_modules', | ||
| '@electron', | ||
| 'devtron', | ||
| 'dist', | ||
| moduleType, | ||
| preloadFileName, | ||
| ), |
There was a problem hiding this comment.
If you define an export for the preload in the package.json, you can resolve it like this:
| ses.registerPreloadScript({ | |
| filePath: path.resolve( | |
| 'node_modules', | |
| '@electron', | |
| 'devtron', | |
| 'dist', | |
| moduleType, | |
| preloadFileName, | |
| ), | |
| import { createRequire } from 'node:module'; | |
| const dirname = moduleType === 'esm' ? import.meta.dirname : __dirname; | |
| const filePath = createRequire(dirname).resolve('@electron/devtron/preload'); | |
| ses.registerPreloadScript({ | |
| filePath, |
This PR changes/adds a couple of things:
ServiceWorkerandRendererprocesses to theMainprocess are now tracked viaSessionlisteners for the-ipc-message,-ipc-invoke, and-ipc-message-syncevents.ipcRenderertracking: IPC events invokingon,off,once,addListener,removeListener,removeAllListenersmethods onipcRendererare now properly tracked along with the method used.Directionbadges andChannelnames.LockToBottomfeature to automatically scroll to the newly added event.serviceWorker.startTask(). As a result, the previous workarounds have been removed in favor of this approach.IPC events going from
MaintoServiceWorkerare not tracked yet.Dark Theme:
