Files
element-web/src/modules/ModuleRunner.ts
Thor Arne Johansen 6392759bec Replace SecurityCustomisations with CryptoSetupExtension (#12342)
* Changed call sites from customisations/security to ModuleRunner.extensions

* Updated depenndecy and added tests

* Fixed style and formatting with prettier

* Fix according to Element PR comments

* Fixing issues raised in PR review

* Removed commented code. Improved encapsulation. Removed noisy logging

* Improved language of comment about calling the factory

* Refactor to get better encapsulation

* Find a better name. Provide explicit reset function. Provide more TSDoc

* Simplify mock for cryptoSetup, and add assertion for exception message.

* Remove unused className property. Adjust TSDoc comments

* Fix linting  and code style issues

* Added test to ensure we canregister anduse experimental extensions

* Fix linting and code-style issues

* Added test to ensure only on registration of experimental extensions

* Added test toensure call to getDehydratedDeviceCallback()

* Test what happens when there is no implementation

* Iterating cryptoSetup tests

* Lint/prettier fix

* Assert both branches when checking for dehydrationkey callback

* Update src/modules/ModuleRunner.ts

Language and formatting

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Reset by setting a fresh ExtensionsManager

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Use regular comment instead of TSDoc style comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update test/MatrixClientPeg-test.ts

No need to extend the base class

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Fix spelling

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Fix spelling

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/modules/ModuleRunner.ts

Fix TSDoc formatting

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Simplify mock setup

* Simplified mock and cleaned up a bit

* Keeping track of extensions is an implementation detail internal to ExtensionsManager.  Language and punctuation

* Addressed issues and comments from PR review

* Update src/modules/ModuleRunner.ts

Keep the flags to track implementations as direct properties

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Fix flattening of implementation map

* Update src/modules/ModuleRunner.ts

Fix whitespace

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-04-12 15:15:17 +00:00

194 lines
6.5 KiB
TypeScript

/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { safeSet } from "matrix-js-sdk/src/utils";
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
import { AnyLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/types";
import {
DefaultCryptoSetupExtensions,
ProvideCryptoSetupExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
import {
DefaultExperimentalExtensions,
ProvideExperimentalExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
import { AppModule } from "./AppModule";
import { ModuleFactory } from "./ModuleFactory";
import "./ModuleComponents";
/**
* Handles and manages extensions provided by modules.
*/
class ExtensionsManager {
// Private backing fields for extensions
private cryptoSetupExtension: ProvideCryptoSetupExtensions;
private experimentalExtension: ProvideExperimentalExtensions;
/** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */
private hasDefaultCryptoSetupExtension = true;
/** `true` if `experimentalExtension` is the default implementation; `false` if it is implemented by a module. */
private hasDefaultExperimentalExtension = true;
/**
* Create a new instance.
*/
public constructor() {
// Set up defaults
this.cryptoSetupExtension = new DefaultCryptoSetupExtensions();
this.experimentalExtension = new DefaultExperimentalExtensions();
}
/**
* Provides a crypto setup extension.
*
* @returns The registered extension. If no module provides this extension, a default implementation is returned.
*/
public get cryptoSetup(): ProvideCryptoSetupExtensions {
return this.cryptoSetupExtension;
}
/**
* Provides an experimental extension.
*
* @remarks
* This method extension is provided to simplify experimentation and development, and is not intended for production code.
*
* @returns The registered extension. If no module provides this extension, a default implementation is returned.
*/
public get experimental(): ProvideExperimentalExtensions {
return this.experimentalExtension;
}
/**
* Add any extensions provided by the module.
*
* @param module - The appModule to check for extensions.
*
* @throws if an extension is provided by more than one module.
*/
public addExtensions(module: AppModule): void {
const runtimeModule = module.module;
/* Add the cryptoSetup extension if any */
if (runtimeModule.extensions?.cryptoSetup) {
if (this.hasDefaultCryptoSetupExtension) {
this.cryptoSetupExtension = runtimeModule.extensions?.cryptoSetup;
this.hasDefaultCryptoSetupExtension = false;
} else {
throw new Error(
`adding cryptoSetup extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
);
}
}
/* Add the experimental extension if any */
if (runtimeModule.extensions?.experimental) {
if (this.hasDefaultExperimentalExtension) {
this.experimentalExtension = runtimeModule.extensions?.experimental;
this.hasDefaultExperimentalExtension = false;
} else {
throw new Error(
`adding experimental extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
);
}
}
}
}
/**
* Handles and coordinates the operation of modules.
*/
export class ModuleRunner {
public static readonly instance = new ModuleRunner();
private extensionsManager = new ExtensionsManager();
private modules: AppModule[] = [];
private constructor() {
// we only want one instance
}
/**
* Exposes all extensions which may be overridden/provided by modules.
*
* @returns An `ExtensionsManager` which exposes the extensions.
*/
public get extensions(): ExtensionsManager {
return this.extensionsManager;
}
/**
* Resets the runner, clearing all known modules, and all extensions
*
* Intended for test usage only.
*/
public reset(): void {
this.modules = [];
this.extensionsManager = new ExtensionsManager();
}
/**
* All custom translations from all registered modules.
*/
public get allTranslations(): TranslationStringsObject {
const merged: TranslationStringsObject = {};
for (const module of this.modules) {
const i18n = module.api.translations;
if (!i18n) continue;
for (const [lang, strings] of Object.entries(i18n)) {
safeSet(merged, lang, merged[lang] || {});
for (const [str, val] of Object.entries(strings)) {
safeSet(merged[lang], str, val);
}
}
}
return merged;
}
/**
* Registers a factory which creates a module for later loading. The factory
* will be called immediately.
* @param factory The module factory.
*/
public registerModule(factory: ModuleFactory): void {
const appModule = new AppModule(factory);
this.modules.push(appModule);
// Check if the new module provides any extensions, and also ensure a given extension is only provided by a single runtime module.
this.extensionsManager.addExtensions(appModule);
}
/**
* Invokes a lifecycle event, notifying registered modules.
* @param lifecycleEvent The lifecycle event.
* @param args The arguments for the lifecycle event.
*/
public invoke(lifecycleEvent: AnyLifecycle, ...args: any[]): void {
for (const module of this.modules) {
module.module.emit(lifecycleEvent, ...args);
}
}
}