Skip to content

Commit

Permalink
Split up state controller
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller committed Sep 21, 2024
1 parent 1c30280 commit ef073a8
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 166 deletions.
8 changes: 4 additions & 4 deletions src/background/stateControllerListeners.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@
import { modComponentRefFactory } from "@/testUtils/factories/modComponentFactories";
import {
getState,
registerModVariables,
setState,
TEST_resetState,
} from "@/contentScript/stateController";
TEST_resetStateController,
} from "@/contentScript/stateController/stateController";
import { MergeStrategies, StateNamespaces } from "@/platform/state/stateTypes";
import type { JSONSchema7Definition } from "json-schema";
import {
deleteSynchronizedModVariablesForMod,
deleteSynchronizedModVariablesForTab,
} from "@/background/stateControllerListeners";
import { getThisFrame } from "webext-messenger";
import { registerModVariables } from "@/contentScript/stateController/modVariablePolicyController";

beforeEach(async () => {
await TEST_resetState();
await TEST_resetStateController();
});

describe("stateControllerListeners", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/background/stateControllerListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { tryParseSessionStorageKey } from "@/platform/state/sessionStateHelpers";
import { tryParseSessionStorageKey } from "@/platform/state/stateHelpers";
import type { RegistryId } from "@/types/registryTypes";

/**
Expand Down
4 changes: 2 additions & 2 deletions src/bricks/effects/pageState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import { unsafeAssumeValidArg } from "@/runtime/runtimeTypes";
import { brickOptionsFactory } from "@/testUtils/factories/runtimeFactories";
import { toExpression } from "@/utils/expressionUtils";
import { GetPageState, SetPageState } from "@/bricks/effects/pageState";
import { TEST_resetState } from "@/contentScript/stateController";
import { TEST_resetStateController } from "@/contentScript/stateController/stateController";
import { MergeStrategies, StateNamespaces } from "@/platform/state/stateTypes";

beforeEach(async () => {
await TEST_resetState();
await TEST_resetStateController();
});

describe("@pixiebrix/state/get", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/bricks/renderers/customForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ import { templates } from "@/components/formBuilder/RjsfTemplates";
import { toExpression } from "@/utils/expressionUtils";
import { unsafeAssumeValidArg } from "@/runtime/runtimeTypes";
import {
TEST_resetState,
TEST_resetStateController,
getState,
setState,
} from "@/contentScript/stateController";
} from "@/contentScript/stateController/stateController";
import type { Target } from "@/types/messengerTypes";
import { StateNamespaces } from "@/platform/state/stateTypes";

Expand Down Expand Up @@ -225,7 +225,7 @@ describe("form data normalization", () => {

describe("CustomFormRenderer", () => {
beforeEach(async () => {
await TEST_resetState();
await TEST_resetStateController();
jest.clearAllMocks();
});

Expand Down
4 changes: 2 additions & 2 deletions src/contentScript/contentScriptPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ import {
} from "@/background/messenger/api";
import {
getState,
registerModVariables,
setState,
} from "@/contentScript/stateController";
} from "@/contentScript/stateController/stateController";
import quickBarRegistry from "@/components/quickBar/quickBarRegistry";
import { expectContext } from "@/utils/expectContext";
import type { PlatformCapability } from "@/platform/capabilities";
Expand Down Expand Up @@ -62,6 +61,7 @@ import { InteractiveLoginRequiredError } from "@/errors/authErrors";
import { deferLogin } from "@/contentScript/integrations/deferredLoginController";
import { selectionMenuActionRegistry } from "@/contentScript/textSelectionMenu/selectionMenuController";
import { getExtensionVersion } from "@/utils/extensionUtils";
import { registerModVariables } from "@/contentScript/stateController/modVariablePolicyController";

/**
* @file Platform definition for mods running in a content script
Expand Down
5 changes: 4 additions & 1 deletion src/contentScript/messenger/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import {
import { getProcesses, initRobot } from "@/contentScript/uipath";
import { checkAvailable } from "@/bricks/available";
import notify from "@/utils/notify";
import { getState, setState } from "@/contentScript/stateController";
import {
getState,
setState,
} from "@/contentScript/stateController/stateController";
import {
cancelTemporaryPanels,
getPanelDefinition,
Expand Down
139 changes: 139 additions & 0 deletions src/contentScript/stateController/modVariablePolicyController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import type { RegistryId } from "@/types/registryTypes";
import type { ModVariablesDefinition } from "@/types/modDefinitionTypes";
import {
StateNamespaces,
SyncPolicies,
type SyncPolicy,
} from "@/platform/state/stateTypes";
import { BusinessError } from "@/errors/businessErrors";
import { isEmpty, pickBy } from "lodash";
import { tryParseSessionStorageKey } from "@/platform/state/stateHelpers";
import type { Nullishable } from "@/utils/nullishUtils";
import { emptyModVariablesDefinitionFactory } from "@/utils/modUtils";
import type { Storage } from "webextension-polyfill";
import { dispatchStateChangeEvent } from "@/contentScript/stateController/stateEventHelpers";

const SCHEMA_POLICY_PROP = "x-sync-policy";

/**
* Map from mod variable name to its synchronization policy.
* Excludes variables with SyncPolicies.NONE.
*/
type VariableSyncPolicyMapping = Record<
string,
Exclude<SyncPolicy, typeof SyncPolicies.NONE>
>;

/**
* Map from mod id to its variable synchronization policy.
* @since 2.1.3
*/
export const modSyncPolicies = new Map<RegistryId, VariableSyncPolicyMapping>();

export function getSyncedVariableNames(modId: RegistryId) {
const modPolicy = modSyncPolicies.get(modId) ?? {};

const tabVariableNames = Object.keys(
pickBy(modPolicy ?? {}, (x) => x === SyncPolicies.TAB),
);
const sessionVariableNames = Object.keys(
pickBy(modPolicy ?? {}, (x) => x === SyncPolicies.SESSION),
);
const syncedVariableNames = [...tabVariableNames, ...sessionVariableNames];

return {
tab: tabVariableNames,
session: sessionVariableNames,
synced: syncedVariableNames,
};
}

export function mapModVariablesToModSyncPolicy(
variables: ModVariablesDefinition,
): VariableSyncPolicyMapping {
return Object.fromEntries(
Object.entries(variables.schema.properties ?? {})
.map(([key, definition]) => {
// eslint-disable-next-line security/detect-object-injection -- constant
const variablePolicy = (definition as UnknownObject)[
SCHEMA_POLICY_PROP
] as SyncPolicy | undefined;

if (variablePolicy && variablePolicy !== SyncPolicies.NONE) {
if (
![SyncPolicies.SESSION, SyncPolicies.TAB].includes(variablePolicy)
) {
throw new BusinessError(
`Unsupported sync policy: ${variablePolicy}`,
);
}

return [
key,
variablePolicy as Exclude<SyncPolicy, typeof SyncPolicies.NONE>,
];
}

return null;
})
.filter((x) => x != null),
) as VariableSyncPolicyMapping;
}

// Keep as separate method so it's safe to call addListener multiple times with the listener
function onSessionStorageChange(
change: Record<string, Storage.StorageChange>,
areaName: string,
): void {
if (areaName === "session") {
for (const key of Object.keys(change)) {
const slot = tryParseSessionStorageKey(key);

if (slot) {
dispatchStateChangeEvent({
namespace: StateNamespaces.MOD,
blueprintId: slot.modId,
});
}
}
}
}

/**
* Register variables and their synchronization policy for a mod.
* @param modId the mod registry id
* @param variables the mod variables definition containing their synchronization policy. If nullish, a blank policy
* is registered.
* @see emptyModVariablesDefinitionFactory
*/
export function registerModVariables(
modId: RegistryId,
variables: Nullishable<ModVariablesDefinition>,
): void {
const modSyncPolicy = mapModVariablesToModSyncPolicy(
variables ?? emptyModVariablesDefinitionFactory(),
);
modSyncPolicies.set(modId, modSyncPolicy);

// If any variables are set to sync, listen for changes to session storage to notify the mods running on this page
if (!isEmpty(modSyncPolicy)) {
browser.storage.onChanged.addListener(onSessionStorageChange);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@

import {
setState,
registerModVariables,
getState,
TEST_resetState,
} from "@/contentScript/stateController";
TEST_resetStateController,
} from "@/contentScript/stateController/stateController";
import { modComponentRefFactory } from "@/testUtils/factories/modComponentFactories";
import {
MergeStrategies,
STATE_CHANGE_JS_EVENT_TYPE,
StateNamespaces,
} from "@/platform/state/stateTypes";
import type { JSONSchema7Definition } from "json-schema";
import { registerModVariables } from "@/contentScript/stateController/modVariablePolicyController";

beforeEach(async () => {
await TEST_resetState();
await TEST_resetStateController();
});

describe("pageState", () => {
Expand Down
Loading

0 comments on commit ef073a8

Please sign in to comment.