Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented StatusController and StatusWriter for setting/updating the status of modules #416

Merged
merged 11 commits into from
Oct 13, 2023
5 changes: 5 additions & 0 deletions frontend/src/framework/ModuleContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";

import { BroadcastChannel } from "./Broadcaster";
import { ModuleInstance } from "./ModuleInstance";
import { ModuleInstanceStatusController } from "./ModuleInstanceStatusController";
import { StateBaseType, StateStore, useSetStoreValue, useStoreState, useStoreValue } from "./StateStore";
import { SyncSettingKey } from "./SyncSettings";

Expand Down Expand Up @@ -56,4 +57,8 @@ export class ModuleContext<S extends StateBaseType> {
setInstanceTitle(title: string): void {
this._moduleInstance.setTitle(title);
}

getStatusController(): ModuleInstanceStatusController {
return this._moduleInstance.getStatusController();
}
}
7 changes: 7 additions & 0 deletions frontend/src/framework/ModuleInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ModuleContext } from "./ModuleContext";
import { StateBaseType, StateOptions, StateStore } from "./StateStore";
import { SyncSettingKey } from "./SyncSettings";
import { Workbench } from "./Workbench";
import { ModuleInstanceStatusControllerInternal } from "./internal/ModuleInstanceStatusControllerInternal";

