Skip to content

Commit

Permalink
Slice 2: Use ModInstance type in mod deployment code (2/3) (#9190)
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller authored Oct 8, 2024
1 parent f75a3d1 commit b0027a6
Show file tree
Hide file tree
Showing 19 changed files with 401 additions and 327 deletions.
83 changes: 44 additions & 39 deletions src/background/deploymentUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { checkDeploymentPermissions } from "@/permissions/deploymentPermissionsH
import { emptyPermissionsFactory } from "@/permissions/permissionsUtils";
import { TEST_setContext } from "webext-detect";
import {
activatedModComponentFactory,
modComponentFactory,
modMetadataFactory,
} from "@/testUtils/factories/modComponentFactories";
Expand Down Expand Up @@ -376,29 +377,31 @@ describe("syncDeployments", () => {
isLinkedMock.mockResolvedValue(true);

const starterBrick = starterBrickDefinitionFactory();
const brick = {
const packageVersion = {
...parsePackage(starterBrick as unknown as RegistryPackage),
timestamp: new Date(),
};
registryFindMock.mockResolvedValue(brick);
registryFindMock.mockResolvedValue(packageVersion);

// An extension without a recipe. Exclude _recipe entirely to handle the case where the property is missing
const modComponent = modComponentFactory({
// A mod without a deployment. Exclude _deployment entirely to handle the case where the property is missing
const manualModComponent = activatedModComponentFactory({
extensionPointId: starterBrick.metadata!.id,
}) as ActivatedModComponent;
delete modComponent._recipe;
delete modComponent._deployment;
_recipe: modMetadataFactory(),
});
delete manualModComponent._deployment;

await saveModComponentState({
activatedModComponents: [modComponent],
activatedModComponents: [manualModComponent],
});

let editorState = initialEditorState;
const { fromModComponent } = adapter(starterBrick.definition.type);
const element = (await fromModComponent(modComponent)) as ButtonFormState;
const editorComponentFormState = (await fromModComponent(
manualModComponent,
)) as ButtonFormState;
editorState = editorSlice.reducer(
editorState,
editorSlice.actions.addModComponentFormState(element),
editorSlice.actions.addModComponentFormState(editorComponentFormState),
);
await saveEditorState(editorState);

Expand Down Expand Up @@ -749,58 +752,58 @@ describe("syncDeployments", () => {
});

test("can deactivate all deployed mods", async () => {
const personalStarterBrick = starterBrickDefinitionFactory();
const personalBrick = {
...parsePackage(personalStarterBrick as unknown as RegistryPackage),
const manualModStarterBrickDefinition = starterBrickDefinitionFactory();
const manualModStarterBrickPackageVersion = {
...parsePackage(
manualModStarterBrickDefinition as unknown as RegistryPackage,
),
timestamp: new Date(),
};

const standaloneModComponent = modComponentFactory({
extensionPointId: personalStarterBrick.metadata!.id,
}) as ActivatedModComponent;

const recipeModComponent = modComponentFactory({
const manuallyActivatedModComponent = activatedModComponentFactory({
_recipe: modMetadataFactory(),
}) as ActivatedModComponent;
});

const deploymentStarterBrick = starterBrickDefinitionFactory();
const deploymentsBrick = {
...parsePackage(deploymentStarterBrick as unknown as RegistryPackage),
const deploymentStarterBrickDefinition = starterBrickDefinitionFactory();
const deploymentStarterBrickPackageVersion = {
...parsePackage(
deploymentStarterBrickDefinition as unknown as RegistryPackage,
),
timestamp: new Date(),
};

const deploymentModComponent = modComponentFactory({
extensionPointId: deploymentStarterBrick.metadata!.id,
extensionPointId: deploymentStarterBrickDefinition.metadata!.id,
_deployment: { id: uuidv4(), timestamp: "2021-10-07T12:52:16.189Z" },
_recipe: modMetadataFactory(),
}) as ActivatedModComponent;

registryFindMock.mockImplementation(async (id) => {
if (id === personalBrick.id) {
return personalBrick;
if (id === manualModStarterBrickPackageVersion.id) {
return manualModStarterBrickPackageVersion;
}

return deploymentsBrick;
return deploymentStarterBrickPackageVersion;
});

let editorState = initialEditorState;

const personalModComponentAdapter = adapter(
personalStarterBrick.definition.type,
const manualModAdapter = adapter(
manualModStarterBrickDefinition.definition.type,
);
const personalModComponentFormState =
(await personalModComponentAdapter.fromModComponent(
standaloneModComponent,
const manualModComponentEditorFormState =
(await manualModAdapter.fromModComponent(
manuallyActivatedModComponent,
)) as ButtonFormState;
editorState = editorSlice.reducer(
editorState,
editorSlice.actions.addModComponentFormState(
personalModComponentFormState,
manualModComponentEditorFormState,
),
);

const deploymentModComponentAdapter = adapter(
deploymentStarterBrick.definition.type,
deploymentStarterBrickDefinition.definition.type,
);
const deploymentElement =
(await deploymentModComponentAdapter.fromModComponent(
Expand All @@ -813,9 +816,8 @@ describe("syncDeployments", () => {

await saveModComponentState({
activatedModComponents: [
standaloneModComponent,
deploymentModComponent,
recipeModComponent,
manuallyActivatedModComponent,
],
});
await saveEditorState(editorState);
Expand All @@ -827,15 +829,18 @@ describe("syncDeployments", () => {

const { activatedModComponents } = await getModComponentState();

expect(activatedModComponents).toHaveLength(2);
expect(activatedModComponents).toHaveLength(1);

const activatedModComponentIds = activatedModComponents.map((x) => x.id);
expect(activatedModComponentIds).toContain(standaloneModComponent.id);
expect(activatedModComponentIds).toContain(recipeModComponent.id);
expect(activatedModComponentIds).toContain(
manuallyActivatedModComponent.id,
);

const { modComponentFormStates } = (await getEditorState()) ?? {};
expect(modComponentFormStates).toBeArrayOfSize(1);
expect(modComponentFormStates![0]!).toEqual(personalModComponentFormState);
expect(modComponentFormStates![0]!).toEqual(
manualModComponentEditorFormState,
);
});

test("deactivates old mod when deployed mod id is changed", async () => {
Expand Down
76 changes: 43 additions & 33 deletions src/background/deploymentUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
*/

import { type Deployment } from "@/types/contract";
import { isEmpty } from "lodash";
import { uniq } from "lodash";
import reportError from "@/telemetry/reportError";
import { getUUID } from "@/telemetry/telemetryHelpers";
import { isLinked, readAuthData, updateUserData } from "@/auth/authStorage";
import reportEvent from "@/telemetry/reportEvent";
import { refreshRegistries } from "@/hooks/useRefreshRegistries";
import { selectActivatedModComponents } from "@/store/modComponents/modComponentSelectors";
import { maybeGetLinkedApiClient } from "@/data/service/apiClient";
import { getExtensionVersion } from "@/utils/extensionUtils";
import { parse as parseSemVer, satisfies, type SemVer } from "semver";
Expand All @@ -40,7 +39,7 @@ import {
findLocalDeploymentConfiguredIntegrationDependencies,
makeUpdatedFilter,
mergeDeploymentIntegrationDependencies,
selectInstalledDeployments,
selectActivatedDeployments,
} from "@/utils/deploymentUtils";
import { selectUpdatePromptState } from "@/store/settings/settingsSelectors";
import settingsSlice from "@/store/settings/settingsSlice";
Expand All @@ -66,10 +65,11 @@ import { SessionValue } from "@/mv3/SessionStorage";
import { FeatureFlags } from "@/auth/featureFlags";
import { API_PATHS } from "@/data/service/urlPaths";
import deactivateMod from "@/background/utils/deactivateMod";
import deactivateModComponentsAndSaveState from "@/background/utils/deactivateModComponentsAndSaveState";
import deactivateModInstancesAndSaveState from "@/background/utils/deactivateModInstancesAndSaveState";
import saveModComponentStateAndReloadTabs, {
type ReloadOptions,
} from "@/background/utils/saveModComponentStateAndReloadTabs";
import { selectModInstances } from "@/store/modComponents/modInstanceSelectors";

// eslint-disable-next-line local-rules/persistBackgroundData -- Static
const { reducer: modComponentReducer, actions: modComponentActions } =
Expand Down Expand Up @@ -107,29 +107,31 @@ export async function deactivateAllDeployedMods(): Promise<void> {
getModComponentState(),
getEditorState(),
]);
const activatedModComponents = selectActivatedModComponents({
const modInstances = selectModInstances({
options: modComponentState,
});

const modComponentsToDeactivate = activatedModComponents.filter(
(activatedModComponent) => !isEmpty(activatedModComponent._deployment),
const modsToDeactivate = modInstances.filter(
(x) => x.deploymentMetadata != null,
);

if (modComponentsToDeactivate.length === 0) {
if (modsToDeactivate.length === 0) {
// Short-circuit to skip reporting telemetry
return;
}

await deactivateModComponentsAndSaveState(modComponentsToDeactivate, {
await deactivateModInstancesAndSaveState(modsToDeactivate, {
editorState,
modComponentState,
});

reportEvent(Events.DEPLOYMENT_DEACTIVATE_ALL, {
auto: true,
deployments: modComponentsToDeactivate
.map((x) => x._deployment?.id)
.filter((x) => x != null),
deployments: uniq(
modsToDeactivate
.map((x) => x.deploymentMetadata?.id)
.filter((x) => x != null),
),
});
}

Expand All @@ -140,36 +142,37 @@ async function deactivateUnassignedDeployments(
getModComponentState(),
getEditorState(),
]);
const activatedModComponents = selectActivatedModComponents({
const allModInstances = selectModInstances({
options: modComponentState,
});

const deployedModIds = new Set(
assignedDeployments.map((deployment) => deployment.package.package_id),
);

const unassignedModComponents = activatedModComponents.filter(
(activatedModComponent) =>
!isEmpty(activatedModComponent._deployment) &&
activatedModComponent._recipe?.id &&
!deployedModIds.has(activatedModComponent._recipe.id),
const unassignedModInstances = allModInstances.filter(
(modInstance) =>
modInstance.deploymentMetadata != null &&
!deployedModIds.has(modInstance.definition.metadata.id),
);

if (unassignedModComponents.length === 0) {
if (unassignedModInstances.length === 0) {
// Short-circuit to skip reporting telemetry
return;
}

await deactivateModComponentsAndSaveState(unassignedModComponents, {
await deactivateModInstancesAndSaveState(unassignedModInstances, {
editorState,
modComponentState,
});

reportEvent(Events.DEPLOYMENT_DEACTIVATE_UNASSIGNED, {
auto: true,
deployments: unassignedModComponents
.map((x) => x._deployment?.id)
.filter((x) => x != null),
deployments: uniq(
unassignedModInstances
.map((x) => x.deploymentMetadata?.id)
.filter((x) => x != null),
),
});
}

Expand All @@ -189,12 +192,15 @@ async function activateDeployment({
let _editorState = editorState;
const { deployment, modDefinition } = activatableDeployment;

const isAlreadyActivated = optionsState.activatedModComponents.some(
(activatedModComponent) =>
activatedModComponent._deployment?.id === deployment.id,
const modInstances = selectModInstances({
options: _optionsState,
});

const isAlreadyActivated = modInstances.some(
(modInstance) => modInstance.deploymentMetadata?.id === deployment.id,
);

// Deactivate existing mod component versions
// Deactivate any existing mod instance corresponding to the deployed package
const result = deactivateMod(deployment.package.package_id, {
modComponentState: _optionsState,
editorState: _editorState,
Expand Down Expand Up @@ -303,9 +309,11 @@ async function selectUpdatedDeployments(
deployments: Deployment[],
{ restricted }: { restricted: boolean },
): Promise<Deployment[]> {
// Always get the freshest options slice from the local storage
const { activatedModComponents } = await getModComponentState();
const updatePredicate = makeUpdatedFilter(activatedModComponents, {
// Always get the freshest data from the local storage
const modInstances = selectModInstances({
options: await getModComponentState(),
});
const updatePredicate = makeUpdatedFilter(modInstances, {
restricted,
});
return deployments.filter((deployment) => updatePredicate(deployment));
Expand Down Expand Up @@ -405,8 +413,10 @@ export async function syncDeployments(): Promise<void> {
return;
}

// Always get the freshest options slice from the local storage
const { activatedModComponents } = await getModComponentState();
// Always get the freshest data from the local storage
const modInstances = selectModInstances({
options: await getModComponentState(),
});

// This is the "heartbeat". The old behavior was to only send if the user had at least one deployment activated.
// Now we're always sending in order to help team admins understand any gaps between number of registered users
Expand Down Expand Up @@ -449,7 +459,7 @@ export async function syncDeployments(): Promise<void> {
{
uid: await getUUID(),
version: getExtensionVersion(),
active: selectInstalledDeployments(activatedModComponents),
active: selectActivatedDeployments(modInstances),
campaignIds,
},
);
Expand Down
Loading

0 comments on commit b0027a6

Please sign in to comment.