From 5d09d7a1265626bf918382b3872eb02713d0b6d4 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 6 May 2021 17:42:53 +0300 Subject: [PATCH] mobx-6: use toJS()-wrapper since monkey-patching require(mobx).toJS doesn't work Signed-off-by: Roman --- docs/extensions/guides/stores.md | 6 ++--- .../catalog/catalog-category-registry.ts | 3 ++- src/common/cluster-store.ts | 4 +-- src/common/configure-packages.ts | 20 +-------------- src/common/hotbar-store.ts | 3 ++- src/common/ipc/ipc.ts | 25 ++++++++++++------- src/common/user-store.ts | 4 +-- src/common/utils/__tests__/toJS.test.ts | 25 +++++++++++++++++++ src/common/utils/index.ts | 1 + src/common/utils/toJS.ts | 18 +++++++++++++ src/common/utils/toggle-set.ts | 9 +------ src/extensions/extension-discovery.ts | 4 +-- src/extensions/extensions-store.ts | 3 ++- src/main/catalog-pusher.ts | 4 +-- src/main/cluster-manager.ts | 11 +++----- src/main/cluster.ts | 12 ++------- src/main/extension-filesystem.ts | 3 ++- .../+workloads-pods/pod-details.tsx | 4 +-- .../command-palette/command-dialog.tsx | 7 ++---- .../components/dock/dock-tab.store.ts | 4 +-- src/renderer/components/dock/terminal.ts | 4 +-- 21 files changed, 95 insertions(+), 79 deletions(-) create mode 100644 src/common/utils/__tests__/toJS.test.ts create mode 100644 src/common/utils/toJS.ts diff --git a/docs/extensions/guides/stores.md b/docs/extensions/guides/stores.md index 9b8fdbb556..44f231667c 100644 --- a/docs/extensions/guides/stores.md +++ b/docs/extensions/guides/stores.md @@ -50,9 +50,9 @@ export class ExamplePreferencesStore extends Store.ExtensionStore any) { - ipcMain.handle(channel, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => { - args = toJS(args); // unwrap possibly leaking observable values + ipcMain.handle(channel, async (event, ...args) => { + const payload = await listener(event, ...args); - return listener(event, ...args); + return sanitizePayload(payload); }); } export async function requestMain(channel: string, ...args: any[]) { - return ipcRenderer.invoke(channel, ...toJS(args)); + return ipcRenderer.invoke(channel, ...args.map(sanitizePayload)); } function getSubFrames(): ClusterFrameInfo[] { - return toJS(Array.from(clusterFrameMap.values())); + return Array.from(clusterFrameMap.values()); } export async function broadcastMessage(channel: string, ...args: any[]) { const views = (webContents || remote?.webContents)?.getAllWebContents(); if (!views) return; + args = args.map(sanitizePayload); if (ipcRenderer) { ipcRenderer.send(channel, ...args); @@ -83,7 +84,13 @@ export function unsubscribeAllFromBroadcast(channel: string) { } export function bindBroadcastHandlers() { - handleRequest(subFramesChannel, () => { - return getSubFrames(); - }); + handleRequest(subFramesChannel, () => getSubFrames()); +} + +/** + * Sanitizing data for IPC-messaging before send. + * Removes possible observable values to avoid exceptions like "can't clone object". + */ +function sanitizePayload(data: any): T { + return toJS(data); } diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 78dbf7f5a3..3406807acf 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -2,7 +2,7 @@ import type { ThemeId } from "../renderer/theme.store"; import { app, remote } from "electron"; import semver from "semver"; import { readFile } from "fs-extra"; -import { action, computed, observable, reaction, toJS, makeObservable } from "mobx"; +import { action, computed, observable, reaction, makeObservable } from "mobx"; import moment from "moment-timezone"; import { BaseStore } from "./base-store"; import migrations from "../migrations/user-store"; @@ -13,7 +13,7 @@ import logger from "../main/logger"; import path from "path"; import os from "os"; import { fileNameMigration } from "../migrations/user-store"; -import { ObservableToggleSet } from "../renderer/utils"; +import { ObservableToggleSet, toJS } from "../renderer/utils"; export interface UserStoreModel { kubeConfigPath: string; diff --git a/src/common/utils/__tests__/toJS.test.ts b/src/common/utils/__tests__/toJS.test.ts new file mode 100644 index 0000000000..70ea6a2735 --- /dev/null +++ b/src/common/utils/__tests__/toJS.test.ts @@ -0,0 +1,25 @@ +import { isObservable, observable } from "mobx"; +import { toJS } from "../toJS"; + +describe("utils/toJS(data: any)", () => { + const y = { y: 2 }; + + const data = observable({ x: 1, y }, {}, { + deep: false, // this will keep ref to "y" + }); + const data2 = { + x: 1, // partially observable + y: observable(y), + }; + + test("converts mobx-observable to corresponding js struct with links preserving", () => { + expect(toJS(data).y).toBe(y); + expect(isObservable(toJS(data).y)).toBeFalsy(); + }); + + test("converts partially observable js struct", () => { + expect(toJS(data2).y).not.toBe(y); + expect(toJS(data2).y).toEqual(y); + expect(isObservable(toJS(data2).y)).toBeFalsy(); + }); +}); diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index d4467db958..57dd6e1cbc 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -6,6 +6,7 @@ export * from "./app-version"; export * from "./autobind"; export * from "./base64"; export * from "./camelCase"; +export * from "./toJS"; export * from "./cloneJson"; export * from "./debouncePromise"; export * from "./defineGlobal"; diff --git a/src/common/utils/toJS.ts b/src/common/utils/toJS.ts new file mode 100644 index 0000000000..8ea100c7cf --- /dev/null +++ b/src/common/utils/toJS.ts @@ -0,0 +1,18 @@ +/** + * Wrapper for mobx.toJS() to support partially observable objects as data-input (>= mobx6). + * Otherwise, output result won't be recursively converted to corresponding plain JS-structure. + * + * @example + * mobx.toJS({one: 1, two: observable.array([2])}); // "data.two" == ObservableArray + */ +import * as mobx from "mobx"; +import { isObservable, observable } from "mobx"; + +export function toJS(data: T): T { + // make data observable for recursive toJS()-output + if (typeof data === "object" && !isObservable(data)) { + return mobx.toJS(observable.box(data).get()); + } + + return mobx.toJS(data); +} diff --git a/src/common/utils/toggle-set.ts b/src/common/utils/toggle-set.ts index 7f73dc8564..adea8735da 100644 --- a/src/common/utils/toggle-set.ts +++ b/src/common/utils/toggle-set.ts @@ -1,4 +1,4 @@ -import { action, IEnhancer, IObservableSetInitialValues, makeObservable, ObservableSet } from "mobx"; +import { ObservableSet } from "mobx"; export class ToggleSet extends Set { public toggle(value: T): void { @@ -10,13 +10,6 @@ export class ToggleSet extends Set { } export class ObservableToggleSet extends ObservableSet { - constructor(data?: IObservableSetInitialValues, enhancer?: IEnhancer) { - super(data, enhancer); - - makeObservable(this); - } - - @action public toggle(value: T): void { if (!this.delete(value)) { // Set.prototype.delete returns false if `value` was not in the set diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 6a7c33cf3b..2a4e1f3ccd 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -2,11 +2,11 @@ import { watch } from "chokidar"; import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; import fse from "fs-extra"; -import { observable, reaction, toJS, when, makeObservable } from "mobx"; +import { observable, reaction, when, makeObservable } from "mobx"; import os from "os"; import path from "path"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; -import { Singleton } from "../common/utils"; +import { Singleton, toJS } from "../common/utils"; import logger from "../main/logger"; import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store"; import { extensionInstaller, PackageJson } from "./extension-installer"; diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index 083e07bcc2..0fdb8fb87f 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -1,6 +1,7 @@ import type { LensExtensionId } from "./lens-extension"; import { BaseStore } from "../common/base-store"; -import { action, computed, observable, toJS, makeObservable } from "mobx"; +import { action, computed, observable, makeObservable } from "mobx"; +import { toJS } from "../common/utils"; export interface LensExtensionsStoreModel { extensions: Record; diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts index 8522dea778..fc68b9f8da 100644 --- a/src/main/catalog-pusher.ts +++ b/src/main/catalog-pusher.ts @@ -1,8 +1,8 @@ -import { reaction, toJS } from "mobx"; +import { reaction } from "mobx"; import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "../common/ipc"; import { CatalogEntityRegistry} from "../common/catalog"; import "../common/catalog-entities/kubernetes-cluster"; -import { Disposer } from "../common/utils"; +import { Disposer, toJS } from "../common/utils"; export class CatalogPusher { static init(catalog: CatalogEntityRegistry) { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 5ca5e38b7e..be8b726aae 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,12 +1,12 @@ import "../common/cluster-ipc"; import type http from "http"; import { ipcMain } from "electron"; -import { action, autorun, reaction, toJS, makeObservable } from "mobx"; +import { action, autorun, makeObservable, reaction } from "mobx"; import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store"; import { Cluster } from "./cluster"; import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; -import { Singleton } from "../common/utils"; +import { Singleton, toJS } from "../common/utils"; import { catalogEntityRegistry } from "../common/catalog"; import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster"; @@ -24,7 +24,6 @@ export class ClusterManager extends Singleton { this.syncClustersFromCatalog(entities); }); - // auto-stop removed clusters autorun(() => { const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values()); @@ -143,9 +142,7 @@ export class ClusterManager extends Singleton { } export function catalogEntityFromCluster(cluster: Cluster) { - return new KubernetesCluster(toJS({ - apiVersion: "entity.k8slens.dev/v1alpha1", - kind: "KubernetesCluster", + return new KubernetesCluster({ metadata: { uid: cluster.id, name: cluster.name, @@ -164,5 +161,5 @@ export function catalogEntityFromCluster(cluster: Cluster) { message: "", active: !cluster.disconnected } - })); + }); } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 58c6bb96b3..01dfdf5301 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,15 +1,6 @@ import { ipcMain } from "electron"; import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store"; -import { - action, - comparer, - computed, - observable, - reaction, - toJS, - when, - makeObservable, -} from "mobx"; +import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; import { ContextHandler } from "./context-handler"; import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; @@ -21,6 +12,7 @@ import logger from "./logger"; import { VersionDetector } from "./cluster-detectors/version-detector"; import { detectorRegistry } from "./cluster-detectors/detector-registry"; import plimit from "p-limit"; +import { toJS } from "../common/utils"; export enum ClusterStatus { AccessGranted = 2, diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index 0d0d1a672a..752113692f 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -2,10 +2,11 @@ import { randomBytes } from "crypto"; import { SHA256 } from "crypto-js"; import { app, remote } from "electron"; import fse from "fs-extra"; -import { action, observable, toJS, makeObservable } from "mobx"; +import { action, observable, makeObservable } from "mobx"; import path from "path"; import { BaseStore } from "../common/base-store"; import { LensExtensionId } from "../extensions/lens-extension"; +import { toJS } from "../common/utils"; interface FSProvisionModel { extensions: Record; // extension names to paths diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index d296a39620..ec907f77cb 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -4,11 +4,11 @@ import React from "react"; import kebabCase from "lodash/kebabCase"; import { disposeOnUnmount, observer } from "mobx-react"; import { Link } from "react-router-dom"; -import { autorun, observable, reaction, toJS, makeObservable } from "mobx"; +import { autorun, observable, reaction, makeObservable } from "mobx"; import { IPodMetrics, nodesApi, Pod, pvcApi, configMapApi } from "../../api/endpoints"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Badge } from "../badge"; -import { boundMethod, cssNames, interval } from "../../utils"; +import { boundMethod, cssNames, interval, toJS } from "../../utils"; import { PodDetailsContainer } from "./pod-details-container"; import { PodDetailsAffinities } from "./pod-details-affinities"; import { PodDetailsTolerations } from "./pod-details-tolerations"; diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index 389a540145..94eddfdaa0 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -1,6 +1,5 @@ - import { Select } from "../select"; -import { computed, observable, toJS, makeObservable } from "mobx"; +import { computed, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; import React from "react"; import { commandRegistry } from "../../../extensions/registries/command-registry"; @@ -52,13 +51,11 @@ export class CommandDialog extends React.Component { return; } - const action = toJS(command.action); - try { CommandOverlay.close(); if (command.scope === "global") { - action({ + command.action({ entity: commandRegistry.activeEntity }); } else if(commandRegistry.activeEntity) { diff --git a/src/renderer/components/dock/dock-tab.store.ts b/src/renderer/components/dock/dock-tab.store.ts index 0757b8212f..39e13c3c2d 100644 --- a/src/renderer/components/dock/dock-tab.store.ts +++ b/src/renderer/components/dock/dock-tab.store.ts @@ -1,5 +1,5 @@ -import { autorun, observable, reaction, toJS } from "mobx"; -import { autoBind, createStorage, StorageHelper } from "../../utils"; +import { autorun, observable, reaction } from "mobx"; +import { autoBind, createStorage, StorageHelper, toJS } from "../../utils"; import { dockStore, TabId } from "./dock.store"; export interface DockTabStoreOptions { diff --git a/src/renderer/components/dock/terminal.ts b/src/renderer/components/dock/terminal.ts index 33d6929537..e8b1408edc 100644 --- a/src/renderer/components/dock/terminal.ts +++ b/src/renderer/components/dock/terminal.ts @@ -1,11 +1,11 @@ import debounce from "lodash/debounce"; -import { reaction, toJS } from "mobx"; +import { reaction } from "mobx"; import { Terminal as XTerm } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import { dockStore, TabId } from "./dock.store"; import { TerminalApi } from "../../api/terminal-api"; import { ThemeStore } from "../../theme.store"; -import { boundMethod } from "../../utils"; +import { boundMethod, toJS } from "../../utils"; import { isMac } from "../../../common/vars"; import { camelCase } from "lodash";