-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add onUpdate event listener for installing starter blueprints on Playground urls #4249
Conversation
…iebrix-extension into feature/4245_playground_recovery a commit message to explain why this merge is necessary, f it merges an updated upstream into a topic branch. ng with '#' will be ignored, and an empty message aborts fix test typo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming here is slightly hard to follow, but it's not too bad and it's all contained in this file, nothing is exported. You could possibly add some jsdoc comments on the functions to help explain the flow of things, and also to clarify what the boolean return values mean.
let extensionsState = await loadOptions(); | ||
return shouldInstall; | ||
} catch (error) { | ||
reportError(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is going to be returned from this function when it catches an error? I think void
will be coerced to false
, but that's not really clear from looking at the code. Can we please return false here explicitly to make it more clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 TypeScript in loose mode misses a lot of such warnings. It should have complained that the signature is boolean
but there's no return
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching this Ben! This was indeed a mistake.
console.debug( | ||
"Skipping starter blueprint installation because the extension is not linked to the PixieBrix service" | ||
); | ||
return []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💙
src/background/starterBlueprints.ts
Outdated
void installStarterBlueprints(); | ||
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { | ||
if (tab?.url?.startsWith(PLAYGROUND_URL)) { | ||
void installStarterBlueprints(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my comment got lost:
This listener will run installStarterBlueprints
every time the playground is opened, for any number of open tabs, and every time a new page is visited within the playground. Given it contains some async getter/setters, this will certainly cause trouble (as well as unnecessary computation)
How about this pattern:
async function initStarterBlueprints() {
if (await isInstalled()) {
return
}
if (await shouldInstall() || await onPlayGroundOpen()) {
install()
}
}
async function onPlayGroundOpen() {
return new Promise(resolve => {
const listener = (id, info, tab) => {
if (tab?.url?.startsWith(PLAYGROUND_URL)) {
browser.tabs.onUpdated.removeListener(listener)
resolve(true)
}
}
browser.tabs.onUpdated.addListener(listener)
})
}
To note:
- initStarterBlueprints is only run once and may be "pending" forever, or until the user opens the playground
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function will run, but I think the check on line 54 prevents any blueprint from being installed twice, right? What kind of trouble are you imagining this will cause? Can you explain how your code example here avoids that trouble?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
browser.tabs.onUpdated
fires a lot of times with all kinds of updates, even favicon updates, status updates, url updates.
installStarterBlueprints
is called the first time- It starts an API call via
getStarterBlueprints
installStarterBlueprints
is called a second time while the API call is still in progress- It starts another API call
At this point line 54 hasn't been reached yet, so any logic there is futile.
My example avoids the trouble because it does call installStarterBlueprints
every time the event happens; the listener is only used once to resolve a promise, and then the listener is removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What kind of trouble are you imagining this will cause?
Some possibilities:
- each client reporting the same error multiple times on line 112
- each client making multiple unnecessary calls to the server, further slowing the server down
loadOptions
being called twice simultaneously, and thensaveOptions
overriding each other (race condition, almost exactly what was described in Verify that recordEvent correctly handles concurrent events #4073)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fregante I'm not sure the pattern you suggested fits the needs of the feature, unless I'm misunderstanding your pseudocode. initStarterBlueprints
will run only once per extension launch. Therefore, the conditional if (await shouldInstall() || await onPlayGroundOpen()) {
will also be run only once. Subsequent visits to the Playground page after uninstalling Playground blueprints/or a failed installation will not work. I'm actually not sure why the event listener is being re-added in the pseudocode if the conditional is run only once anyways.
We need users to always have Starter Blueprints installed for them when visiting the Playground page, to catch the cases if they were to uninstall the Blueprints and revisit the Playground, or if an error like a network request failed to install them the first time. In this case, I think I might go with a locking (and/or maybe debounce?) pattern in order to solve the concerns you outlined above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need users to always have Starter Blueprints installed for them when visiting the Playground page
I didn't think about that, although it would only be hit if they visit the playground, delete the blueprint, and then visit the playground again — all while never quitting the browser.
I guess it's fine to debounce it to once per minute, that would work even better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the discussion Federico! Yeah, debounce sounds good to me also.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah thanks federico! Great considerations here
Codecov Report
@@ Coverage Diff @@
## main #4249 +/- ##
==========================================
+ Coverage 48.45% 48.51% +0.05%
==========================================
Files 887 887
Lines 26105 26143 +38
Branches 5391 5397 +6
==========================================
+ Hits 12650 12684 +34
- Misses 12537 12540 +3
- Partials 918 919 +1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
}) | ||
); | ||
} | ||
|
||
export async function installStarterBlueprints(): Promise<void> { | ||
async function installBlueprints( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To ensure that it's not called undebounced (as it currently is on line 150), we can prepend _
to the name.
I generally inline the function in the debounce
definition (debounce(() => {}, opts)
) but for long functions it's less readable.
async function installBlueprints( | |
async function _installBlueprints( |
src/background/starterBlueprints.ts
Outdated
|
||
await saveOptions(extensionsState); | ||
const installed = await installStarterBlueprints(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While unlikely, it could be called concurrently with browser.tabs.onUpdated
. It doesn't hurt to use the debounced version everywhere to avoid these leaks
What does this PR do?
onUpdate
event listener on browser tabs that will install starter blueprints if the user visits/refreshes a Playground page.Reviewer Tips
starterBlueprints.ts
. Lots of additional refactoring was done to support this feature. The tests instarterBlueprint.test.ts
describe expected behavior for first-time installs.Discussion
onUpdate
event listener for Playground installation, in order to prevent issues with theonUpdate
event being fired rapidly (as it often does).Demo
Screen.Recording.2022-09-07.at.9.26.27.AM.mov
Future Work
Checklist