SynQApp / Extension

Your music companion for the web, with a portable mini player and the ability to listen to any music link on your preferred service!
https://www.synqapp.io
Apache License 2.0
24 stars 2 forks source link

Messaging and state management refactor #22

Closed tekkeon closed 11 months ago

tekkeon commented 11 months ago

Important Note

Only after doing all of this refactor work did it become apparent that there was a problem with the hub messaging solution I created in this PR. The background service worker is short lived and forces the hub port to be closed after 30-90 seconds. After a lot of trial and error with several ideas to keep the background service worker alive, I managed to get a solution to work in the next PR by reconnecting the port everytime it disconnects.

Overview

This PR refactors both the messaging and the state management strategies to clean up hacks from before and standardize our communication as much as possible across the popup, background service worker, main world content scripts, and UI content scripts.

State Management

Prior to this change, we were following something analogous to the event-carried state transfer pattern where state was independently managed and replicated across the popup and sidebar. With this change, state has now been centralized into a single Redux store. Using Plasmo and some other tools, the Redux store is hooked into and persisted by the web browsers storage solution available to extensions. These tools then sync this state across the popup, content script UIs, and background service worker.

However, it does not sync the store to main world content scripts, which is where our music controllers live. As such, I created a background message handler that allows the main world content scripts to dispatch Redux actions via the background service worker.

Now, because the state is persisted across sessions and across domains, we'll have to make some decisions about how we handle multiple tabs being open at the same time, either by figuring out a good way to represent all of that state in our store or by limiting control to only one open tab across all UIs (which differs from YTM+ which allows the user to select which tab to control every time them open the mini player). But there are ways to handle both options regardless.

Messaging

Prior to this change, messaging was a convoluted mess using a mix of chrome.runtime.sendMessage, chrome.tabs.sendMessage, sendToBackground, sendToContentScript, and an awful message relay script that handled passing messages to our main world content scripts from an isolated content script along with several utilities to enable it. Now, things are not perfect but are at least much better.

Through the use of Plasmo's hub feature and some additional utilities I created, main world content scripts now have a direct line to talk to the background service worker, allowing us to completely remove the message relay and utilities. Additionally, with a new relay message handler in the background script, both the popup and sidebar can now send messages directly to the main world content script (the music controllers), which allowed me to remove a lot of convoluted contexts and context implementations that differed between popup and sidebar but are now standardized.

Basically it was a clusterf*** and now it is less-so.

One thing to note about the hub feature is that it requires us to use the externally_connectable feature in the manifest, which could be a security risk if we did not trust the music service websites. That said, given that:

  1. We do not handle sensitive data
  2. We trust those websites and trust them to be secure from XSS attacks

we should be good to do this. Using hub also requires the main world content scripts to pass in the extension's ID, so I had to create an isolated script whose sole purpose is to retrieve the extension ID. We could have hard coded it or used environment variables, but I believe this would become a pain for local development as the extension ID is subject to change anytime you fully uninstall and reinstall the extension locally. This solution allows it to work in any circumstance.