a screenshot of The Third Can's configuration page, v0.2.0
a screenshot of The Third Can's configuration page, v0.2.0

the third can

i made a browser extension with theki and contributions from sandvich. it adds lots of useful features to TwoCans.

i figure that in addition to sharing how you can use the extension for yourself, i could talk a bit here about how it works internally. but first, the obligatory:

i'd list the features here, but they're already listed right there on the readme so i'm not going to repeat myself (as much)

you can also run the development build (instructions on the github readme). it's less stable and doesn't persist across browser restarts (at least on firefox), but it gets new features sooner than the standard methods. not recommended unless you want to help test new features.

ok, rambling time

now that we've gotten the big stuff out of the way, i can just talk about whatever here. expect comments about how thethirdcan works, planned features, etc. do not expect incredibly high quality writing. will probably be updated periodically as i have Thoughts.

script injection

so here's the deal: some features of thethirdcan, such as the skip button confirmation prompt, require access to objects in the javascript runtime of the webpage (notably the TC object that has a lot of functions for answering, skipping, etc). for security reasons, the extension code cannot interact directly with this namespace, so we need a way to be able to write code that runs within the same namespace.

at the beginning, this was simple. i could just write a script in a separate file, and use the extension to add a new <script> tag to the webpage with that file as the source. and to be honest, it's still effectively that simple. but it's such a common pattern that we needed a centralized function for consistency.


we had another issue around the same time. the extension has a little popup to allow users to enable or disable any feature they want, and while they all saved their values in the same object, beyond that it was pretty much the wild west. you had to handle all the saving and loading mechanics yourself, check the config values all over the place, and so on.

on the side of the config popup, we rewrote it to be a bit more structured. we keep a list of enum options and boolean options, and pretty much everything else is handled automagically. (i say this as if it's complicated, it's really just iterating over a bunch of DOM objects from the html). however, this didn't help with the end that consumes the config values.

introducing third

the best javascript object since slicedBread!

third is a big collection of utilities, including some that solve both the script injection problem and the configuration consumption problem, much of which even occurs in the same function! let's take a look at how we built up the Big And Very Important Function, third.InjectToggleableScripts:

so we began with a simple function, third.InjectScript. it takes a path to the script that needs to be injected, and an optional configuration object in case the script element needs to be async, or defer, or anything like that. and we considered when we usually need to inject scripts--it is usually "when this configuration option is true". so instead of having many separate blocks that looked like this, we could do with a single function call that grouped together config options with the scripts that they enable.

if (config.someOption) {

so we created a new api, the first iteration of third.InjectToggleableScripts, which would be called like this:

    someOption: "someScript.js",
    someOtherOption: "someOtherScript.js"

it was a good first attempt, and works well in many situations. however, it doesn't support all the options of third.InjectScript, so we updated so that it could take an object instead of just a string. the object would contain a name element with the former string as its value, and then various configuration options.

sometimes, though, we need to inject multiple scripts when an option is enabled, and that doesn't fit with this structure. we would need two identical keys, which is simply not an option. so we had to make the configuration object even more dynamic: allowing the string/object to be replaced with an array of the same. at this point the api is getting pretty complicated, but it does almost everything we need, and we are almost to the current iteration. but before we get to the current iteration, we need to take a break to talk about fourth.


originaly, we thought third was the only utility object we'd need. however, an issue came up in that there's no good way to inject third itself into a page to provide utilities for injected scripts--and even if we could, it contains functions that just don't make sense to execute in that context. so we decided to keep up the naming scheme (even if perhaps we shouldn't've) and introduce an injected utility object, fourth.

injecting fourth

since at this point all scripts were injected with third.InjectToggleableScripts, we could take advantage of that to only inject fourth when it was potentially relevant. once we filter the scripts to only the ones that need to be injected, we pause for a moment. the list could be empty, and if so, we can return early. otherwise, we move on to inject fourth and then the remaining scripts. but here's the thing: depending on what page we're on, some of fourth's utilities can be performed more efficiently than in the general case. so here's what we do to define fourth's api:

  1. inject a simple script defining const fourth = {};
  2. inject a script for all the fourth fuctions with invariant definitions.
  3. depending on what page we are on, inject a separate function for each function that has a potentially variable definition (ensuring, of course, that the api remains consistent). problem is, we don't have any way to tell what page we're on. so that is the final change that gets us to the current incarnation of third.InjectToggleableScripts: adding one more argument at the end to indicate what page we're on.

at this point, third's injection method has been working really well for us, and (along with improved docs) has made it much faster and less error-prone to build and test new features!