Skip to content

Commit

Permalink
(#85) Custom react hook for all component that needs same backend data
Browse files Browse the repository at this point in the history
  • Loading branch information
mario4tier committed Apr 22, 2024
1 parent 1a37927 commit 123d0e3
Show file tree
Hide file tree
Showing 14 changed files with 560 additions and 454 deletions.
98 changes: 63 additions & 35 deletions typescript/vscode-extension/src/BackendSync.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,60 @@
import { SuibaseJSONStorage } from "./common/SuibaseJSONStorage";
import { API_URL, WORKDIRS_KEYS } from "./common/Consts";
import { Mutex } from "async-mutex";
import { BaseWebview } from "./bases/BaseWebview";
import { UpdateVersions } from "./common/ViewMessages";
import { SuibaseJson } from "./common/SuibaseJson";

// One instance per workdir, instantiated in same size and order as WORKDIRS_KEYS.
class BackendWorkdirTracking {
versions: SuibaseJson; // Result from getVersions backend call.

constructor() {
this.versions = new SuibaseJson();
}
}

// Readonly interface of a suibaseJSONStorage singleton.
//
// This wrapper handles also all update messages FROM the extension and
// allow for any components to set callback into it.
export class BackendSync {
private static sInstance?: BackendSync;
private mSuibaseJSONStorage?: SuibaseJSONStorage;

private mWorkdir: string; // One of "localnet", "mainnet", "testnet", "devnet".

//private mCurMethodUUID: string;
//private mCurDataUUID: string;
private mWorkdir: string; // Last known active workdir. One of "localnet", "mainnet", "testnet", "devnet".
private mWorkdirTrackings: BackendWorkdirTracking[] = []; // One instance per workdir, align with WORKDIRS_KEYS.

// Fit the VSCode initialization pattern.
// Constructor should be called only from GlobalStorage.activate().
// Release of resources done by GlobalStorage.deactivate().
// Singleton
private constructor() {
//this.mCurMethodUUID = "";
//this.mCurDataUUID = "";
this.mWorkdir = "";
this.mSuibaseJSONStorage = new SuibaseJSONStorage();

// Create one instance of BackendWorkdirTracking for each element in WORKDIRS_KEYS.
for (let workdirIdx = 0; workdirIdx < WORKDIRS_KEYS.length; workdirIdx++) {
this.mWorkdirTrackings.push(new BackendWorkdirTracking());
}
}

public static activate() {
if (!BackendSync.sInstance) {
BackendSync.getInstance();
} else {
console.error("GlobalStorage.activate() called more than once");
console.error("BackendSync.activate() called more than once");
}
}

public static deactivate() {
const instance = BackendSync.sInstance;
if (instance) {
delete instance.mSuibaseJSONStorage;
instance.mWorkdirTrackings = [];
}
delete BackendSync.sInstance;
BackendSync.sInstance = undefined;
}

public static getInstance(): BackendSync {
if (!BackendSync.sInstance) {
BackendSync.sInstance = new BackendSync();

// Initialize callback for all Webview messages.
BaseWebview.setBackendSyncMessageCallback(
BackendSync.sInstance.handleViewMessage.bind(BackendSync.sInstance)
);

// Start periodic sync with backend.
setTimeout(() => {
if (BackendSync.sInstance) {
BackendSync.sInstance.syncLoop();
Expand All @@ -60,9 +68,20 @@ export class BackendSync {
return this.mWorkdir;
}

// Allow to trig a force refresh of states handled by this loop.
public handleViewMessage(message: any): void {
try {
if (message.name === "ForceVersionsRefresh" || message.name === "InitView") {
// TODO For now just send the versions. InitView should proactively send more.
this.forceRefresh();
}
} catch (error) {
console.error(`Error in handleViewMessage: ${error}`);
}
}

// Allow to trig a force refresh of all backend states.
// Can be called safely from anywhere.
public forceLoopRefresh(): void {
public forceRefresh(): void {
this.syncLoop(true);
}

Expand All @@ -79,7 +98,7 @@ export class BackendSync {

private async asyncLoop(forceRefresh: boolean): Promise<void> {
await this.loopMutex.runExclusive(async () => {
this.update();
this.update(forceRefresh);

if (forceRefresh === false) {
// Schedule another call in one second.
Expand Down Expand Up @@ -129,6 +148,7 @@ export class BackendSync {
}

private async fetchGetVersions(workdir: string) {
// TODO Use BackendWorkdirTacking to detect and ignore out-of-order responses.
return await this.fetchBackend("getVersions", workdir);
}

Expand All @@ -143,24 +163,27 @@ export class BackendSync {
}
}

public async update() {
public async update(forceRefresh: boolean) {
// Get the global states from the backend.

// The VersionsUpdate message is periodically pushed to the views.
//
// The views then identify if they need to synchronize further with the extension
// and trig update requests messages as needed.
try {
await this.updateUsingBackend();
await this.updateUsingBackend(forceRefresh);
} catch (error) {
// Do nothing, assume the caller will retry later.
}
}

private async updateUsingBackend() {
private async updateUsingBackend(forceRefresh: boolean) {
// Do getVersions for every possible workdir.
//
// TODO Optimize to do this to retrieve all only when dashboard is visible, otherwise, just update active.
// TODO Optimize to do this to retrieve all only when dashboard is visible, otherwise just do the active?
if (forceRefresh) {
console.log("updateUsingBackend() called with forceRefresh!!!!!!!!!");
}

// Iterate the WORKDIRS_KEYS
for (let workdirIdx = 0; workdirIdx < WORKDIRS_KEYS.length; workdirIdx++) {
Expand All @@ -170,14 +193,23 @@ export class BackendSync {
try {
// This is an example of response stored in data:
// {"jsonrpc":"2.0","result":{
// "header":{"method":"getVersions", "methodUuid":"8HIGKAE8L54850LDHQ7NN9EDG0","dataUuid":"067F4QSD45QPT1BUET42FFHM0S","key":"localnet"},
// "versions":[{"method":"getWorkdirStatus","methodUuid":"ET1217DP0503LF4PFMB49J0LUC","dataUuid":"067F4QSD45QPT1BUET3JOJPQ50","key":"localnet"}],
// "header":{"method":"getVersions", "methodUuid":"...","dataUuid":"...","key":"localnet"},
// "versions":[{"method":"getWorkdirStatus","methodUuid":"...","dataUuid":"...","key":"localnet"}],
// "asuiSelection":"localnet"},
// "id":2}

// Broadcast VersionsUpdate message to all the views.
// Update the SuibaseJson instance for the workdir.
const workdirTracking = this.mWorkdirTrackings[workdirIdx];
const hasChanged = workdirTracking.versions.update(
data.result.header.methodUuid,
data.result.header.dataUuid,
data.result
);

// Broadcast UpdateVersions message to all the views when change detected or requested.
// The views will then decide if they need to synchronize further with the extension.
BaseWebview.broadcastMessage(new UpdateVersions(workdirIdx, data));
if (hasChanged || forceRefresh) {
BaseWebview.broadcastMessage(new UpdateVersions(workdirIdx, data.result));
}
} catch (error) {
const errorMsg = `Error in load_from_backend: ${error}. Data: ${JSON.stringify(data)}`;
console.error(errorMsg);
Expand All @@ -186,8 +218,4 @@ export class BackendSync {
}
}
}

// Update the data for the context requested.

// Verify if the asuiSelection match the current context, if not, then switch to it and retrieve the new context.
}
21 changes: 14 additions & 7 deletions typescript/vscode-extension/src/bases/BaseWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export class BaseWebview implements vscode.WebviewViewProvider {
// can be accessed by "name" for some message handling.
private static instances: { [key: string]: BaseWebview } = {};

// Allow BackendSync to peek at every message for all views.
private static backendSyncMessageCallback: (message: any) => void;
public static setBackendSyncMessageCallback(callback: (message: any) => void): void {
BaseWebview.backendSyncMessageCallback = callback;
}

// Allow the subclasses read-access to the panel variable.
protected getWebview() {
if (!this.panel) {
Expand All @@ -53,11 +59,6 @@ export class BaseWebview implements vscode.WebviewViewProvider {
return this.title;
}

/*
protected getExtensionUri() {
return this.extensionUri;
}*/

/**
* The BaseWebview class private constructor (called only from the derived class).
*/
Expand All @@ -83,6 +84,11 @@ export class BaseWebview implements vscode.WebviewViewProvider {

public static activate(context: vscode.ExtensionContext) {
BaseWebview.context = context;
BaseWebview.backendSyncMessageCallback = (message: any) => {
console.error(
`BaseWebview.backendSyncMessageCallback() called before set for message: ${JSON.stringify(message)}`
);
};
}

public static deactivate() {
Expand Down Expand Up @@ -116,8 +122,8 @@ export class BaseWebview implements vscode.WebviewViewProvider {
// Register message handling.
webviewView.webview.onDidReceiveMessage(
(message: any) => {
console.log("BaseWebview.onDidReceiveMessage() called");
this.handleMessage(message);
this.handleMessage(message); // For derived class to implement.
BaseWebview.backendSyncMessageCallback?.(message); // For BackendSync to peek at every message.
},
undefined,
this.disposables
Expand Down Expand Up @@ -167,6 +173,7 @@ export class BaseWebview implements vscode.WebviewViewProvider {
this.panel.webview.onDidReceiveMessage(
(message: any) => {
this.handleMessage(message);
BaseWebview.backendSyncMessageCallback?.(message); // For BackendSync to peek at every message.
},
undefined,
this.disposables
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// The purpose of the SuibaseJSONStorage is to :
// - Always get any data (even if not up to date) and revert to a default when not initialized.
// - Allow to compare two JSON storage and optionally update a target.
// - Trigger 'changes' callbacks.
// - Compare very quickly two JSON storage and optionally update the storage.
// - Trigger the 'deltaDetected' callback.
//
// This is tightly couples with how suibase-daemon synchronize its data.
type SuibaseJsonCallback = (suibaseJson: SuibaseJson) => void;
// This is a base class that handle "json" as a whole. Derived classes
// interpret the JSON for finer grained handling.

export class SuibaseJson {
public type: string;
public version: number;
public data: string;
// A change of method UUID means that delta detection using the dataUUID is
// not valid.
//
// Therefore, delta should be done by comparing the data as a whole.
public methodUUID: string;

// Allows to quickly detects delta. This is a time sortable UUID, therefore
// an update using a lower dataUUI should be ignored (out of order).
public dataUUID: string;

public json: any;

// Constructor
constructor(type: string, version: number, data: string) {
this.type = type;
this.version = version;
this.data = data;
constructor() {
this.methodUUID = "";
this.dataUUID = "";
this.json = null;
}

// Create a new SuibaseJSON from parsing a JSON string.
// Return an error message (string type) on any parsing failure.
public static fromString(jsonString: string): SuibaseJson | string {
// TODO More validation.
try {
const json = JSON.parse(jsonString);
return new SuibaseJson(json.type, json.version, json.data);
} catch (e) {
return `Error parsing JSON string: ${e} string: ${jsonString}`;
// Return true if the json has changed.
public update(methodUUID: string, dataUUID: string, json: any): boolean {
if (this.json === null || this.methodUUID !== methodUUID || dataUUID > this.dataUUID) {
this.methodUUID = methodUUID;
this.dataUUID = dataUUID;
this.json = json;
this.deltaDetected();
return true;
}
return false;
}

public update(newJSON: SuibaseJson) {
this.version = newJSON.version;
this.data = newJSON.data;
protected deltaDetected() {
// Callback handled by a derived class when a delta is detected.
//console.log(`SuibaseJson.deltaDetected() called for ${JSON.stringify(this.json)}`);
}
}

// This is to be used internally by SuibaseJSONStorage only.
/*
class StorageValue {
public suibaseJson: SuibaseJson;
public onChangeCallbacks: Array<SuibaseJsonCallback>;
Expand All @@ -45,8 +54,9 @@ class StorageValue {
this.suibaseJson = suibaseJson;
this.onChangeCallbacks = [];
}
}
}*/

/*
export class SuibaseJSONStorage {
// Map key string to SuibaseJson elements.
Expand Down Expand Up @@ -108,3 +118,4 @@ export class SuibaseJSONStorage {
return newMappedElement;
}
}
*/
18 changes: 17 additions & 1 deletion typescript/vscode-extension/src/common/ViewMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class ViewMessages {
name: string;

constructor(name: string) {
this.name = name;
this.name = name;
}
}

Expand Down Expand Up @@ -64,3 +64,19 @@ export class SuiCommand extends ViewMessages {
this.command = command;
}
}

export class InitView extends ViewMessages {
// Request the extension to send all data commonly needed by a view
// (sync with the backend as needed).
constructor() {
super("InitView");
}
}

export class ForceVersionsRefresh extends ViewMessages {
// Request the extension to send the latest Versions information.
// It is assumed the view will further request what is needed.
constructor() {
super("ForceVersionsRefresh");
}
}
15 changes: 9 additions & 6 deletions typescript/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ export function activate(context: vscode.ExtensionContext) {
// Low-level APIs
SuibaseData.activate(); // Only state/status storage (no app logic).
SuibaseExec.activate(context); // Used to perform shell commands.
BaseWebview.activate(context); // Base class for all webviews.
BaseWebview.activate(context); // Base class for all webview.

// MUST be activated after BaseWebview (because of callback initialization).
// MUST be activated before SuibaseSidebar (so it is ready before UI interaction).
//
// Data flows are:
// suibase-daemon --HTTP JSON-RPC--> BackendSync ---(update messages)---> Webview --> React States
// BackendSync <--(request messages)-- Webview <-- React States
BackendSync.activate();

// "App logic" enabled next.
SuibaseCommands.activate(context);
Expand All @@ -26,11 +34,6 @@ export function activate(context: vscode.ExtensionContext) {
SuibaseSidebar.activate(context);

// Enable getting states from the backend.
//
// Data flows are:
// suibase-daemon --HTTP JSON-RPC--> Global SuibaseData ---(updated data)---> View SuibaseData --> React States
// Global SuibaseData <--(request update)-- View SuibaseData <-- React States
BackendSync.activate();

console.log("extension activate() completed");
}
Expand Down
Loading

0 comments on commit 123d0e3

Please sign in to comment.