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

Slice 1: Initial ModInstance type and adapter applied to re-/activation code (1/3) #9182

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

twschiller
Copy link
Contributor

@twschiller twschiller commented Sep 22, 2024

What does this PR do?

Discussion

  • By using adapters, we can evaluate the ergonomics of the shape before migrating the persisted mod component slice shape
  • Using the mod instance selector in the lifecycle is a bit tricky due to draft behavior:
    const modComponentsToActivate = options.activatedModComponents.filter(
    • We may want to punt on that until we change Page Editor behavior to use drafts for all mod components when a mod is selected vs. only drafts for mod components that have been interacted with in the Page Editor
  • A benefit of doing this work now is that it simplifies some of the deployment code that @fungairino is working on

Future Work

Prioritizing changes that simplify deployment code, doesn't require a migration, and avoids the Page Editor:

  1. Rewrite deployment updater to pass around ModInstance[] instead of ActivatedModComponent[]: Slice 2: Use ModInstance type in mod deployment code (2/3) #9190
  2. Rewrite Mods Screen / Launcher to pass around ModInstance[] instead of passing around ActivatedModComponent[]. E.g., see buildModsList, buildGetModActivationStatus, etc.: Slice 3: Use ModInstance type on the mods screen (3/3) #9191

Consider adding a wrapper that wraps ModInstance with helper methods for derived data/properties. E.g., modId, isPaused, etc. without duplicating the data

For more information on our expectations for the PR process, see the
code review principles doc

@@ -890,115 +883,3 @@ describe("buildNewMod", () => {
},
);
});

