From e9dcdaf522fb974128c6d86df4316c37ec08df46 Mon Sep 17 00:00:00 2001 From: CodexHere Date: Tue, 5 Mar 2024 01:18:50 -0600 Subject: [PATCH 01/17] * Removed lodash.merge in favor of @fastify/deepmerge for speed and size. * Added ability to toggle encrypting of single setting name. This is used by set by name and merge in the Context Provider. --- README.md | 7 ++-- package-lock.json | 31 ++++++++-------- package.json | 4 +-- src/scripts/Managers/SettingsManager.ts | 35 +++++++++++++++---- src/scripts/Managers/TemplateManager.ts | 5 ++- src/scripts/utils/Forms/Builder.ts | 4 +-- .../AbstractFormSchemaProcessor.ts | 6 ++-- .../utils/Forms/SchemaProcessors/Form.ts | 4 +-- .../Grouping/GroupSubSchema.ts | 4 +-- .../SchemaProcessors/Grouping/GroupingBase.ts | 4 +-- .../SchemaProcessors/Grouping/GroupingRow.ts | 4 +-- .../Inputs/CheckedMultiInput.ts | 4 +-- typedoc.json | 8 ++--- 13 files changed, 69 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index c32b268..64a0b20 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## TODO -### Default Settings +### Default Settings Thinkery * Need `RendererInstanceEvents.SETTINGS_STALE` to re-init settings * Necessary for when a Plugin/etc changes settings/schema @@ -42,8 +42,9 @@ | `AppBootstrapper::RendererStart` | `'app' \| 'configure'` - The Renderer has started in some . Plugins should listen to this and do their kick offs. Currently, we're explicitly calling a `render*` functions. | | `Plugin::SyncSettings` | Called when something (generally a plugin, but could be `Form::Interactions`) updates the state of the Form/Settings/Schema and needs everything to come into sync. | +#### TODO + * Refactor: - * REVIEW ALL DOCUMENTATION NOW!!!! * Forms: * For multi-select items, need separation between value/label * See info above @@ -51,8 +52,6 @@ * Form schema processors should have validation for required properties, just because * Thought Experiment: Accessor Function `setSetting('settingName', someValue)` * Used to set individual setting - * Possibly consider `mergeSettings({ settingName1: 'someValue', settingName2: 'some checked value' })` to merge an entire tree of values at once - * Quite likely can iteratively call `setSetting`? * Would use the Schema to derive how to inject/select setting * i.e., `FormSchemaCheckedMultiInput` would iterate through values and build the expected `selectedIndex:value` setting. * i.e., Groupings will probably need some recursion to this? diff --git a/package-lock.json b/package-lock.json index 4e174ae..881ecdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,20 +9,20 @@ "version": "0.2.4", "license": "ISC", "dependencies": { + "@fastify/deepmerge": "^1.3.0", "@picocss/pico": "^2.0.3", "events": "^3.3.0", "handlebars": "^4.7.8", "lodash.get": "^4.4.2", - "lodash.merge": "^4.6.2", "lodash.set": "^4.3.2", "lz-string": "^1.5.0", + "mergician": "^2.0.0", "splitting": "^1.0.6" }, "devDependencies": { "@mxssfd/typedoc-theme": "^1.1.3", "@types/events": "^3.0.3", "@types/lodash.get": "^4.4.9", - "@types/lodash.merge": "^4.6.9", "@types/lodash.set": "^4.3.9", "@types/lz-string": "^1.5.0", "@types/node": "^20.11.5", @@ -419,6 +419,11 @@ "node": ">=12" } }, + "node_modules/@fastify/deepmerge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", + "integrity": "sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==" + }, "node_modules/@material/material-color-utilities": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz", @@ -779,15 +784,6 @@ "@types/lodash": "*" } }, - "node_modules/@types/lodash.merge": { - "version": "4.6.9", - "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", - "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/lodash.set": { "version": "4.3.9", "resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.9.tgz", @@ -1365,11 +1361,6 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, "node_modules/lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -1413,6 +1404,14 @@ "node": ">= 12" } }, + "node_modules/mergician": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mergician/-/mergician-2.0.0.tgz", + "integrity": "sha512-iws4icDgAOMryAiKEtIj+MxljRPLf56oe7aJpaTVPvPnAran+BwwsGsEXAtlEwtNy81B4q2sNqMHBXRTGhEQ+Q==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", diff --git a/package.json b/package.json index c60fe28..555e79d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "@mxssfd/typedoc-theme": "^1.1.3", "@types/events": "^3.0.3", "@types/lodash.get": "^4.4.9", - "@types/lodash.merge": "^4.6.9", "@types/lodash.set": "^4.3.9", "@types/lz-string": "^1.5.0", "@types/node": "^20.11.5", @@ -40,13 +39,14 @@ "vite-plugin-dts": "^3.7.1" }, "dependencies": { + "@fastify/deepmerge": "^1.3.0", "@picocss/pico": "^2.0.3", "events": "^3.3.0", "handlebars": "^4.7.8", "lodash.get": "^4.4.2", - "lodash.merge": "^4.6.2", "lodash.set": "^4.3.2", "lz-string": "^1.5.0", + "mergician": "^2.0.0", "splitting": "^1.0.6" } } diff --git a/src/scripts/Managers/SettingsManager.ts b/src/scripts/Managers/SettingsManager.ts index 96b7fa4..e4b96ef 100644 --- a/src/scripts/Managers/SettingsManager.ts +++ b/src/scripts/Managers/SettingsManager.ts @@ -86,8 +86,13 @@ export class SettingsManager { set(setting: unknown, value: unknown): void { // Single Setting Name targeted with a value, we'll take it as a simple set. if (typeof setting === 'string') { - // ! FIXME : Need to encrypt based on type + const encryptedTypes = ['hidden', 'password']; + const processed = this.context?.getProcessedSchema(); + const schemaEntry = processed?.mappings.byName[setting]; this._settings[setting as keyof PluginSettingsBase] = value as any; + if (schemaEntry && encryptedTypes.includes(schemaEntry.inputType)) { + this.toggleSettingEncrypted(this._settings, setting, true); + } } else { // Entire object sent in, wholesale set! if (value) { @@ -264,14 +269,30 @@ export class SettingsManager { }; Object.keys(maskableEntries ?? {}).forEach(settingName => { - const val = get(settings, settingName); - - if (val) { - const codingDir = encrypt ? btoa : atob; - set(settings, settingName, codingDir(val)); - } + this.toggleSettingEncrypted(settings, settingName, encrypt); }); return settings; } + + /** + * Toggle a Setting by Name to be Encrypted or Decrypted. + * + * @param settings - Settings object to act on. + * @param settingName - Settings Name to act on. + * @param encrypt - Whether to Encrypt or Decrypt to value. + */ + private toggleSettingEncrypted( + settings: PluginSettings, + settingName: string, + encrypt: boolean + ) { + const val = get(settings, settingName); + if (!val) { + return; + } + + const codingDir = encrypt ? btoa : atob; + set(settings, settingName, codingDir(val)); + } } diff --git a/src/scripts/Managers/TemplateManager.ts b/src/scripts/Managers/TemplateManager.ts index 98babef..3a4df86 100644 --- a/src/scripts/Managers/TemplateManager.ts +++ b/src/scripts/Managers/TemplateManager.ts @@ -4,7 +4,7 @@ * @module */ -import merge from 'lodash.merge'; +import merge from '@fastify/deepmerge'; import { TemplatesContextProvider } from '../ContextProviders/TemplatesContextProvider.js'; import { BuildTemplateMap, TemplateIDsBase, TemplateMap } from '../utils/Templating.js'; import coreTemplate from './coreTemplates.html?raw'; @@ -38,7 +38,6 @@ export class TemplateManager { // Load all template file url values const resp = await fetch(url); templateData = await resp.text(); - templateData = templateData.replace('%PACKAGE_VERSION%', import.meta.env.PACKAGE_VERSION); return templateData; } @@ -48,7 +47,7 @@ export class TemplateManager { const templateMap = BuildTemplateMap(templateData); // Merge into overall Templates Map - merge(this.templates, templateMap); + this.templates = merge()(this.templates, templateMap) as TemplateMap; return templateMap; } diff --git a/src/scripts/utils/Forms/Builder.ts b/src/scripts/utils/Forms/Builder.ts index 7d9cd2a..08d0c22 100644 --- a/src/scripts/utils/Forms/Builder.ts +++ b/src/scripts/utils/Forms/Builder.ts @@ -4,7 +4,7 @@ * @module */ -import merge from 'lodash.merge'; +import merge from '@fastify/deepmerge'; import { Form } from './SchemaProcessors/Form.js'; import { GroupArray } from './SchemaProcessors/Grouping/GroupArray.js'; import { GroupList } from './SchemaProcessors/Grouping/GroupList.js'; @@ -173,7 +173,7 @@ export const BuildFormSchema = ( const newInput = BuildInput(entry, formData); // Accumulate iterative results - results = merge({}, results, newInput, { + results = merge({ all: true })(results, newInput, { html: results.html + newInput.html }); } diff --git a/src/scripts/utils/Forms/SchemaProcessors/AbstractFormSchemaProcessor.ts b/src/scripts/utils/Forms/SchemaProcessors/AbstractFormSchemaProcessor.ts index 9e20ff2..846fc0d 100644 --- a/src/scripts/utils/Forms/SchemaProcessors/AbstractFormSchemaProcessor.ts +++ b/src/scripts/utils/Forms/SchemaProcessors/AbstractFormSchemaProcessor.ts @@ -4,7 +4,7 @@ * @module */ -import merge from 'lodash.merge'; +import merge from '@fastify/deepmerge'; import { ToId } from '../../misc.js'; import { FormSchemaEntryBase, @@ -54,7 +54,7 @@ export class AbstractFormSchemaProcessor diff --git a/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupSubSchema.ts b/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupSubSchema.ts index 2caa6e4..ddf20e7 100644 --- a/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupSubSchema.ts +++ b/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupSubSchema.ts @@ -4,7 +4,7 @@ * @module */ -import merge from 'lodash.merge'; +import merge from '@fastify/deepmerge'; import { BuildFormSchema } from '../../Builder.js'; import { FormSchemaGrouping } from '../../types.js'; import { BaseFormSchemaProcessor } from '../BaseFormSchemaProcessor.js'; @@ -32,7 +32,7 @@ export class GroupSubSchema extends BaseFormSchemaProcessor this.entry.description ? `
${this.entry.description}
` : ''; const subSchemaResults = BuildFormSchema(this.entry.subSchema, this.formData); - merge(this.mappings, subSchemaResults.mappings); + this.mappings = merge()(this.mappings, subSchemaResults.mappings); return `
{ ); const childResults = childInput.process(); - merge(this.mappings, childResults.mappings); + this.mappings = merge()(this.mappings, childResults.mappings); return childResults.html; }); diff --git a/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupingRow.ts b/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupingRow.ts index 0951e85..ce329b3 100644 --- a/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupingRow.ts +++ b/src/scripts/utils/Forms/SchemaProcessors/Grouping/GroupingRow.ts @@ -4,7 +4,7 @@ * @module */ -import merge from 'lodash.merge'; +import merge from '@fastify/deepmerge'; import { BuildInput } from '../../Builder.js'; import { FormSchemaGroupingRow } from '../../types.js'; import { SimpleInput } from '../Inputs/SimpleInput.js'; @@ -44,7 +44,7 @@ export class GroupingRow extends SimpleInput { ); // Accumulate iterative results - merge(this.mappings, childResults.mappings); + this.mappings = merge()(this.mappings, childResults.mappings); return childResults.html; }); diff --git a/src/scripts/utils/Forms/SchemaProcessors/Inputs/CheckedMultiInput.ts b/src/scripts/utils/Forms/SchemaProcessors/Inputs/CheckedMultiInput.ts index ab9609e..a5e8877 100644 --- a/src/scripts/utils/Forms/SchemaProcessors/Inputs/CheckedMultiInput.ts +++ b/src/scripts/utils/Forms/SchemaProcessors/Inputs/CheckedMultiInput.ts @@ -4,7 +4,7 @@ * @module */ -import merge from 'lodash.merge'; +import merge from '@fastify/deepmerge'; import { FormSchemaCheckedMultiInput } from '../../types.js'; import { InputWrapper } from '../InputWrapper.js'; import { CheckedInput } from './CheckedInput.js'; @@ -45,7 +45,7 @@ export class CheckedMultiInput extends SimpleInput // Accumulate recursive/iterative results outString += childResults.html; - merge(this.mappings, childResults.mappings); + this.mappings = merge()(this.mappings, childResults.mappings); }); return ` diff --git a/typedoc.json b/typedoc.json index ab950ac..0881c93 100644 --- a/typedoc.json +++ b/typedoc.json @@ -10,8 +10,8 @@ "hideGenerator": true, "sourceLinkExternal": true, - "plugin": ["@mxssfd/typedoc-theme"], - "theme": "my-theme" - // "plugin": ["typedoc-material-theme"], - // "themeColor": "#cb9820" + // "plugin": ["@mxssfd/typedoc-theme"], + // "theme": "my-theme" + "plugin": ["typedoc-material-theme"], + "themeColor": "#cb9820" } From 6e3f58c20e5db1ed7f6085208154429930d1cc62 Mon Sep 17 00:00:00 2001 From: CodexHere Date: Tue, 5 Mar 2024 15:18:05 -0600 Subject: [PATCH 02/17] * Renamed SettingsRenderer -> ConfigurationRenderer * Added bogus modules for doc generation purposes --- README.md | 7 ++++--- src/scripts/AppBootstrapper.ts | 14 +++++++------- src/scripts/ContextProviders/index.ts | 5 +++++ src/scripts/Managers/index.ts | 5 +++++ ...ettingsRenderer.ts => ConfigurationRenderer.ts} | 10 +++++----- ...rerHelper.ts => ConfigurationRendererHelper.ts} | 8 ++++---- src/scripts/Renderers/index.ts | 5 +++++ src/scripts/index.ts | 2 +- src/scripts/types/Managers.ts | 2 +- src/scripts/types/index.ts | 5 +++++ src/scripts/utils/Forms/SchemaProcessors/index.ts | 5 +++++ src/scripts/utils/Forms/index.ts | 5 +++++ src/scripts/utils/index.ts | 5 +++++ 13 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 src/scripts/ContextProviders/index.ts create mode 100644 src/scripts/Managers/index.ts rename src/scripts/Renderers/{SettingsRenderer.ts => ConfigurationRenderer.ts} (97%) rename src/scripts/Renderers/{SettingsRendererHelper.ts => ConfigurationRendererHelper.ts} (97%) create mode 100644 src/scripts/Renderers/index.ts create mode 100644 src/scripts/types/index.ts create mode 100644 src/scripts/utils/Forms/SchemaProcessors/index.ts create mode 100644 src/scripts/utils/Forms/index.ts create mode 100644 src/scripts/utils/index.ts diff --git a/README.md b/README.md index 64a0b20..c733ba4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ * Need `RendererInstanceEvents.SETTINGS_STALE` to re-init settings * Necessary for when a Plugin/etc changes settings/schema - * Should this trigger an entire re-`init` of the `SettingsRenderer` or can we get away with re-Deserializing the Form Data? + * Should this trigger an entire re-`init` of the `ConfigurationRenderer` or can we get away with re-Deserializing the Form Data? * All Inputs * Needs REQUIRED set * Needs READONLY set @@ -49,6 +49,7 @@ * For multi-select items, need separation between value/label * See info above * Default/Required/Readonly Values for both single/multi/group Entries + * Needs to properly support `` - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist * Form schema processors should have validation for required properties, just because * Thought Experiment: Accessor Function `setSetting('settingName', someValue)` * Used to set individual setting @@ -72,7 +73,7 @@ * Maybe another `SCHEMA_STALE` for schema changes? * Current Registering of settings injects meta, but assumes meta derives from a registration object that returns a mapping of Middleware Chains and Events, making it easy to dump this stuff as metadata... This may not be as plain and simple if plugins do self-registration. * Need to think how to properly list/get middleware chain names and event names - * I believe ProcessedSchema stays a thing of the SettingsManager, but still needs accessor for `SettingsRenderer` to render the form data, etc. + * I believe ProcessedSchema stays a thing of the SettingsManager, but still needs accessor for `ConfigurationRenderer` to render the form data, etc. * Consider making `getSettings` take in an `encrypt: boolean` and getting rid of `getMaskedSettings` * `DisplayManager`: Rename `DisplayAccessor` to `DisplayContext` for the type * `PluginManager`: No Changes - Not Context material, just a Manager. @@ -114,7 +115,7 @@ * This means settings values should be MASKED on SET * But not wholesale sets, just set-by-name and merging. * Of course, should have ability to disable masking on "set" value. - * Will likely need a refactor of `SettingsRenderer` integration with `SettingsManager` and associative `Context`. + * Will likely need a refactor of `ConfigurationRenderer` integration with `SettingsManager` and associative `Context`. * Consider adding `debug` and replace `console.log` * https://bundlephobia.com/package/debug@4.3.4 * If we don't add it, remove `console.log` diff --git a/src/scripts/AppBootstrapper.ts b/src/scripts/AppBootstrapper.ts index a7f6094..0c422ed 100644 --- a/src/scripts/AppBootstrapper.ts +++ b/src/scripts/AppBootstrapper.ts @@ -11,7 +11,7 @@ import { PluginManager } from './Managers/PluginManager.js'; import { SettingsManager } from './Managers/SettingsManager.js'; import { TemplateManager } from './Managers/TemplateManager.js'; import { AppRenderer } from './Renderers/AppRenderer.js'; -import { SettingsRenderer } from './Renderers/SettingsRenderer.js'; +import { ConfigurationRenderer } from './Renderers/ConfigurationRenderer.js'; import { AppBootstrapperOptions, PluginManagerEmitter, PluginManagerEvents } from './types/Managers.js'; import { PluginImportResults, PluginSettingsBase } from './types/Plugin.js'; import { RendererConstructor, RendererInstance, RendererInstanceEvents } from './types/Renderers.js'; @@ -33,7 +33,7 @@ import { RendererConstructor, RendererInstance, RendererInstanceEvents } from '. * document.addEventListener('DOMContentLoaded', () => { * const bootstrapper = new AppBootstrapper({ * needsAppRenderer: true, - * needsSettingsRenderer: true, + * needsConfigurationRenderer: true, * defaultPlugin: Plugin_Core * }); * @@ -120,7 +120,7 @@ export class AppBootstrapper { /** * Determines and Initializes a {@link RendererInstance | `RendererInstance`} to present to the User. * - * > NOTE: Possible {@link RendererInstance | `RendererInstance`}s are: {@link AppRenderer | `AppRenderer`}, and {@link SettingsRenderer | `SettingsRenderer`}. + * > NOTE: Possible {@link RendererInstance | `RendererInstance`}s are: {@link AppRenderer | `AppRenderer`}, and {@link ConfigurationRenderer | `ConfigurationRenderer`}. */ private async initRenderer() { const settings = this.settingsManager!.get(); @@ -128,15 +128,15 @@ export class AppBootstrapper { // Force NOT configured if `forceShowSettings` is in Settings, otherwise actually check validity const isConfigured = (!settings.forceShowSettings && true === areSettingsValid) || false; - const { needsSettingsRenderer, needsAppRenderer } = this.bootstrapOptions; - // Wants a `SettingsRenderer`, and `isConfigured` is `false` - const shouldRenderSettings = false === isConfigured && needsSettingsRenderer; + const { needsConfigurationRenderer, needsAppRenderer } = this.bootstrapOptions; + // Wants a `ConfigurationRenderer`, and `isConfigured` is `false` + const shouldRenderSettings = false === isConfigured && needsConfigurationRenderer; // Wants an `AppRenderer`, and `isConfigured` is `true` const shouldRenderApp = true === isConfigured && needsAppRenderer; // Select which Renderer to instantiate... let rendererClass: RendererConstructor | undefined = - shouldRenderSettings ? SettingsRenderer + shouldRenderSettings ? ConfigurationRenderer : shouldRenderApp ? AppRenderer : undefined; diff --git a/src/scripts/ContextProviders/index.ts b/src/scripts/ContextProviders/index.ts new file mode 100644 index 0000000..267fa82 --- /dev/null +++ b/src/scripts/ContextProviders/index.ts @@ -0,0 +1,5 @@ +/** + * Context Providers for Integration points usable by Plugins and various aspects of the Application + * + * @module + */ diff --git a/src/scripts/Managers/index.ts b/src/scripts/Managers/index.ts new file mode 100644 index 0000000..b910f06 --- /dev/null +++ b/src/scripts/Managers/index.ts @@ -0,0 +1,5 @@ +/** + * Managers for controlling various aspects of the Application + * + * @module + */ diff --git a/src/scripts/Renderers/SettingsRenderer.ts b/src/scripts/Renderers/ConfigurationRenderer.ts similarity index 97% rename from src/scripts/Renderers/SettingsRenderer.ts rename to src/scripts/Renderers/ConfigurationRenderer.ts index 2691cea..af08051 100644 --- a/src/scripts/Renderers/SettingsRenderer.ts +++ b/src/scripts/Renderers/ConfigurationRenderer.ts @@ -12,7 +12,7 @@ import { Deserialize, Serialize } from '../utils/Forms/Serializer.js'; import { GetLocalStorageItem, SetLocalStorageItem } from '../utils/LocalStorage.js'; import { RenderTemplate } from '../utils/Templating.js'; import { ToId, debounce } from '../utils/misc.js'; -import { SettingsRendererHelper } from './SettingsRendererHelper.js'; +import { ConfigurationRendererHelper } from './ConfigurationRendererHelper.js'; /** * Elements we know about in this {@link RendererInstance | `RendererInstance`}. @@ -37,12 +37,12 @@ const RemoveArrayIndex = (paramName: string) => paramName.replace(indexRegExp, ' * * @typeParam PluginSettings - Shape of the Settings object the Plugin can access. */ -export class SettingsRenderer +export class ConfigurationRenderer extends EventEmitter implements RendererInstance { /** Delegated UX Behaviors to a Helper Class */ - private helper?: SettingsRendererHelper; + private helper?: ConfigurationRendererHelper; /** Debounced Handler called any of the Settings Form inputs are changed. */ private _onSettingsChanged: (event: Event) => void; /** Handler for when the Settings Form is scrolled. */ @@ -51,7 +51,7 @@ export class SettingsRenderer private elements: ElementMap = {} as ElementMap; /** - * Create a new {@link SettingsRenderer | `SettingsRenderer`}. + * Create a new {@link ConfigurationRenderer | `ConfigurationRenderer`}. * * @param options - Incoming Options for this Renderer. * @typeParam PluginSettings - Shape of the Settings object the Plugin can access. @@ -88,7 +88,7 @@ export class SettingsRenderer Deserialize(this.elements['form'], settings); - this.helper = new SettingsRendererHelper(this.options); + this.helper = new ConfigurationRendererHelper(this.options); this.helper.init(); // Update Form Validity state data only if we're starting with settings diff --git a/src/scripts/Renderers/SettingsRendererHelper.ts b/src/scripts/Renderers/ConfigurationRendererHelper.ts similarity index 97% rename from src/scripts/Renderers/SettingsRendererHelper.ts rename to src/scripts/Renderers/ConfigurationRendererHelper.ts index 970aaec..91a8ef6 100644 --- a/src/scripts/Renderers/SettingsRendererHelper.ts +++ b/src/scripts/Renderers/ConfigurationRendererHelper.ts @@ -1,5 +1,5 @@ /** - * Helper Behaviors for better UX within the `SettingsRenderer` + * Helper Behaviors for better UX within the `ConfigurationRenderer` * * @module */ @@ -36,11 +36,11 @@ const isScrollTTYExpired = () => { }; /** - * Helper Behaviors for better UX within the `SettingsRenderer`. + * Helper Behaviors for better UX within the `ConfigurationRenderer`. * * @typeParam PluginSettings - Shape of the Settings object the Plugin can access. */ -export class SettingsRendererHelper { +export class ConfigurationRendererHelper { /** Debounced Handler for when the URL Text is Moused. */ private onUrlMouseEnterDebouncer: DebounceResult; /** Local `ElementMap` mapping name -> Element the {@link RendererInstance | `RendererInstance`} Helper needs to access. */ @@ -49,7 +49,7 @@ export class SettingsRendererHelper { private settingsOptionsFormCache: PluginSettings = {} as PluginSettings; /** - * Create a new {@link SettingsRenderer | `SettingsRenderer`}. + * Create a new {@link ConfigurationRenderer | `ConfigurationRenderer`}. * * @param options - Incoming Options for this Renderer. * @typeParam PluginSettings - Shape of the Settings object the Plugin can access. diff --git a/src/scripts/Renderers/index.ts b/src/scripts/Renderers/index.ts new file mode 100644 index 0000000..f49a033 --- /dev/null +++ b/src/scripts/Renderers/index.ts @@ -0,0 +1,5 @@ +/** + * Application and Configuration Renderers to present different portions of the Application to the User + * + * @module + */ diff --git a/src/scripts/index.ts b/src/scripts/index.ts index d06f40b..863521b 100755 --- a/src/scripts/index.ts +++ b/src/scripts/index.ts @@ -5,7 +5,7 @@ import Plugin_Core, { AppSettings_Chat } from './Plugin_Core.js'; document.addEventListener('DOMContentLoaded', () => { const bootstrapper = new AppBootstrapper({ needsAppRenderer: true, - needsSettingsRenderer: true, + needsConfigurationRenderer: true, defaultPlugin: Plugin_Core }); diff --git a/src/scripts/types/Managers.ts b/src/scripts/types/Managers.ts index 540733e..98f5676 100644 --- a/src/scripts/types/Managers.ts +++ b/src/scripts/types/Managers.ts @@ -22,7 +22,7 @@ export type AppBootstrapperOptions = { /** Initial and Default Plugin to load upon initialization. */ defaultPlugin: PluginConstructor; /** Tells the bootstrapper whether the Application needs a Settings Renderer */ - needsSettingsRenderer?: true; + needsConfigurationRenderer?: true; /** Tells the bootstrapper whether the Application needs an Application Renderer */ needsAppRenderer?: true; }; diff --git a/src/scripts/types/index.ts b/src/scripts/types/index.ts new file mode 100644 index 0000000..3fabc3a --- /dev/null +++ b/src/scripts/types/index.ts @@ -0,0 +1,5 @@ +/** + * Type Declarations for the Application + * + * @module + */ diff --git a/src/scripts/utils/Forms/SchemaProcessors/index.ts b/src/scripts/utils/Forms/SchemaProcessors/index.ts new file mode 100644 index 0000000..a7e8a80 --- /dev/null +++ b/src/scripts/utils/Forms/SchemaProcessors/index.ts @@ -0,0 +1,5 @@ +/** + * Polymorphic (`FormSchemaEntryProcessor`) Facades to Process `FormSchemaEntry` JSON values + * + * @module + */ diff --git a/src/scripts/utils/Forms/index.ts b/src/scripts/utils/Forms/index.ts new file mode 100644 index 0000000..8f2b92d --- /dev/null +++ b/src/scripts/utils/Forms/index.ts @@ -0,0 +1,5 @@ +/** + * Form Builder API + * + * @module + */ diff --git a/src/scripts/utils/index.ts b/src/scripts/utils/index.ts new file mode 100644 index 0000000..e99c990 --- /dev/null +++ b/src/scripts/utils/index.ts @@ -0,0 +1,5 @@ +/** + * Utilities for the Application + * + * @module + */ From 1befb12678cabef561bfb2bf5cf690bee20bdf7c Mon Sep 17 00:00:00 2001 From: CodexHere Date: Wed, 6 Mar 2024 13:56:53 -0600 Subject: [PATCH 03/17] * Cleaned up and extracted Events documentation * LifecycleManager now exists, and handles events for various lifecycle patterns. Also manages Lock state, held within the AppBootstrapper. * All ContextProviders now gate (via Lock state) registration/config-only accessors, and will throw an Error if violated. * Renamed Settings->Configuration/etc as it pertains to the App lifecycle (ie, app mode vs config mode) * Managers now consume and propagate LockHolder * Managers now have a proper API to avoid member access. * Some ContextProvider implementations turned into proxied calls to encapsulate member access to the Manager. * Hashed out all Events as `CoreEvents` * string values of Events are contextualized to the author of the event to give an idea of the source of event flow. * Converted some callback methodology to Events. * Propagated through ConfigurationRenderer and Plugins * ContextProviders use ES6 private (i.e., # modifier) because TS does not actually encapsulate `private` members. This means things like the Manager, internal settings, etc were accessible to Plugins. Now with the private modifier, Plugins can't intrude into the system. --- .github/docs/app_lifecycle.drawio.svg | 60 +++--- .github/docs/events.md | 19 ++ README.md | 31 ++-- src/scripts/AppBootstrapper.ts | 124 ++++++------- .../ContextProviders/BusContextProvider.ts | 128 +++---------- .../DisplayContextProvider.ts | 11 +- .../SettingsContextProvider.ts | 51 ++++-- .../StylesheetsContextProvider.ts | 22 +++ .../TemplatesContextProvider.ts | 75 +++++--- src/scripts/ContextProviders/index.ts | 8 + .../ContextProviders/schemaSettingsCore.json | 2 +- .../schemaSettingsCore_Gamut.json | 4 +- src/scripts/Managers/BusManager.ts | 172 +++++++++++++++--- src/scripts/Managers/LifecycleManager.ts | 138 ++++++++++++++ src/scripts/Managers/PluginManager.ts | 9 +- src/scripts/Managers/SettingsManager.ts | 62 +++---- src/scripts/Managers/TemplateManager.ts | 41 ++++- src/scripts/Managers/coreTemplates.html | 21 ++- src/scripts/Plugin_Core.ts | 49 +++-- src/scripts/Renderers/AppRenderer.ts | 29 +-- .../Renderers/ConfigurationRenderer.ts | 63 +++---- .../Renderers/ConfigurationRendererHelper.ts | 12 +- src/scripts/index.ts | 4 +- src/scripts/types/ContextProviders.ts | 64 ++++++- src/scripts/types/Events.ts | 109 +++++++++++ src/scripts/types/Managers.ts | 70 +++---- src/scripts/types/Plugin.ts | 14 -- src/scripts/types/Renderers.ts | 15 +- src/scripts/utils/EnhancedEventEmitter.ts | 4 +- .../SchemaProcessors/Grouping/GroupingBase.ts | 4 +- src/styles/_focus-mode.scss | 1 + src/styles/settings.scss | 6 +- static/plugins/Example/index.js | 85 +++++---- static/plugins/HangoutHere/index.js | 51 ++++-- static/plugins/Twitch - Chat/index.js | 86 +++++---- static/plugins/Twitch - EmoteSwap/index.js | 55 ++++-- 36 files changed, 1091 insertions(+), 608 deletions(-) create mode 100644 .github/docs/events.md create mode 100644 src/scripts/Managers/LifecycleManager.ts create mode 100644 src/scripts/types/Events.ts diff --git a/.github/docs/app_lifecycle.drawio.svg b/.github/docs/app_lifecycle.drawio.svg index 5d02c5e..5e9dff7 100644 --- a/.github/docs/app_lifecycle.drawio.svg +++ b/.github/docs/app_lifecycle.drawio.svg @@ -1,4 +1,4 @@ - + @@ -46,7 +46,7 @@ -
+
Create Managers @@ -54,7 +54,7 @@
- + Create Managers @@ -83,7 +83,7 @@ -
+
Gets Settings from URL @@ -91,7 +91,7 @@
- + Gets Settings from URL @@ -121,7 +121,7 @@ -
+
Init TemplateManager @@ -129,7 +129,7 @@
- + Init TemplateManager @@ -141,7 +141,7 @@ -
+
Init PluginManager @@ -149,7 +149,7 @@
- + Init PluginManager @@ -212,7 +212,7 @@ -
+
Dynamically Import @@ -220,7 +220,7 @@
- + Dynamically Import @@ -252,7 +252,7 @@ -
+
Register with @@ -333,7 +333,7 @@ -
+
BusContextProvider::registerMiddleware @@ -341,7 +341,7 @@
- + BusContextProvider::registerMiddleware @@ -349,7 +349,7 @@ -
+
BusContextProvider::registerEvents @@ -357,7 +357,7 @@
- + BusContextProvider::registerEvents @@ -374,7 +374,7 @@
- + Event
@@ -391,7 +391,7 @@
- + Event
@@ -408,7 +408,7 @@
- + Event @@ -460,7 +460,7 @@ -
+
StylesheetContextProvider::register @@ -495,7 +495,7 @@ -
+
TemplateContextProvider::register @@ -527,24 +527,6 @@ - - - - - -
-
-
- Create Managers -
-
-
-
- - Create Managers - -
-
diff --git a/.github/docs/events.md b/.github/docs/events.md new file mode 100644 index 0000000..1dc43e7 --- /dev/null +++ b/.github/docs/events.md @@ -0,0 +1,19 @@ +# LifeCycle Events: + +LifeCycle Events are emitted in two different contexts: Internal communication, and Plugin communication. + +> Event Names combine the "Source" and "Event Name" into a single string with the form: `Source::EventName`. +> Example: `"PluginManager::PluginsLoaded"` + +* Events with the Scope of `Internal` (listed below) are directly emitted-from the "Source". +* Events with the Scope of `Plugin` (listed below) are directly emitted-from a `PluginInstance` on the `BusManager::emitter`, by way of `ContextProvider_Bus::emit`. +* Events with *multiple* Scopes are directional from A -> B. + +| Source | Event Name | Scope | Note | +| ---------------- | ----------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| PluginManager | PluginsLoaded | Internal -> Internal | Called after *all* Plugins are Loaded to show Import Errors, and initialize the Bus. | +| PluginManager | PluginsUnloaded | Internal -> Internal | Called after *all* Plugins are Unloaded, which resets the environment (mostly resetting the Bus). | +| AppBootstrapper | RendererStart | Internal -> Internal
Internal -> Plugin | Signifies a Renderer has started in some ``.
Plugins should listen to this and do their kick offs.
Event is re-Fired after a `RendererInstance::PluginsChanged` in `configure` Render Mode. | +| RendererInstance | PluginsChanged | Internal | Called while Configuring, when the Settings have changed to add/remove Plugins.
This should only occur in `configure` Render Mode from the `ConfigurationRenderer`.
| +| Plugin | SyncSettings | Plugin -> Internal | Called by a Plugin when it updates Form/Settings Data for the User (i.e., Auth Flows, etc).
Handled by the `ConfigurationRenderer`.
| +| Plugin | MiddlewareExecute | Plugin -> Internal | Called from a Plugin when it wants to execute a Middleware Chain.
Note: Only the *first* Plugin to Register against the Chain Name will be able to Execute the Chain.
Handled by the `BusManager`.
| diff --git a/README.md b/README.md index c733ba4..d7f8324 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # HangoutHere Overlay: Chat +* [LifeCycle Diagram](.github/docs/app_lifecycle.drawio.svg) +* [Events](.github/docs/events.md) + ## TODO ### Default Settings Thinkery @@ -30,18 +33,6 @@ ### General TODO -#### Lifecycle Events: - * Do they all have common namespace? I.E., `Core::EventName` - -| Event Name | Note | -| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PluginManager::PluginsLoaded` | Called after all plugins are loaded, should re-mask settings
* But *why* do we want to re-mask settings *THEN*? | -| `PluginManager::PluginsUnloaded` | Called after all plugins are unloaded, which resets the environment (mostly Bus). | -| `Renderer::PluginsChanged` | Called when Configuring, when the settings have changed to add/remove Plugins. Ideally, should never occur at app/run-time.
* Replaces the `RendererInstanceEvents.PLUGINS_STALE` event. | -| `Renderer::SyncCache` | Called when the state of the Form is out of sync with the `ProcessedFormSchema` cache and needs to be re-processed against the current Form Settings. Generally, this is called when a `FormSchemaGroupingList` adds a new Entry through user interaction.
* I think this might need to be re-broadcasted on the Renderer for the `LifecycleManager` to listen to and then reprocess plugin schemas. | -| `AppBootstrapper::RendererStart` | `'app' \| 'configure'` - The Renderer has started in some . Plugins should listen to this and do their kick offs. Currently, we're explicitly calling a `render*` functions. | -| `Plugin::SyncSettings` | Called when something (generally a plugin, but could be `Form::Interactions`) updates the state of the Form/Settings/Schema and needs everything to come into sync. | - #### TODO * Refactor: @@ -88,7 +79,6 @@ * Consider building `LifecycleManager` which then houses the event handlers for various manager events. Keeps it tidy and isolated. * Ensure registration functions are blocked during runtime! * `busManager.disableAddingListeners` should be renamed to just `toggle(enable?:boolean)` - * Plugins store Context on `register`, but should be actually done on event starting. * Context Finalizing: * Make sure Contexts don't expose anything they shouldn't! * Double check during JS-runtime we can't access private members/methods! @@ -121,22 +111,23 @@ * If we don't add it, remove `console.log` * Consider creating a new bg style, or various themes: * https://www.joshwcomeau.com/gradient-generator/ - * Events should be evaluated: - * Lifecycle events should be either cleanly separated, or aggregated into a single group: - * PluginManagerEvents.LOADED - * BusManagerEvents.MIDDLEWARE_EXECUTE - * etc - * Make sure they have clean/nice/unique string values, and not jus "middleware-execute" as a lame value! +* Check Plugin Lifecycle: + * Errors on Import + * Errors on RendererStarted (from `LifecycleManager`) * Core Plugin needs to require Twitch-Chat * Need to make sure we have `Twitch - Chat` enabled * Maybe this is unnecessary if the process enforces enabling as mentioned in `Refactor > Bootstrapper` * All Plugins' settings names need prefixes to avoid collissions, as well as updates within their code! This *could* spread to other plugins for a name, so consider a string search before replacing. IE, EmoteSwap checking for Chat options. +* Example Plugin: + * Needs to mention in description, and add advanced `isConfigured` check for `enabled` items missing a `message`. + * Convert `Show Error at Runtime?` to Enabled/Message like others. + * Can we combine them all into one `grouparray`? * Work on Forms Validator Input types, make sure regexes are REALLY good! * Add About/FAQ/ETC links on Settings Page * How to use it, etc. * or should it be a github wiki? * Create some cool examples: - * renderSettings + * renderConfiguration * Hello plugin should re-enable button if text is not `"Hello from the Plugin!"` * listen to click of button, show error * ?? This could replace the timeout errors diff --git a/src/scripts/AppBootstrapper.ts b/src/scripts/AppBootstrapper.ts index 0c422ed..cb6b57b 100644 --- a/src/scripts/AppBootstrapper.ts +++ b/src/scripts/AppBootstrapper.ts @@ -7,14 +7,16 @@ import { DisplayContextProvider } from './ContextProviders/DisplayContextProvider.js'; import { StylesheetsContextProvider } from './ContextProviders/StylesheetsContextProvider.js'; import { BusManager } from './Managers/BusManager.js'; +import { LifecycleManager } from './Managers/LifecycleManager.js'; import { PluginManager } from './Managers/PluginManager.js'; import { SettingsManager } from './Managers/SettingsManager.js'; import { TemplateManager } from './Managers/TemplateManager.js'; import { AppRenderer } from './Renderers/AppRenderer.js'; import { ConfigurationRenderer } from './Renderers/ConfigurationRenderer.js'; -import { AppBootstrapperOptions, PluginManagerEmitter, PluginManagerEvents } from './types/Managers.js'; -import { PluginImportResults, PluginSettingsBase } from './types/Plugin.js'; -import { RendererConstructor, RendererInstance, RendererInstanceEvents } from './types/Renderers.js'; +import { AppBootstrapperEmitter, CoreEvents, PluginManagerEmitter } from './types/Events.js'; +import { AppBootstrapperOptions, LockHolder } from './types/Managers.js'; +import { RenderMode, RendererConstructor, RendererInstance } from './types/Renderers.js'; +import { EnhancedEventEmitter } from './utils/EnhancedEventEmitter.js'; /** * Initiates and Maintains the Application Lifecycle. @@ -27,11 +29,11 @@ import { RendererConstructor, RendererInstance, RendererInstanceEvents } from '. * Example Applicatin Kickoff: * ```js * import { AppBootstrapper } from './AppBootstrapper.js'; - * import Plugin_Core, { AppSettings_Chat } from './Plugin_Core.js'; + * import Plugin_Core from './Plugin_Core.js'; * * // Start the Application once DOM has loaded * document.addEventListener('DOMContentLoaded', () => { - * const bootstrapper = new AppBootstrapper({ + * const bootstrapper = new AppBootstrapper({ * needsAppRenderer: true, * needsConfigurationRenderer: true, * defaultPlugin: Plugin_Core @@ -43,29 +45,46 @@ import { RendererConstructor, RendererInstance, RendererInstanceEvents } from '. * * @typeParam PluginSettings - Shape of the Settings object the Plugin can access. */ -export class AppBootstrapper { +export class AppBootstrapper extends EnhancedEventEmitter implements AppBootstrapperEmitter, LockHolder { + /** Semaphore indicating Lock status. When Locked, Registration and Config-only access like manipulating Settings are unavailable. */ + isLocked: boolean = false; + + /** Which `` to Render (for Plugins). */ + renderMode: RenderMode = 'configure'; + /** Instance of the {@link Managers/BusManager.BusManager | `BusManager`}. */ private busManager?: BusManager; + /** Instance of the {@link SettingsManager | `SettingsManager`}. */ private settingsManager?: SettingsManager; + /** Instance of the {@link PluginManager | `PluginManager`} (as a {@link types/Managers.PluginManagerEmitter | `PluginManagerEmitter`}). */ - private pluginManager?: PluginManagerEmitter; + private pluginManager?: PluginManagerEmitter; + /** Instance of the {@link TemplateManager | `TemplateManager`}. */ private templateManager?: TemplateManager; + /** Instance of the {@link DisplayContextProvider | `DisplayContextProvider`} acting as a `DisplayManager`. */ private displayContext?: DisplayContextProvider; + /** Instance of the {@link StylesheetsContextProvider | `StylesheetsContextProvider`} acting as a `StyleManager` */ private stylesheetContext?: StylesheetsContextProvider; + /** Instance of the {@link RendererInstance | `RendererInstance`} chosen to present to the User. */ private renderer?: RendererInstance; + /** Instance of the {@link LifecycleManager | `LifecycleManager`} managing Application Lifecycle. */ + private lifecycleManager?: LifecycleManager; + /** * Create a new {@link AppBootstrapper | `AppBootstrapper`}. * * @param bootstrapOptions - Incoming Options for the {@link AppBootstrapper | `AppBootstrapper`}. * @typeParam PluginSettings - Shape of the Settings object the Plugin can access. */ - constructor(public bootstrapOptions: AppBootstrapperOptions) {} + constructor(public bootstrapOptions: AppBootstrapperOptions) { + super(); + } /** * Kickoff the entire Application's Lifecycle! @@ -75,8 +94,6 @@ export class AppBootstrapper { this.buildManagers(); await this.initManagers(); await this.initRenderer(); - this.bindManagerEvents(); - this.bindRendererEvents(); } catch (err) { this.displayContext?.showError(err as Error); } @@ -88,11 +105,11 @@ export class AppBootstrapper { * Also builds any *Global* {@link types/ContextProviders | `ContextProviders`}. */ private async buildManagers() { - this.busManager = new BusManager(); - this.templateManager = new TemplateManager(); - this.stylesheetContext = new StylesheetsContextProvider(); + this.busManager = new BusManager(this); + this.templateManager = new TemplateManager(this); + this.stylesheetContext = new StylesheetsContextProvider(this); this.displayContext = new DisplayContextProvider(this.templateManager); - this.settingsManager = new SettingsManager(globalThis.location.href); + this.settingsManager = new SettingsManager(this, globalThis.location.href); this.pluginManager = new PluginManager({ defaultPlugin: this.bootstrapOptions.defaultPlugin, @@ -105,12 +122,23 @@ export class AppBootstrapper { template: this.templateManager } }); + + this.lifecycleManager = new LifecycleManager({ + bootstrapper: this, + bus: this.busManager, + display: this.displayContext, + plugin: this.pluginManager, + settings: this.settingsManager, + stylesheets: this.stylesheetContext, + template: this.templateManager + }); } /** * Initialize Managers in appropriate order. */ private async initManagers() { + await this.lifecycleManager!.init(); await this.settingsManager!.init(); await this.busManager!.init(); await this.templateManager!.init(); @@ -130,73 +158,47 @@ export class AppBootstrapper { const isConfigured = (!settings.forceShowSettings && true === areSettingsValid) || false; const { needsConfigurationRenderer, needsAppRenderer } = this.bootstrapOptions; // Wants a `ConfigurationRenderer`, and `isConfigured` is `false` - const shouldRenderSettings = false === isConfigured && needsConfigurationRenderer; + const shouldrenderConfiguration = false === isConfigured && needsConfigurationRenderer; // Wants an `AppRenderer`, and `isConfigured` is `true` const shouldRenderApp = true === isConfigured && needsAppRenderer; // Select which Renderer to instantiate... - let rendererClass: RendererConstructor | undefined = - shouldRenderSettings ? ConfigurationRenderer + const rendererClass: RendererConstructor | undefined = + shouldrenderConfiguration ? ConfigurationRenderer : shouldRenderApp ? AppRenderer : undefined; + // Store the `renderMode` for future renders. + this.renderMode = isConfigured ? 'app' : 'configure'; + // If we don't have a `RendererConstructor` selected, the work here is done! if (!rendererClass) { return; } this.renderer = new rendererClass({ - template: this.templateManager!, - settings: this.settingsManager!, + bus: this.busManager!, + display: this.displayContext!, plugin: this.pluginManager!, - display: this.displayContext! + settings: this.settingsManager!, + template: this.templateManager! }); // Init the Renderer await this.renderer.init(); - } - /** - * Bind Events that are sent from various Managers. - * - * > These are an IoC approach to managing the Lifecycle without requiring - * a lot of injected dependencies. - */ - private bindManagerEvents() { - // Upon Plugins being Loaded, we want to re-init the `BusManager`, - // and re-cache the Parsed JSON Results. - // Show Errors if there were any failed imports of Plugins. - this.pluginManager!.addListener(PluginManagerEvents.LOADED, (importResults: PluginImportResults) => { - this.busManager!.init(); - this.busManager!.disableAddingListeners(); - this.settingsManager!.updateProcessedSchema(importResults.good); - - if (importResults.bad && 0 !== importResults.bad.length) { - this.displayContext?.showError(importResults.bad); + // Emit the `RendererStart` Event on the "Internal" Scope. + // This will be proxied by the `LifecycleManager` + (this as AppBootstrapperEmitter).emit(CoreEvents.RendererStarted, { + renderer: this.renderer, + renderMode: this.renderMode, + ctx: { + bus: this.busManager?.context, + display: this.displayContext, + settings: this.settingsManager?.context, + stylesheets: this.stylesheetContext, + template: this.templateManager?.context } }); - - // Upon Plugins being Unloaded, reset the `BusManager`, and the Settings Schema. - this.pluginManager!.addListener(PluginManagerEvents.UNLOADED, () => { - this.busManager!.reset(); - }); - } - - /** - * Bind Events that are sent from the active {@link RendererInstance | `RendererInstance`}. - * - * > These are an IoC approach to managing the Lifecycle without requiring - * a lot of injected dependencies. - */ - private bindRendererEvents() { - // Upon Plugin List being changed, we want to reload all Plugins - // and restart the `RendererInstance`. - this.renderer?.addListener(RendererInstanceEvents.PLUGINS_STALE, async () => { - // Reloads all plugins (first unloads) - await this.pluginManager?.registerAllPlugins(); - // Re-init the active `RendererInstance`, which should effectively - // restart the portion of the Application the User is presented. - await this.renderer?.init(); - }); } } diff --git a/src/scripts/ContextProviders/BusContextProvider.ts b/src/scripts/ContextProviders/BusContextProvider.ts index 01f4d19..7e96387 100644 --- a/src/scripts/ContextProviders/BusContextProvider.ts +++ b/src/scripts/ContextProviders/BusContextProvider.ts @@ -7,12 +7,6 @@ import { BusManager } from '../Managers/BusManager.js'; import { ContextProvider_Bus } from '../types/ContextProviders.js'; import { PluginEventRegistration, PluginInstance, PluginMiddlewareMap } from '../types/Plugin.js'; -import { MiddlewareChain, MiddlewareLink } from '../utils/Middleware.js'; - -type PluginBusMap = { - events: PluginEventRegistration; - middleware: PluginMiddlewareMap; -}; /** * Context Provider for the Application Bus. @@ -20,10 +14,8 @@ type PluginBusMap = { * Injected at Registration and Runtime for Application Integration/Access. */ export class BusContextProvider implements ContextProvider_Bus { - /** - * Mapping of Plugin Ref to an Events/Middleware combo map. - */ - private pluginBusMap: Map = new Map(); + /** {@link Managers/BusManager.BusManager | `BusManager`} instance for the {@link types/ContextProviders.ContextProvider_Bus | `ContextProvider_Bus`} to act on. */ + #manager: BusManager; /** * Proxy `call` to the Application Emitter. @@ -46,52 +38,12 @@ export class BusContextProvider implements ContextProvider_Bus { emit: (type: string | number, ...args: any[]) => boolean; /** - * Creates new {@link BusContextProvider | `BusContextProvider`}. - * - * @param manager - {@link BusManager | `BusManager`} instance for the {@link types/ContextProviders.ContextProvider_Bus | `ContextProvider_Bus`} to act on. - */ - constructor(private manager: BusManager) { - this.emit = this.manager.emitter.emit.bind(this.manager.emitter); - this.call = this.manager.emitter.call.bind(this.manager.emitter); - } - - /** - * Unregister a Plugin from the Application. - * - * Removes known Registered Listeners, and the registered Links for Chains. + * Register Middleware Links for Chains with the Application. * * @param plugin - Instance of the Plugin to act on. + * @param registrationMap - Registration of the MiddlewareMap declarations for the Plugin. */ - unregister(plugin: PluginInstance): void { - const pluginMap = this.pluginBusMap.get(plugin.ref); - - if (!pluginMap) { - return; - } - - // Unregister Events - Object.entries(pluginMap.events.recieves || {}).forEach(([eventName, eventListener]) => - this.manager.emitter.removeListener(eventName, eventListener) - ); - - // Unregister Middleware Links for all Chain Names in the mapping - Object.entries(pluginMap.middleware || {}).forEach(([chainName, chainLinks]) => { - const chain = this.manager.chainMap.get(chainName); - - chainLinks.forEach(link => chain?.unuse(link)); - - this.manager.chainMap.delete(chainName); - }); - - // Unregister Middleware Name -> Plugin mappings - for (const [name, pluginRef] of this.manager.pluginMap.entries()) { - if (pluginRef === plugin.ref) { - this.manager.pluginMap.delete(name); - } - } - - this.pluginBusMap.delete(plugin.ref); - } + registerMiddleware: (plugin: PluginInstance, registrationMap: PluginMiddlewareMap) => void; /** * Register Sends/Recieves for Events with the Application. @@ -101,67 +53,29 @@ export class BusContextProvider implements ContextProvider_Bus { * @param plugin - Instance of the Plugin to act on. * @param registrationMap - Registration of the Sends/Recieves declarations for the Plugin. */ - registerEvents(plugin: PluginInstance, registrationMap: PluginEventRegistration): void { - // Cache Events mapping - const pluginMap: PluginBusMap = this.pluginBusMap.get(plugin.ref) || ({} as PluginBusMap); - pluginMap.events = registrationMap; - - if (registrationMap.recieves) { - for (const [eventName, eventFunction] of Object.entries(registrationMap.recieves)) { - // AddListener on behalf of the plugin, and force-bind the function to the PluginInstance - this.manager.emitter.addListener(eventName, eventFunction.bind(plugin)); - } - } - } + registerEvents: (plugin: PluginInstance, registrationMap: PluginEventRegistration) => void; /** - * Register Middleware Links for Chains with the Application. + * Unregister a Plugin from the Application. + * + * Removes known Registered Listeners, and the registered Links for Chains. * * @param plugin - Instance of the Plugin to act on. - * @param registrationMap - Registration of the MiddlewareMap declarations for the Plugin. */ - registerMiddleware(plugin: PluginInstance, registrationMap: PluginMiddlewareMap): void { - for (const [middlewareName, middlewareLinks] of Object.entries(registrationMap)) { - //! FIXME: Should be done through API, not direct access! - let chosenChain = this.manager.chainMap.get(middlewareName); - - // This is the first registration for the Chain - if (!chosenChain) { - // Create a new Chain to be chosen - chosenChain = new MiddlewareChain(); - // Register this plugin as the leader for this Chain - this.manager.pluginMap.set(middlewareName, plugin.ref); - // Register this Chain for the middlewareName - this.manager.chainMap.set(middlewareName, chosenChain); - - console.info(`Registering '${plugin.name}' as leader of Chain: ${middlewareName}`); - } - - chosenChain.use(...middlewareLinks); - } - - // Register each MiddlewareChain with an Error MiddlewareLink - for (const chain of this.manager.chainMap.values()) { - chain.use(this.errorMiddlewareLink); - } - } + unregister: (plugin: PluginInstance) => void; /** - * Error Handling {@link MiddlewareLink | `MiddlewareLink`}. - * - * This Handler is added to the end of every {@link MiddlewareChain | `MiddlewareChain`} - * upon Registration, and as such is treated as an "Error Handler" for the entire Chain. - * You can compare this to a `Promise.catch()`. + * Creates new {@link BusContextProvider | `BusContextProvider`}. * - * @param _context - (Ignored) Contextual State passed from Link to Link. - * @param next - `Next` function to call in order to progress Chain. - * @param error - If supplied, the entire Chain is in Error. + * @param manager - {@link Managers/BusManager.BusManager | `BusManager`} instance for the {@link types/ContextProviders.ContextProvider_Bus | `ContextProvider_Bus`} to act on. */ - private errorMiddlewareLink: MiddlewareLink = async (_context, next, error) => { - if (error) { - throw error; - } else { - await next(); - } - }; + constructor(manager: BusManager) { + this.#manager = manager; + + this.emit = this.#manager.emitter.emit.bind(this.#manager.emitter); + this.call = this.#manager.emitter.call.bind(this.#manager.emitter); + this.registerMiddleware = this.#manager.registerMiddleware.bind(this.#manager); + this.registerEvents = this.#manager.registerEvents.bind(this.#manager); + this.unregister = this.#manager.unregister.bind(this.#manager); + } } diff --git a/src/scripts/ContextProviders/DisplayContextProvider.ts b/src/scripts/ContextProviders/DisplayContextProvider.ts index d71980b..ae750d1 100644 --- a/src/scripts/ContextProviders/DisplayContextProvider.ts +++ b/src/scripts/ContextProviders/DisplayContextProvider.ts @@ -12,12 +12,17 @@ import { RenderTemplate } from '../utils/Templating.js'; * Context Provider for Display. */ export class DisplayContextProvider implements ContextProvider_Display { + /** {@link Managers/TemplateManager.TemplateManager | `TemplateManager`} instance for the {@link types/ContextProviders.ContextProvider_Display | `ContextProvider_Display`} to act on. */ + #manager: TemplateManager; + /** * Create a new {@link DisplayContextProvider | `DisplayContextProvider`}. * * @param manager - Instance of {@link TemplateManager | `TemplateManager`} to get Templates from. */ - constructor(private manager: TemplateManager) {} + constructor(manager: TemplateManager) { + this.#manager = manager; + } /** * Display an Info message to the User. @@ -27,7 +32,7 @@ export class DisplayContextProvider implements ContextProvider_Display { */ showInfo = (message: string, title?: string | undefined): void => { const body = globalThis.document.body; - const template = this.manager.context?.getId('modalMessage'); + const template = this.#manager.context?.getId('modalMessage'); if (!template) { return; @@ -62,7 +67,7 @@ export class DisplayContextProvider implements ContextProvider_Display { err.forEach(console.error); // Render `modalMessage` Template to the `body`. - const template = this.manager.context?.getId('modalMessage'); + const template = this.#manager.context?.getId('modalMessage'); if (!template) { return; diff --git a/src/scripts/ContextProviders/SettingsContextProvider.ts b/src/scripts/ContextProviders/SettingsContextProvider.ts index 59d71b8..19b0020 100644 --- a/src/scripts/ContextProviders/SettingsContextProvider.ts +++ b/src/scripts/ContextProviders/SettingsContextProvider.ts @@ -6,10 +6,12 @@ import { SettingsManager } from '../Managers/SettingsManager.js'; import { ContextProvider_Settings } from '../types/ContextProviders.js'; +import { LockHolder } from '../types/Managers.js'; import { PluginInstance, PluginSettingsBase } from '../types/Plugin.js'; import { BuildFormSchema } from '../utils/Forms/Builder.js'; import { GroupSubSchema } from '../utils/Forms/SchemaProcessors/Grouping/GroupSubSchema.js'; import { FormSchemaGrouping, ProcessedFormSchema } from '../utils/Forms/types.js'; +import { ApplicationIsLockedError } from './index.js'; import SettingsSubSchemaDefault from './schemaSettingsCore.json'; /** @@ -55,23 +57,32 @@ export class SettingsContextProvider implements ContextProvider_Settings { merge: (settings: PluginSettings) => void; /** Mapped cached store of the original Settings Schema per Plugin. */ - private pluginSchemaCacheMap: Map = new Map(); + #pluginSchemaCacheMap: Map = new Map(); /** Reference Symbol representing a Plugin for the default `FormSchemaGrouping`. */ - private ref = Symbol('built-in'); + #ref = Symbol('built-in'); + + /** Instance of {@link LockHolder | `LockHolder`} to evaluate Lock Status. */ + #lockHolder: LockHolder; + /** {@link SettingsManager | `SettingsManager`} instance for the {@link types/ContextProviders.ContextProvider_Settings | `ContextProvider_Settings`} to act on. */ + #manager: SettingsManager; /** * Creates new {@link SettingsContextProvider | `SettingsContextProvider`}. * + * @param lockHolder - Instance of {@link LockHolder | `LockHolder`} to evaluate Lock Status. * @param manager - {@link SettingsManager | `SettingsManager`} instance for the {@link types/ContextProviders.ContextProvider_Settings | `ContextProvider_Settings`} to act on. */ - constructor(private manager: SettingsManager) { + constructor(lockHolder: LockHolder, manager: SettingsManager) { + this.#lockHolder = lockHolder; + this.#manager = manager; + // Proxy properties to the Manager - this.set = this.manager.set.bind(this.manager); - this.merge = this.manager.merge.bind(this.manager); - this.get = this.manager.get.bind(this.manager); + this.set = this.#manager.set.bind(this.#manager); + this.merge = this.#manager.merge.bind(this.#manager); + this.get = this.#manager.get.bind(this.#manager); // Cache the built-in `ProcessedFormSchema` - this.pluginSchemaCacheMap.set(this.ref, SettingsSubSchemaDefault as FormSchemaGrouping); + this.#pluginSchemaCacheMap.set(this.#ref, SettingsSubSchemaDefault as FormSchemaGrouping); } /** @@ -87,8 +98,8 @@ export class SettingsContextProvider implements ContextProvider_Settings { mode: 'raw' | 'encrypted' | 'decrypted' = 'raw' ): PluginSettings | null { const settings = this.get(mode); - const defaultSchemaGrouping = this.pluginSchemaCacheMap.get(this.ref)!; - const schemaGrouping = this.pluginSchemaCacheMap.get(plugin.ref); + const defaultSchemaGrouping = this.#pluginSchemaCacheMap.get(this.#ref)!; + const schemaGrouping = this.#pluginSchemaCacheMap.get(plugin.ref); if (!schemaGrouping) { return null; @@ -113,12 +124,12 @@ export class SettingsContextProvider implements ContextProvider_Settings { */ getProcessedSchema(): ProcessedFormSchema | null { // No Plugins have registered any FormSchema's - if (0 === this.pluginSchemaCacheMap.size) { + if (0 === this.#pluginSchemaCacheMap.size) { return null; } // ! Assumes Plugins added `FormSchema`s in Priority-order - const schemaList = [...this.pluginSchemaCacheMap.values()]; + const schemaList = [...this.#pluginSchemaCacheMap.values()]; return BuildFormSchema(schemaList, this.get()); } @@ -128,8 +139,12 @@ export class SettingsContextProvider implements ContextProvider_Settings { * @param plugin - Instance of the Plugin to act on. */ unregister(plugin: PluginInstance): void { + if (this.#lockHolder.isLocked) { + throw new ApplicationIsLockedError(); + } + // Get the cached Schema for the Plugin - const schemaGrouping = this.pluginSchemaCacheMap.get(plugin.ref); + const schemaGrouping = this.#pluginSchemaCacheMap.get(plugin.ref); if (!schemaGrouping) { return; @@ -140,10 +155,10 @@ export class SettingsContextProvider implements ContextProvider_Settings { const pluginSettingsNames = Object.keys(processed.mappings.byName); // Have Manager `remove` from the Settings - pluginSettingsNames.forEach(name => this.manager.remove(name)); + pluginSettingsNames.forEach(name => this.#manager.removeSetting(name)); // Remove Plugin Schema from cache - this.pluginSchemaCacheMap.delete(plugin.ref); + this.#pluginSchemaCacheMap.delete(plugin.ref); } /** @@ -155,9 +170,13 @@ export class SettingsContextProvider implements ContextProvider_Settings { * @param schemaUrl - URL of the `FormSchemaGrouping` to load as JSON. */ async register(plugin: PluginInstance, schemaUrl: URL): Promise { - const schemaGrouping: FormSchemaGrouping = await this.manager.loadSchemaData(plugin, schemaUrl.href); + if (this.#lockHolder.isLocked) { + throw new ApplicationIsLockedError(); + } + + const schemaGrouping: FormSchemaGrouping = await this.#manager.loadSchemaData(plugin, schemaUrl.href); // Store original Schema Data for updating process cache if ever necessary - this.pluginSchemaCacheMap.set(plugin.ref, schemaGrouping); + this.#pluginSchemaCacheMap.set(plugin.ref, schemaGrouping); } } diff --git a/src/scripts/ContextProviders/StylesheetsContextProvider.ts b/src/scripts/ContextProviders/StylesheetsContextProvider.ts index bde38d5..d8bc9c9 100644 --- a/src/scripts/ContextProviders/StylesheetsContextProvider.ts +++ b/src/scripts/ContextProviders/StylesheetsContextProvider.ts @@ -5,9 +5,11 @@ */ import { ContextProvider_Stylesheets } from '../types/ContextProviders.js'; +import { LockHolder } from '../types/Managers.js'; import { PluginInstance } from '../types/Plugin.js'; import { AddStylesheet } from '../utils/DOM.js'; import { ToId } from '../utils/misc.js'; +import { ApplicationIsLockedError } from './index.js'; /** * Stylesheets Context Provider. @@ -15,6 +17,18 @@ import { ToId } from '../utils/misc.js'; * Upon Registering/Unregistering, will Add/Remove a `` tag to the CSS URL. */ export class StylesheetsContextProvider implements ContextProvider_Stylesheets { + /** Instance of {@link LockHolder | `LockHolder`} to evaluate Lock Status. */ + #lockHolder: LockHolder; + + /** + * Create a new {@link StylesheetsContextProvider | `StylesheetsContextProvider`}. + * + * @param lockHolder - Instance of {@link LockHolder | `LockHolder`} to evaluate Lock Status. + */ + constructor(lockHolder: LockHolder) { + this.#lockHolder = lockHolder; + } + /** * Unregister Stylesheets for a Plugin. * @@ -23,6 +37,10 @@ export class StylesheetsContextProvider implements ContextProvider_Stylesheets { * @param plugin - Instance of the Plugin to act on. */ unregister(plugin: PluginInstance): void { + if (this.#lockHolder.isLocked) { + throw new ApplicationIsLockedError(); + } + const id = ToId(plugin.name); // Remove link stylesheets with matching data attr @@ -38,6 +56,10 @@ export class StylesheetsContextProvider implements ContextProvider_Stylesheets { * @param styleSheetUrl - URL of the Stylesheet to load. */ register(plugin: PluginInstance, styleSheetUrl: URL): void { + if (this.#lockHolder.isLocked) { + throw new ApplicationIsLockedError(); + } + const link = AddStylesheet(styleSheetUrl.href); const id = ToId(plugin.name); diff --git a/src/scripts/ContextProviders/TemplatesContextProvider.ts b/src/scripts/ContextProviders/TemplatesContextProvider.ts index 857c703..ac5ddf7 100644 --- a/src/scripts/ContextProviders/TemplatesContextProvider.ts +++ b/src/scripts/ContextProviders/TemplatesContextProvider.ts @@ -6,8 +6,10 @@ import { TemplateManager } from '../Managers/TemplateManager.js'; import { ContextProvider_Template } from '../types/ContextProviders.js'; +import { LockHolder } from '../types/Managers.js'; import { PluginInstance } from '../types/Plugin.js'; import { TemplateIDsBase, TemplateMap } from '../utils/Templating.js'; +import { ApplicationIsLockedError } from './index.js'; /** * Context Provider for Templates. @@ -15,15 +17,43 @@ import { TemplateIDsBase, TemplateMap } from '../utils/Templating.js'; * > Once Registered, the `