Skip to content

Commit

Permalink
Use mod instance in marketplace mod updater
Browse files Browse the repository at this point in the history
Remove test debugging statement
  • Loading branch information
twschiller committed Sep 23, 2024
1 parent 55eabff commit 56f447b
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 178 deletions.
27 changes: 17 additions & 10 deletions src/background/deploymentUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ import deactivateModInstancesAndSaveState from "@/background/utils/deactivateMod
import saveModComponentStateAndReloadTabs, {
type ReloadOptions,
} from "@/background/utils/saveModComponentStateAndReloadTabs";
import { selectModInstances } from "@/store/modComponents/modInstanceSelectors";
import {
selectModInstanceMap,
selectModInstances,
} from "@/store/modComponents/modInstanceSelectors";

// eslint-disable-next-line local-rules/persistBackgroundData -- Static
const { reducer: modComponentReducer, actions: modComponentActions } =
Expand Down Expand Up @@ -192,22 +195,26 @@ async function activateDeployment({
let _editorState = editorState;
const { deployment, modDefinition } = activatableDeployment;

const modInstances = selectModInstances({
const modInstanceMap = selectModInstanceMap({
options: _optionsState,
});

const isAlreadyActivated = modInstances.some(
// Check if the deployment is already activated, regardless of the package id
const isAlreadyActivated = [...modInstanceMap.values()].some(
(modInstance) => modInstance.deploymentMetadata?.id === deployment.id,
);

// Deactivate any existing mod instance corresponding to the deployed package
const result = deactivateMod(deployment.package.package_id, {
modComponentState: _optionsState,
editorState: _editorState,
});
// Deactivate any existing mod instance corresponding to the deployed package, regardless of deployment
const packageModInstance = modInstanceMap.get(deployment.package.package_id);
if (packageModInstance) {
const result = deactivateMod(packageModInstance, {
modComponentState: _optionsState,
editorState: _editorState,
});

_optionsState = result.modComponentState;
_editorState = result.editorState;
_optionsState = result.modComponentState;
_editorState = result.editorState;
}

// Activate the deployed mod with the service definition
_optionsState = modComponentReducer(
Expand Down
36 changes: 0 additions & 36 deletions src/background/modUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,42 +143,6 @@ describe("getActivatedMarketplaceModVersions function", () => {
},
]);
});

it("reports error if multiple mod component versions activated for same mod", async () => {
const sameMod = modMetadataFactory({
sharing: publicSharingDefinitionFactory(),
});

const onePublicActivatedMod = activatedModComponentFactory({
_recipe: sameMod,
});

const sameModDifferentVersion = modMetadataFactory({
...sameMod,
version: "2.0.0" as SemVerString,
});

const anotherPublicActivatedMod = activatedModComponentFactory({
_recipe: sameModDifferentVersion,
});

await saveModComponentState({
activatedModComponents: [
onePublicActivatedMod,
anotherPublicActivatedMod,
],
});

const result = await getActivatedMarketplaceModVersions();

expect(result).toEqual([
{
name: onePublicActivatedMod!._recipe!.id,
version: onePublicActivatedMod!._recipe!.version,
},
]);
expect(reportError).toHaveBeenCalled();
});
});

describe("fetchModUpdates function", () => {
Expand Down
96 changes: 36 additions & 60 deletions src/background/modUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,21 @@ import {
import type { RegistryId, SemVerString } from "@/types/registryTypes";
import type { ModDefinition } from "@/types/modDefinitionTypes";
import modComponentSlice from "@/store/modComponents/modComponentSlice";
import { groupBy, isEmpty, uniq } from "lodash";
import { isEmpty } from "lodash";
import { queueReloadModEveryTab } from "@/contentScript/messenger/api";
import { getEditorState, saveEditorState } from "@/store/editorStorage";
import type { EditorState } from "@/pageEditor/store/editor/pageEditorTypes";
import type { ActivatedModComponent } from "@/types/modComponentTypes";
import { collectModOptions } from "@/store/modComponents/modComponentUtils";
import type { ModComponentState } from "@/store/modComponents/modComponentTypes";
import { uninstallContextMenu } from "@/background/contextMenus/uninstallContextMenu";
import collectExistingConfiguredDependenciesForMod from "@/integrations/util/collectExistingConfiguredDependenciesForMod";
import { flagOn } from "@/auth/featureFlagStorage";
import { assertNotNullish } from "@/utils/nullishUtils";
import { FeatureFlags } from "@/auth/featureFlags";
import { API_PATHS } from "@/data/service/urlPaths";
import deactivateMod from "@/background/utils/deactivateMod";
import {
selectModInstanceMap,
selectModInstances,
} from "@/store/modComponents/modInstanceSelectors";

const UPDATE_INTERVAL_MS = 10 * 60 * 1000;

Expand All @@ -62,41 +63,21 @@ type PackageVersionPair = { name: RegistryId; version: SemVerString };
export async function getActivatedMarketplaceModVersions(): Promise<
PackageVersionPair[]
> {
const { activatedModComponents } = await getModComponentState();
const modInstances = selectModInstances({
options: await getModComponentState(),
});

// Typically most Marketplace mods would not be a deployment. If this happens to be the case,
// the deployment updater will handle the updates.
const mods: Array<ActivatedModComponent["_recipe"]> = activatedModComponents
.filter((mod) => mod._recipe?.sharing?.public && !mod._deployment)
.map((mod) => mod._recipe);

const modVersions: PackageVersionPair[] = [];

for (const [name, modComponents] of Object.entries(
groupBy(mods, "id"),
) as Array<[RegistryId, Array<ActivatedModComponent["_recipe"]>]>) {
const uniqueModVersions: SemVerString[] = uniq(
modComponents
.map((modComponent) => modComponent?.version)
.filter((x) => x != null),
);

if (uniqueModVersions.length > 1) {
reportError(
new Error(
`Found multiple mod component versions activated for the same mod: ${name} (${uniqueModVersions.join(
", ",
)})`,
),
);
}

assertNotNullish(uniqueModVersions[0], "Mod component version is required");

modVersions.push({ name, version: uniqueModVersions[0] });
}
const marketplaceModInstances = modInstances.filter(
(mod) => mod.definition.sharing.public && !mod.deploymentMetadata,
);

return modVersions;
return marketplaceModInstances.map(({ definition }) => {
const { id, version } = definition.metadata;
assertNotNullish(version, "Mod component version is required");
return { name: id, version };
});
}

/**
Expand Down Expand Up @@ -146,55 +127,50 @@ export async function fetchModUpdates(): Promise<BackwardsCompatibleUpdate[]> {
*
* The ModComponents will have new UUIDs.
*
* @param modDefinition the mod to update
* @param newModDefinition the mod to update
* @param reduxState the current state of the modComponent and editor redux stores
* @returns new redux state with the mod updated
*/
export function updateMod(
modDefinition: ModDefinition,
newModDefinition: ModDefinition,
{ options: modComponentState, editor: editorState }: ActivatedModState,
): ActivatedModState {
const modInstanceMap = selectModInstanceMap({
options: modComponentState,
});

const modInstance = modInstanceMap.get(newModDefinition.metadata.id);

// Must be present because updateMod is only called when there is an update available for an existing mod
assertNotNullish(modInstance, "Mod instance not found");

console.log({
modComponentState: JSON.stringify(modComponentState, null, 2),
});

const {
modComponentState: nextModComponentState,
editorState: nextEditorState,
// This type is weird, please ignore it for now, we need to clean up a lot of stuff with these
// mod component types. These "deactivated" components are not passed anywhere else, or put into
// redux, or anything like that. They are only used to collect the configured dependencies and the
// mod options in order to re-install the mod (see the calls to collectExistingConfiguredDependenciesForMod
// and collectRecipeOptions immediately following this code).
deactivatedModComponents,
} = deactivateMod(modDefinition.metadata.id, {
} = deactivateMod(modInstance, {
modComponentState,
editorState,
});

for (const deactivatedModComponent of deactivatedModComponents) {
for (const modComponentId of modInstance.modComponentIds) {
// Remove the menu item UI from all mods. We must explicitly remove context menu items because otherwise the user
// will see duplicate menu items because the old/new mod components have different UUIDs.
// `updateMods` calls `queueReloadModEveryTab`. Therefore, if the user clicks on a tab where the new version of the
// mod component is not loaded yet, they'll get a notification to reload the page.
void uninstallContextMenu({ modComponentId: deactivatedModComponent.id });
void uninstallContextMenu({ modComponentId });
}

const configuredDependencies = collectExistingConfiguredDependenciesForMod(
modDefinition,
deactivatedModComponents,
);

const optionsArgs = collectModOptions(
deactivatedModComponents.filter((modComponent) => modComponent.optionsArgs),
);

const finalModComponentState = modComponentSlice.reducer(
nextModComponentState,
modComponentSlice.actions.activateMod({
modDefinition,
configuredDependencies,
optionsArgs,
modDefinition: newModDefinition,
// Activate using the same options/configuration
configuredDependencies: modInstance.integrationsArgs,
optionsArgs: modInstance.optionsArgs,
screen: "background",
isReactivate: true,
}),
Expand All @@ -210,8 +186,8 @@ async function updateMods(modUpdates: BackwardsCompatibleUpdate[]) {
let newOptionsState = await getModComponentState();
let newEditorState = await getEditorState();

for (const { backwards_compatible: update } of modUpdates) {
const { options, editor } = updateMod(update, {
for (const { backwards_compatible: updatedDefinition } of modUpdates) {
const { options, editor } = updateMod(updatedDefinition, {
options: newOptionsState,
editor: newEditorState,
});
Expand Down
70 changes: 16 additions & 54 deletions src/background/utils/deactivateMod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,20 @@ import {
modMetadataFactory,
activatedModComponentFactory,
} from "@/testUtils/factories/modComponentFactories";
import { starterBrickDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories";
import { type ActivatedModComponent } from "@/types/modComponentTypes";
import { type RegistryId } from "@/types/registryTypes";
import { type ModInstance } from "@/types/modInstanceTypes";
import { modInstanceFactory } from "@/testUtils/factories/modInstanceFactories";
import { mapModInstanceToActivatedModComponents } from "@/store/modComponents/modInstanceUtils";

describe("deactivateMod", () => {
let modToDeactivate: ActivatedModComponent["_recipe"];
let modToDeactivate: ModInstance;

beforeEach(async () => {
modToDeactivate = modMetadataFactory({});
const anotherMod = modMetadataFactory({});
modToDeactivate = modInstanceFactory();
const anotherMod = modMetadataFactory();

await saveModComponentState({
activatedModComponents: [
activatedModComponentFactory({
_recipe: modToDeactivate,
}),
activatedModComponentFactory({
_recipe: modToDeactivate,
}),
...mapModInstanceToActivatedModComponents(modToDeactivate),
activatedModComponentFactory({
_recipe: anotherMod,
}),
Expand All @@ -55,50 +50,17 @@ describe("deactivateMod", () => {
const modComponentState = await getModComponentState();
const editorState = await getEditorState();

const {
modComponentState: nextModComponentState,
deactivatedModComponents,
} = deactivateMod(modToDeactivate!.id, {
modComponentState,
editorState,
});

expect(deactivatedModComponents).toHaveLength(2);
expect(deactivatedModComponents[0]!._recipe!.id).toEqual(
modToDeactivate!.id,
);
expect(deactivatedModComponents[1]!._recipe!.id).toEqual(
modToDeactivate!.id,
const { modComponentState: nextModComponentState } = deactivateMod(
modToDeactivate,
{
modComponentState,
editorState,
},
);

expect(nextModComponentState.activatedModComponents).toHaveLength(1);
});

it("should do nothing if mod id does not have any activated mod components", async () => {
const starterBrick = starterBrickDefinitionFactory();
const modComponent = activatedModComponentFactory({
extensionPointId: starterBrick.metadata!.id,
_recipe: modMetadataFactory({}),
});

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

const modComponentState = await getModComponentState();
const editorState = await getEditorState();

const {
modComponentState: nextModComponentState,
deactivatedModComponents,
} = deactivateMod("@test/id-doesnt-exist" as RegistryId, {
modComponentState,
editorState,
});

expect(deactivatedModComponents).toEqual([]);
expect(nextModComponentState.activatedModComponents).toEqual(
modComponentState.activatedModComponents,
);
expect(
nextModComponentState.activatedModComponents[0]!._recipe!.id,
).not.toEqual(modToDeactivate.definition.metadata.id);
});
});
Loading

0 comments on commit 56f447b

Please sign in to comment.