describe("findMaxIntegrationDependencyApiVersion", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to utility folder test file

@twschiller twschiller changed the title [WIP] mod instance type and adapters [WIP] initial mod instance type and adapters Sep 22, 2024
Copy link

github-actions bot commented Sep 22, 2024

Playwright test results

passed  122 passed
flaky  12 flaky
skipped  4 skipped

Details

report  Open report ↗︎
stats  138 tests across 45 suites
duration  1 hour, 15 minutes, 56 seconds
commit  625c84f
info  For more information on how to debug and view this report, see our readme

Flaky tests

msedge-setup › setup/affiliated.setup.ts › authenticate with affiliated user
msedge › tests/modLifecycle.spec.ts › create, run, package, and update mod
chrome › tests/pageEditor/addStarterBrick.spec.ts › Add starter brick to mod
msedge › tests/pageEditor/addStarterBrick.spec.ts › Add starter brick to mod
msedge › tests/pageEditor/brickActions.spec.ts › brick actions panel behavior
msedge › tests/pageEditor/copyMod.spec.ts › run a copied mod with a built-in integration
chrome › tests/pageEditor/specialPages.spec.ts › Restricted browser page
msedge › tests/regressions/welcomeStarterBricks.spec.ts › #8740: can view the starter mods on the pixiebrix.com/welcome page
chrome › tests/runtime/insertAtCursor.spec.ts › Insert at Cursor › 8157: can insert at cursor from side bar
chrome › tests/runtime/sidebar/sidebarAuth.spec.ts › Connect action in partner auth sidebar takes user to the Extension Console
chrome › tests/runtime/textSnippets.spec.ts › text snippet shortcut functionality
msedge › tests/runtime/textSnippets.spec.ts › text snippet shortcut functionality

Skipped tests

chrome › tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts › #8104: Do not automatically close the sidebar when saving in the Page Editor
msedge › tests/regressions/doNotCloseSidebarOnPageEditorSave.spec.ts › #8104: Do not automatically close the sidebar when saving in the Page Editor
chrome › tests/runtime/googleSheetsIntegration.spec.ts › can activate a google spreadsheet mod with config options
msedge › tests/runtime/googleSheetsIntegration.spec.ts › can activate a google spreadsheet mod with config options

@twschiller twschiller changed the title [WIP] initial mod instance type and adapters [WIP] initial mod instance type and adapter applies to activation code Sep 22, 2024
@twschiller twschiller changed the title [WIP] initial mod instance type and adapter applies to activation code [WIP] initial mod instance type and adapter applied to re-/activation code Sep 22, 2024
const hasPersonalDeployment = activatedModComponentsForMod?.some(
(x) => x._deployment?.isPersonalDeployment,
);
const hasPersonalDeployment =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Example of simplifying personal deployment code

* In the future, we might consider eliminating by using a predictable id based on the mod instance id and position
* in the mod definition. But that's not possible today because the ids use a UUID format.
*/
modComponentIds: UUID[];
Copy link
Contributor Author

@twschiller twschiller Sep 22, 2024

Choose a reason for hiding this comment

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

A key insight I didn't originally anticipate is that ModInstance has to track the mod component ids. (See comment in code for why)

@twschiller twschiller changed the title [WIP] initial mod instance type and adapter applied to re-/activation code Initial mod instance type and adapter applied to re-/activation code Sep 22, 2024
@twschiller twschiller marked this pull request as ready for review September 22, 2024 13:09
Copy link

codecov bot commented Sep 22, 2024

Codecov Report

Attention: Patch coverage is 92.00000% with 12 lines in your changes missing coverage. Please review.

Project coverage is 74.82%. Comparing base (8318d74) to head (625c84f).
Report is 321 commits behind head on main.

Files with missing lines Patch % Lines
src/activation/useActivateModWizard.ts 92.10% 3 Missing ⚠️
src/store/modComponents/modComponentUtils.ts 90.62% 3 Missing ⚠️
src/store/modComponents/modInstanceSelectors.ts 85.71% 2 Missing ⚠️
src/utils/registryUtils.ts 33.33% 2 Missing ⚠️
.../sidebar/activateMod/ActivateMultipleModsPanel.tsx 66.66% 1 Missing ⚠️
src/store/modComponents/modInstanceUtils.ts 96.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9182      +/-   ##
==========================================
+ Coverage   74.24%   74.82%   +0.57%     
==========================================
  Files        1332     1369      +37     
  Lines       40817    42205    +1388     
  Branches     7634     7885     +251     
==========================================
+ Hits        30306    31580    +1274     
- Misses      10511    10625     +114     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@twschiller twschiller changed the title Initial mod instance type and adapter applied to re-/activation code Initial mod instance type and adapter applied to re-/activation code (1/2) Sep 22, 2024
@twschiller twschiller changed the title Initial mod instance type and adapter applied to re-/activation code (1/2) Initial mod instance type and adapter applied to re-/activation code (1/3) Sep 22, 2024
* @since 1.7.37
* @note This function is just for safety, there's currently no way for a mod to end up with "mixed" integration api versions.
*/
export function findMaxIntegrationDependencyApiVersion(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved the methods out of page editor directory to where they can be used cross-context

/**
* Returns a sharing object private package defined in the user's scope.
*/
export function createPrivateSharing(): Sharing {
Copy link
Contributor Author

@twschiller twschiller Sep 22, 2024

Choose a reason for hiding this comment

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

There's a sharing factory. But in general, I think we want to keep prod data creation separate from test factories to avoid dummy data accidentally finding its way into production code

It would be good to align on naming conventions to distinguish, e.g., using create prefix in prod code vs. factory suffix

@@ -62,7 +62,6 @@ function setupInputs(): {
modDefinition: ModDefinition;
} {
const formValues: WizardValues = {
modComponents: { 0: true },
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dropped unused modComponents toggle field from the type

export type UseActivateModWizardResult = {
wizardSteps: WizardStep[];
initialValues: WizardValues;
validationSchema: Yup.AnyObjectSchema;
};

function getInitialIntegrationDependencies(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracted these 2 from other method because they're both quite long

@@ -60,8 +60,6 @@ function useModNotFoundRedirectEffect(error: unknown): void {

/**
* Common page for activating a mod definition
*
* @param modDefinitionQuery The mod definition to activate
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Invalid documentation

* Maps activated mod components to a mod instance.
* @param modComponents mod components from the mod
*/
export function mapActivatedModComponentsToModInstance(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The primary adapter to review in this PR

// definition vs. looking up the definition
options: emptyModOptionsDefinitionFactory(),
sharing: modMetadata.sharing ?? createPrivateSharing(),
updated_at: modMetadata.updated_at ?? firstComponent.updateTimestamp,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We might consider this line of refactoring as an opportunity to fix the snake case and/or perform other reshaping

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would perform that at the service layer level. We also want to map extensionPoints -> starterBricks at the same time

@twschiller twschiller changed the title Initial mod instance type and adapter applied to re-/activation code (1/3) Initial ModInstance type and adapter applied to re-/activation code (1/3) Sep 22, 2024
@twschiller twschiller changed the title Initial ModInstance type and adapter applied to re-/activation code (1/3) Slice 1: Initial ModInstance type and adapter applied to re-/activation code (1/3) Sep 24, 2024
Comment on lines +31 to +36
if (!Array.isArray(options.activatedModComponents)) {
console.warn("state migration has not been applied yet", {
options,
});
throw new TypeError("state migration has not been applied yet");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

NIT: better to compare options._persist.version to persistModComponentOptionsConfig.version as it would make this selector much more change-tolerant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense - I had copied this from the modComponentSelectors code

if (!Array.isArray(options.activatedModComponents)) {

Copy link
Contributor Author

@twschiller twschiller Sep 26, 2024

Choose a reason for hiding this comment

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

I think that might be a problem for test ergonomics given that test stores typically don't have the persistence props attached to the slices? https://github.com/pixiebrix/pixiebrix-extension/blob/feature%2Fdevex-mod-instance/src/pageEditor/testHelpers.ts#L38-L38

Let's punt to be separate work?

Comment on lines +49 to +58
export const selectModInstanceMap = createSelector(
selectModInstances,
(modInstances) =>
new Map(
modInstances.map((modInstance) => [
modInstance.definition.metadata.id,
modInstance,
]),
),
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would avoid returning a Map from a redux selector. We shouldn't ever store a Map in redux, so we would still be adapting redux state even after a redux migration

Copy link
Contributor Author

@twschiller twschiller Sep 26, 2024

Choose a reason for hiding this comment

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

I would avoid returning a Map from a redux selector. We shouldn't ever store a Map in redux, so we would still be adapting redux state even after a redux migration

Could you clarify your concern for returning a Map? The output of selectors don't need to be serializable

We shouldn't ever store a Map in redux, so we would still be adapting redux state even after a redux migration

My understanding of selectors is that they adapt the persisted redux state into a form that's easy for the application to use. They allow you, for example, to have the data denormalized in the redux state (to avoid invalid data) and then normalize it for the application to use

Not sure what you'd recommend instead. The options off the top of my head are:

  • Creating a selector factory that takes an id and produces a selector?
  • Returning an array having the callsite do an O(n) search?
  • Having this return an object for the callsite to do the lookup? From an ergonomics perspective, a Map is preferable to using an object as a map

Copy link
Collaborator

@grahamlangford grahamlangford Sep 26, 2024

Choose a reason for hiding this comment

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

If you intend the data to never be stored in Redux, then I can withdraw my objection.

My understanding was the intention of these selectors in particular was to serve as a stand-in for the eventual redux migration. Since we can't store Maps, we would have to rewrite both the selector and the consumers. Not an issue if the selector will always contain the logic of creating the Map

Copy link
Contributor Author

@twschiller twschiller Sep 26, 2024

Choose a reason for hiding this comment

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

The redux state will be an array of ModInstances which are serializable. And there can be a selector for that.

But business logic that uses the slice has a need to be able to get the ModInstance for a given mod id.

So it's useful to also have a selector that returns a lookup. And it's useful to memoize that since multiple components on the screen may be performing the lookup. Our code on main has to perform a lot of O(n) lookups of mod components

There's also a hook if the users needs a specific mod id (or undefined) vs. needing the map. Some of the activation methods (e.g., useActivateMod) and mods screen method need the Map because the returned callbacks take mod id as an argument. So we can't use the hook in those cases

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agreed. I'm fine with the Map in that case. Just wanted to call attention in case the plan was to store the Map

Copy link
Contributor Author

@twschiller twschiller Sep 26, 2024

Choose a reason for hiding this comment

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

As you mention, when we go to migrate the redux state, there will be a question of storing as an array vs. object keyed by mod registry id. My general feeling is array works best since the lookups would generally be by:

  • all mod instances
  • registry id
  • deployment id

But keep as array leaves open if we ever want to support multiple mod instances for a given mod id. And for these lookups we can just use reselect

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

Successfully merging this pull request may close these issues.

2 participants