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

Merged
merged 12 commits into from
Oct 7, 2024

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  120 passed
flaky  14 flaky
skipped  4 skipped

Details

report  Open report ↗︎
stats  138 tests across 45 suites
duration  45 minutes, 23 seconds
commit  b4faee6
info  For more information on how to debug and view this report, see our readme

Flaky tests

chrome-setup › setup/unaffiliated.setup.ts › authenticate with unaffiliated user
msedge › tests/deployments/deploymentActivation.spec.ts › activate a deployed mod in the extension console
chrome › tests/pageEditor/addStarterBrick.spec.ts › Add new mod with different starter brick components
chrome › tests/pageEditor/addStarterBrick.spec.ts › Add starter brick to mod
msedge › tests/pageEditor/addStarterBrick.spec.ts › Add new mod with different starter brick components
msedge › tests/pageEditor/addStarterBrick.spec.ts › Add starter brick to mod
msedge › tests/pageEditor/brickActions.spec.ts › brick actions panel behavior
msedge › tests/pageEditor/modEditorPane.spec.ts › mod editor pane behavior
chrome › tests/runtime/insertAtCursor.spec.ts › Insert at Cursor › 8157: can insert at cursor from side bar
msedge › tests/runtime/insertAtCursor.spec.ts › Insert at Cursor › 8157: can insert at cursor from side bar
chrome › tests/runtime/setInputValue.spec.ts › can set input value
chrome › tests/runtime/sidebar/sidebarController.spec.ts › sidebar controller › shows focus dialog in top-level frame
msedge › tests/smoke/floatingActionButton.spec.ts › sidebar page smoke test › can hide the floating action button
msedge › tests/workshop/createMod.spec.ts › can create a new mod from a yaml definition and update it

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)

Copy link
Collaborator

@fungairino fungairino Oct 7, 2024

Choose a reason for hiding this comment

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

In addition, this is expected to be a non-empty array, right? We should at least update the comment with that note, or see how much effort it would be to enforce a non-empty array type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's non-empty. The stronger invariant is that it needs to match the length of definition.extensionPoints. So, in the future we might consider reshaping this type to ensure that matches. But that would involve enriching/reshaping the definition. Right now my preference is to reuse the ModDefinition type for the definition prop

@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 91.94631% with 12 lines in your changes missing coverage. Please review.

Project coverage is 74.99%. Comparing base (8318d74) to head (b4faee6).
Report is 343 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.99%   +0.74%     
==========================================
  Files        1332     1372      +40     
  Lines       40817    42427    +1610     
  Branches     7634     7925     +291     
==========================================
+ Hits        30306    31817    +1511     
- Misses      10511    10610      +99     

☔ 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
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

Comment on lines +66 to +67
const firstComponent = modComponents[0];
assertNotNullish(firstComponent, "activatedModComponents is empty");
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: there's a very simple type definition we can write for a non-empty array to avoid needing a run-time check here:

type NonEmptyArray<T> = [T, ...T[]];

This would mean doing some more type-checking upstream though

Copy link
Contributor Author

@twschiller twschiller Oct 7, 2024

Choose a reason for hiding this comment

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

Cool - I'd vote we leave for future DevEx improvement since it's easy to layer on later. See Typefest discussion: sindresorhus/type-fest#476

Comment on lines +96 to +98
if (modComponent._recipe?.id !== modMetadata.id) {
throw new Error("Mod component does not match mod metadata");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: After doing this runtime check, we could refine the type definition if we wanted to.

type ArrayWithSameId<T extends { id: unknown }> = T extends { id: infer IdType }
  ? (T & { id: IdType })[]
  : never;

Copy link
Contributor Author

@twschiller twschiller Oct 7, 2024

Choose a reason for hiding this comment

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

I'm not sure what type you're planning on refining? Once the mod components have been grouped into a ModInstance there's no data duplication for that invariant to describe

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh yup, I was just thinking about how to describe this runtime check as a type enforcement but in this case it's not as useful.

Copy link

github-actions bot commented Oct 7, 2024

No loom links were found in the first post. Please add one there if you'd like to it to appear on Slack.

Do not edit this comment manually.

@twschiller twschiller added this to the 2.1.4 milestone Oct 7, 2024
@twschiller twschiller enabled auto-merge (squash) October 7, 2024 19:21
@twschiller twschiller merged commit ba1e9da into main Oct 7, 2024
29 checks passed
@twschiller twschiller deleted the feature/devex-mod-instance branch October 7, 2024 19:33
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.

3 participants