Skip to content

Commit

Permalink
Implemented StatusController and StatusWriter for setting/updatin…
Browse files Browse the repository at this point in the history
…g the status of modules (#416)
  • Loading branch information
rubenthoms authored Oct 13, 2023
1 parent 1fb813b commit 120053c
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 48 deletions.
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

0 comments on commit 120053c

Please sign in to comment.