Skip to content
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

Merged
merged 21 commits into from
Sep 8, 2022

Conversation

mnholtz
Copy link
Collaborator

@mnholtz mnholtz commented Sep 7, 2022

What does this PR do?

  • Closes Create starter blueprints install link #4245
  • Depends on https://github.com/pixiebrix/pixiebrix-app/pull/2250
  • Instead of implementing an installation link for Playground blueprints (as described in the issue), this adds an onUpdate event listener on browser tabs that will install starter blueprints if the user visits/refreshes a Playground page.
  • We are still "preinstalling" Starter Blueprints & opening the Playground on first-time installs if successful just in case this process fails

Reviewer Tips

  • Take a look mainly at changes to starterBlueprints.ts. Lots of additional refactoring was done to support this feature. The tests in starterBlueprint.test.ts describe expected behavior for first-time installs.

Discussion

  • I've added an installation lock and a debounce to the onUpdate event listener for Playground installation, in order to prevent issues with the onUpdate event being fired rapidly (as it often does).

Demo

Screen.Recording.2022-09-07.at.9.26.27.AM.mov

Future Work

  • We decided that we'd like to modify the Webflow Playground pages to include a more helpful message if the Playground blueprint installation still fails.

Checklist

  • Add tests
  • Designate a primary reviewer @fregante

mnholtz and others added 3 commits September 7, 2022 11:09
…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
Copy link
Contributor

@BLoe BLoe left a 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);
Copy link
Contributor

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?

Copy link
Contributor

@fregante fregante Sep 7, 2022

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.

Copy link
Collaborator Author

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.

@mnholtz mnholtz changed the title Feature/4245 playground recovery Add onUpdate event listener for installing starter blueprints on Playground urls Sep 7, 2022
console.debug(
"Skipping starter blueprint installation because the extension is not linked to the PixieBrix service"
);
return [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💙

void installStarterBlueprints();
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tab?.url?.startsWith(PLAYGROUND_URL)) {
void installStarterBlueprints();
Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Contributor

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.

  1. installStarterBlueprints is called the first time
  2. It starts an API call via getStarterBlueprints
  3. installStarterBlueprints is called a second time while the API call is still in progress
  4. 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.

Copy link
Contributor

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 then saveOptions overriding each other (race condition, almost exactly what was described in Verify that recordEvent correctly handles concurrent events #4073)

Copy link
Collaborator Author

@mnholtz mnholtz Sep 8, 2022

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.

Copy link
Contributor

@fregante fregante Sep 8, 2022

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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-commenter
Copy link

codecov-commenter commented Sep 8, 2022

Codecov Report

Merging #4249 (271a258) into main (63f3851) will increase coverage by 0.05%.
The diff coverage is 85.45%.

❗ Current head 271a258 differs from pull request most recent head f35c539. Consider uploading reports for the commit f35c539 to get more accurate results

@@            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     
Impacted Files Coverage Δ
src/background/starterBlueprints.ts 86.11% <85.45%> (-1.39%) ⬇️
src/options/pages/blueprints/BlueprintsPage.tsx 0.00% <0.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

Copy link
Contributor

@fregante fregante left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filipino-traditional-fisherman-boats-philippines-135762770

})
);
}

export async function installStarterBlueprints(): Promise<void> {
async function installBlueprints(
Copy link
Contributor

@fregante fregante Sep 8, 2022

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.

Suggested change
async function installBlueprints(
async function _installBlueprints(


await saveOptions(extensionsState);
const installed = await installStarterBlueprints();
Copy link
Contributor

@fregante fregante Sep 8, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create starter blueprints install link
5 participants