export enum ModuleInstanceState {
INITIALIZING,
Expand All @@ -35,6 +36,7 @@ export class ModuleInstance<StateType extends StateBaseType> {
private _cachedDefaultState: StateType | null;
private _cachedStateStoreOptions?: StateOptions<StateType>;
private _initialSettings: InitialSettings | null;
private _statusController: ModuleInstanceStatusControllerInternal;

constructor(
module: Module<StateType>,
Expand All @@ -57,6 +59,7 @@ export class ModuleInstance<StateType extends StateBaseType> {
this._fatalError = null;
this._cachedDefaultState = null;
this._initialSettings = null;
this._statusController = new ModuleInstanceStatusControllerInternal();

this._broadcastChannels = {} as Record<string, BroadcastChannel>;

Expand Down Expand Up @@ -178,6 +181,10 @@ export class ModuleInstance<StateType extends StateBaseType> {
return this._module;
}

getStatusController(): ModuleInstanceStatusControllerInternal {
return this._statusController;
}

subscribeToImportStateChange(cb: () => void): () => void {
this._importStateSubscribers.add(cb);
return () => {
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/framework/ModuleInstanceStatusController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export enum StatusMessageType {
Warning = "warning",
Error = "error",
}

export enum StatusSource {
View = "view",
Settings = "settings",
}

export interface ModuleInstanceStatusController {
addMessage(source: StatusSource, message: string, type: StatusMessageType): void;
clearMessages(source: StatusSource): void;
setLoading(isLoading: boolean): void;

setDebugMessage(source: StatusSource, message: string): void;
incrementReportedComponentRenderCount(source: StatusSource): void;

reviseAndPublishState(): void;
}
78 changes: 78 additions & 0 deletions frontend/src/framework/StatusWriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from "react";

import { ModuleContext } from "./ModuleContext";
import { ModuleInstanceStatusController, StatusMessageType, StatusSource } from "./ModuleInstanceStatusController";

export class ViewStatusWriter {
private _statusController: ModuleInstanceStatusController;

constructor(statusController: ModuleInstanceStatusController) {
this._statusController = statusController;
}

setLoading(isLoading: boolean): void {
this._statusController.setLoading(isLoading);
}

addError(message: string): void {
this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Error);
}

addWarning(message: string): void {
this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Warning);
}

setDebugMessage(message: string): void {
this._statusController.setDebugMessage(StatusSource.View, message);
}
}

export class SettingsStatusWriter {
private _statusController: ModuleInstanceStatusController;

constructor(statusController: ModuleInstanceStatusController) {
this._statusController = statusController;
}

addError(message: string): void {
this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Error);
}

addWarning(message: string): void {
this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Warning);
}

setDebugMessage(message: string): void {
this._statusController.setDebugMessage(StatusSource.Settings, message);
}
}

export function useViewStatusWriter(moduleContext: ModuleContext<any>): ViewStatusWriter {
const statusController = moduleContext.getStatusController();

const statusWriter = React.useRef<ViewStatusWriter>(new ViewStatusWriter(statusController));

statusController.clearMessages(StatusSource.View);
statusController.incrementReportedComponentRenderCount(StatusSource.View);

React.useEffect(function handleRender() {
statusController.reviseAndPublishState();
});

return statusWriter.current;
}

export function useSettingsStatusWriter(moduleContext: ModuleContext<any>): ViewStatusWriter {
const statusController = moduleContext.getStatusController();

const statusWriter = React.useRef<ViewStatusWriter>(new ViewStatusWriter(statusController));

statusController.clearMessages(StatusSource.Settings);
statusController.incrementReportedComponentRenderCount(StatusSource.Settings);

React.useEffect(function handleRender() {
statusController.reviseAndPublishState();
});

return statusWriter.current;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React from "react";

import {
ModuleInstanceStatusController,
StatusMessageType,
StatusSource,
} from "@framework/ModuleInstanceStatusController";

import { cloneDeep, filter, isEqual, keys } from "lodash";

type StatusMessage = {
source: StatusSource;
message: string;
type: StatusMessageType;
};

type StatusControllerState = {
messages: StatusMessage[];
loading: boolean;
viewDebugMessage: string;
settingsDebugMessage: string;
viewRenderCount: number | null;
settingsRenderCount: number | null;
};

export class ModuleInstanceStatusControllerInternal implements ModuleInstanceStatusController {
protected _stateCandidates: StatusControllerState;
protected _state: StatusControllerState = {
messages: [],
loading: false,
viewDebugMessage: "",
settingsDebugMessage: "",
viewRenderCount: null,
settingsRenderCount: null,
};
private _subscribers: Map<keyof StatusControllerState, Set<() => void>> = new Map();

constructor() {
this._stateCandidates = cloneDeep(this._state);
}

addMessage(source: StatusSource, message: string, type: StatusMessageType): void {
this._stateCandidates.messages.push({
source,
message,
type,
});
}

clearMessages(source: StatusSource): void {
this._stateCandidates.messages = this._stateCandidates.messages.filter((msg) => msg.source !== source);
}

setLoading(isLoading: boolean): void {
this._stateCandidates.loading = isLoading;
}

setDebugMessage(source: StatusSource, message: string): void {
if (source === StatusSource.View) {
this._stateCandidates.viewDebugMessage = message;
}
if (source === StatusSource.Settings) {
this._stateCandidates.settingsDebugMessage = message;
}
}

incrementReportedComponentRenderCount(source: StatusSource): void {
if (source === StatusSource.View) {
if (this._stateCandidates.viewRenderCount === null) {
this._stateCandidates.viewRenderCount = 0;
}
this._stateCandidates.viewRenderCount++;
}
if (source === StatusSource.Settings) {
if (this._stateCandidates.settingsRenderCount === null) {
this._stateCandidates.settingsRenderCount = 0;
}
this._stateCandidates.settingsRenderCount++;
}
}

reviseAndPublishState(): void {
const differentStateKeys = filter(keys(this._stateCandidates), (key: keyof StatusControllerState) => {
return !isEqual(this._state[key], this._stateCandidates[key]);
}) as (keyof StatusControllerState)[];

this._state = cloneDeep(this._stateCandidates);

differentStateKeys.forEach((stateKey) => {
this.notifySubscribers(stateKey);
});
}

private notifySubscribers(stateKey: keyof StatusControllerState): void {
const subscribers = this._subscribers.get(stateKey);
if (subscribers) {
subscribers.forEach((subscriber) => {
subscriber();
});
}
}

makeSnapshotGetter<T extends keyof StatusControllerState>(stateKey: T): () => StatusControllerState[T] {
const snapshotGetter = (): any => {
return this._state[stateKey];
};

return snapshotGetter;
}

makeSubscriberFunction<T extends keyof StatusControllerState>(
stateKey: T
): (onStoreChangeCallback: () => void) => () => void {
// Using arrow function in order to keep "this" in context
const subscriber = (onStoreChangeCallback: () => void): (() => void) => {
const subscribers = this._subscribers.get(stateKey) || new Set();
subscribers.add(onStoreChangeCallback);
this._subscribers.set(stateKey, subscribers);

return () => {
subscribers.delete(onStoreChangeCallback);
};
};

return subscriber;
}
}

export function useStatusControllerStateValue<T extends keyof StatusControllerState>(
statusController: ModuleInstanceStatusControllerInternal,
stateKey: T
): StatusControllerState[T] {
const value = React.useSyncExternalStore<StatusControllerState[T]>(
statusController.makeSubscriberFunction(stateKey),
statusController.makeSnapshotGetter(stateKey)
);

return value;
}
Loading