1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

more ipc work

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-02-22 11:50:30 -05:00
parent 170e19e7cd
commit b21d1aa4d6
6 changed files with 121 additions and 105 deletions

View File

@ -14,14 +14,6 @@ const subFrames = createTypedInvoker({
verifier: isEmptyArgs,
});
export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
ipcMain.handle(channel, listener);
}
export async function requestMain(channel: string, ...args: any[]) {
return ipcRenderer.invoke(channel, ...args);
}
function getSubFrames(): ClusterFrameInfo[] {
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
}

View File

@ -1,6 +1,6 @@
import { UpdateInfo } from "electron-updater";
import { UpdateFileInfo, ReleaseNoteInfo } from "builder-util-runtime";
import { bindTypeGuard, createUnionGuard, hasOptionalProperty, hasTypedProperty, isNull, isObject, isString, isTypedArray, isNumber, isBoolean } from "../utils/type-narrowing";
import { bindTypeGuard, unionTypeGuard, hasOptionalProperty, hasTypedProperty, isNull, isObject, isString, isTypedArray, isNumber, isBoolean } from "../utils/type-narrowing";
import { createTypedSender } from "./type-enforced-ipc";
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
@ -23,7 +23,7 @@ function isUpdateFileInfo(src: unknown): src is UpdateFileInfo {
function isReleaseNoteInfo(src: unknown): src is ReleaseNoteInfo {
return isObject(src)
&& hasTypedProperty(src, "version", isString)
&& hasTypedProperty(src, "note", createUnionGuard(isString, isNull));
&& hasTypedProperty(src, "note", unionTypeGuard(isString, isNull));
}
function isUpdateInfo(src: unknown): src is UpdateInfo {
@ -31,8 +31,8 @@ function isUpdateInfo(src: unknown): src is UpdateInfo {
&& hasTypedProperty(src, "version", isString)
&& hasTypedProperty(src, "releaseDate", isString)
&& hasTypedProperty(src, "files", bindTypeGuard(isTypedArray, isUpdateFileInfo))
&& hasOptionalProperty(src, "releaseName", createUnionGuard(isString, isNull))
&& hasOptionalProperty(src, "releaseNotes", createUnionGuard(isString, isReleaseNoteInfo, isNull))
&& hasOptionalProperty(src, "releaseName", unionTypeGuard(isString, isNull))
&& hasOptionalProperty(src, "releaseNotes", unionTypeGuard(isString, isReleaseNoteInfo, isNull))
&& hasOptionalProperty(src, "stagingPercentage", isNumber);
}

View File

@ -1,3 +1,12 @@
type TypeGuard<T> = (arg: unknown) => arg is T;
type Rest<T extends any[]> = T extends [any, ...infer R] ? R : any;
type First<T extends any[]> = T extends [infer R, ...any[]] ? R : any;
type TypeGuardReturnType<T extends (src: unknown) => src is any> = T extends (src: unknown) => src is infer R ? R : any;
type UnionTypeGuardReturnType<T extends TypeGuard<any>[]> = TypeGuardReturnType<First<T>> | (T extends [any] ? never : UnionTypeGuardReturnType<Rest<T>>);
type TupleReturnType<T extends TypeGuard<any>[]> = {
[K in keyof T]: T[K] extends TypeGuard<infer T> ? T : never
};
/**
* Narrows `val` to include the property `key` (if true is returned)
* @param val The object to be tested
@ -60,6 +69,17 @@ export function isTypedArray<T>(val: unknown, isEntry: (entry: unknown) => entry
return Array.isArray(val) && val.every(isEntry);
}
/**
* checks to see if `src` is a tuple with elements matching each of the type guards
* @param src The value to be checked
* @param typeguards the list of type-guards to check each element
*/
export function isTuple<T extends TypeGuard<any>[]>(src: unknown, ...typeguards: T): src is TupleReturnType<T> {
return Array.isArray(src)
&& (src.length <= typeguards.length)
&& typeguards.every((typeguard, i) => typeguard(src[i]));
}
/**
* checks if val is of type string
* @param val the value to be checked
@ -112,30 +132,28 @@ export function isNull(val: unknown): val is null {
* ```
* bindTypeGuard(isTypedArray, isString); // Predicate<string[]>
* bindTypeGuard(isRecord, isString, isBoolean); // Predicate<Record<string, boolean>>
* bindTypeGuard(isTuple, isString, isBoolean); // Predicate<[string, boolean]>
*
* Note: this function does not currently nest as a direct argument to itself.
* It needs to be extracted to a variable for typescript's type checker to work.
* ```
*/
export function bindTypeGuard<FnArgs extends any[], T>(fn: (arg1: unknown, ...args: FnArgs) => arg1 is T, ...boundArgs: FnArgs): Predicate<T> {
export function bindTypeGuard<FnArgs extends any[], T>(fn: (arg1: unknown, ...args: FnArgs) => arg1 is T, ...boundArgs: FnArgs): TypeGuard<T> {
return (arg1: unknown): arg1 is T => fn(arg1, ...boundArgs);
}
type Predicate<T> = (arg: unknown) => arg is T;
type Rest<T extends any[]> = T extends [any, ...infer R] ? R : any;
type First<T extends any[]> = T extends [infer R, ...any[]] ? R : any;
type ReturnPredicateType<T extends (src: unknown) => src is any> = T extends (src: unknown) => src is infer R ? R : any;
type OrReturnPredicateType<T extends Predicate<any>[]> = ReturnPredicateType<First<T>> | (T extends [any] ? never : OrReturnPredicateType<Rest<T>>);
/**
* Create a new type-guard for the union of the types that each of the
* predicates are type-guarding for
* @param predicates a list of predicates that should be executed in order
* @param typeGuards a list of predicates that should be executed in order
*
* Example:
* ```
* createUnionGuard(isString, isBoolean); // Predicate<string | boolean>
* ```
*/
export function createUnionGuard<Predicates extends Predicate<any>[]>(...predicates: Predicates): Predicate<OrReturnPredicateType<Predicates>> {
return (arg: unknown): arg is OrReturnPredicateType<Predicates> => {
return predicates.some(predicate => predicate(arg));
export function unionTypeGuard<TypeGuards extends TypeGuard<any>[]>(...typeGuards: TypeGuards): TypeGuard<UnionTypeGuardReturnType<TypeGuards>> {
return (arg: unknown): arg is UnionTypeGuardReturnType<TypeGuards> => {
return typeGuards.some(typeguard => typeguard(arg));
};
}

View File

@ -7,26 +7,36 @@ import os from "os";
import path from "path";
import { createTypedInvoker, createTypedSender, isEmptyArgs } from "../common/ipc";
import { getBundledExtensions } from "../common/utils/app-version";
import { hasTypedProperty, isBoolean } from "../common/utils/type-narrowing";
import { bindTypeGuard, hasTypedProperty, isBoolean, isObject, isString, isTuple } from "../common/utils/type-narrowing";
import logger from "../main/logger";
import { extensionInstaller, PackageJson } from "./extension-installer";
import { extensionsStore } from "./extensions-store";
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
import { LensExtensionId, LensExtensionManifest, isLensExtensionManifest } from "./lens-extension";
export interface InstalledExtension {
id: LensExtensionId;
id: LensExtensionId;
readonly manifest: LensExtensionManifest;
readonly manifest: LensExtensionManifest;
// Absolute path to the non-symlinked source folder,
// e.g. "/Users/user/.k8slens/extensions/helloworld"
readonly absolutePath: string;
// Absolute path to the non-symlinked source folder,
// e.g. "/Users/user/.k8slens/extensions/helloworld"
readonly absolutePath: string;
// Absolute to the symlinked package.json file
readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json
isEnabled: boolean;
}
// Absolute to the symlinked package.json file
readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json
isEnabled: boolean;
}
export function isInstalledExtension(src: unknown): src is InstalledExtension {
return isObject(src)
&& hasTypedProperty(src, "id", isString)
&& hasTypedProperty(src, "manifest", isLensExtensionManifest)
&& hasTypedProperty(src, "absolutePath", isString)
&& hasTypedProperty(src, "manifestPath", isString)
&& hasTypedProperty(src, "isBundled", isBoolean)
&& hasTypedProperty(src, "isEnabled", isBoolean);
}
const logModule = "[EXTENSION-DISCOVERY]";
@ -34,11 +44,6 @@ export const manifestFilename = "package.json";
type DiscoveryLoadingState = [isLoaded: boolean];
function isExtensionDiscoveryChannelMessage(args: unknown[]): args is DiscoveryLoadingState {
return hasTypedProperty(args, 0, isBoolean)
&& args.length === 0;
}
/**
* Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
* @param lstat the stats to compare
@ -49,7 +54,7 @@ function isDirectoryLike(lstat: fs.Stats): boolean {
const extensionDiscoveryState = createTypedSender({
channel: "extension-discovery:state",
verifier: isExtensionDiscoveryChannelMessage,
verifier: bindTypeGuard(isTuple, isBoolean),
});
function ExtensionDiscoveryInitState(): boolean {

View File

@ -4,7 +4,7 @@ import { isEqual } from "lodash";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import path from "path";
import { getHostedCluster } from "../common/cluster-store";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { createTypedInvoker, createTypedSender, isEmptyArgs } from "../common/ipc";
import logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery";
import { extensionsStore } from "./extensions-store";
@ -13,6 +13,7 @@ import type { LensMainExtension } from "./lens-main-extension";
import type { LensRendererExtension } from "./lens-renderer-extension";
import * as registries from "./registries";
import fs from "fs";
import { bindTypeGuard, isString, isTuple, isTypedArray } from "../common/utils/type-narrowing";
// lazy load so that we get correct userData
export function extensionPackagesRoot() {
@ -21,6 +22,23 @@ export function extensionPackagesRoot() {
const logModule = "[EXTENSIONS-LOADER]";
type InstalledExtensions = [extId: string, metadata: InstalledExtension][];
function isInstalledExtensions(args: unknown[]): args is InstalledExtensions {
return isTypedArray(args, bindTypeGuard(isTuple, isString, isInstalledExtensions));
}
const installedExtensions = createTypedSender({
channel: "extensions:installed",
verifier: bindTypeGuard(isTuple, isInstalledExtensions),
});
const initialInstalledExtensions = createTypedInvoker({
channel: "extensions:initial-installed",
verifier: isEmptyArgs,
handler: () => extensionLoader.toBroadcastData(),
});
/**
* Loads installed extensions to the Lens application
*/
@ -28,12 +46,6 @@ export class ExtensionLoader {
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>();
// IPC channel to broadcast changes to extensions from main
protected static readonly extensionsMainChannel = "extensions:main";
// IPC channel to broadcast changes to extensions from renderer
protected static readonly extensionsRendererChannel = "extensions:renderer";
// emits event "remove" of type LensExtension when the extension is removed
private events = new EventEmitter();
@ -55,20 +67,24 @@ export class ExtensionLoader {
// Transform userExtensions to a state object for storing into ExtensionsStore
@computed get storeState() {
return Object.fromEntries(
Array.from(this.userExtensions)
.map(([extId, extension]) => [extId, {
enabled: extension.isEnabled,
name: extension.manifest.name,
}])
Array.from(this.userExtensions, ([extId, extension]) => [extId, {
enabled: extension.isEnabled,
name: extension.manifest.name,
}])
);
}
@action
async init() {
installedExtensions.on((event, extensions) => {
this.isLoaded = true;
this.syncExtensions(extensions);
});
reaction(() => this.toBroadcastData(), installedExtensions.broadcast);
if (ipcRenderer) {
await this.initRenderer();
} else {
await this.initMain();
}
await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]);
@ -113,54 +129,33 @@ export class ExtensionLoader {
}
}
protected async initMain() {
this.isLoaded = true;
this.loadOnMain();
reaction(() => this.toJSON(), () => {
this.broadcastExtensions();
});
handleRequest(ExtensionLoader.extensionsMainChannel, () => {
return Array.from(this.toJSON());
});
subscribeToBroadcast(ExtensionLoader.extensionsRendererChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
this.syncExtensions(extensions);
});
}
protected async initRenderer() {
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
this.isLoaded = true;
this.syncExtensions(extensions);
const initial = await initialInstalledExtensions.invoke();
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
// Remove deleted extensions in renderer side only
this.extensions.forEach((_, lensExtensionId) => {
if (!receivedExtensionIds.includes(lensExtensionId)) {
this.removeExtension(lensExtensionId);
}
});
};
reaction(() => this.toJSON(), () => {
this.broadcastExtensions(false);
});
requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler);
subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
extensionListHandler(extensions);
});
this.isLoaded = true;
this.syncExtensions(initial);
}
syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) {
extensions.forEach(([lensExtensionId, extension]) => {
if (!isEqual(this.extensions.get(lensExtensionId), extension)) {
this.extensions.set(lensExtensionId, extension);
@action
protected syncExtensions(extensions: InstalledExtensions) {
const receivedExtIds = new Set();
for (const [extId, metadata] of extensions) {
receivedExtIds.add(extId);
if (!isEqual(this.extensions.get(extId), metadata)) {
this.extensions.set(extId, metadata);
}
});
}
if (ipcRenderer) {
// Remove deleted extensions in renderer side only
for (const extId of this.extensions.keys()) {
if (!receivedExtIds.has(extId)) {
this.removeExtension(extId);
}
}
}
}
loadOnMain() {
@ -236,7 +231,7 @@ export class ExtensionLoader {
}
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) {
return reaction(() => this.toJSON(), installedExtensions => {
return reaction(() => this.toBroadcastData(), installedExtensions => {
for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId);
@ -294,16 +289,12 @@ export class ExtensionLoader {
return this.extensions.get(extId);
}
toJSON(): Map<LensExtensionId, InstalledExtension> {
return toJS(this.extensions, {
toBroadcastData(): [LensExtensionId, InstalledExtension][] {
return toJS(Array.from(this.extensions.entries()), {
exportMapsAsObjects: false,
recurseEverything: true,
});
}
broadcastExtensions(main = true) {
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
}
}
export const extensionLoader = new ExtensionLoader();

View File

@ -2,6 +2,7 @@ import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx";
import { filesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
import { hasOptionalProperty, hasTypedProperty, isObject, isString } from "../common/utils/type-narrowing";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@ -15,6 +16,15 @@ export interface LensExtensionManifest {
lens?: object; // fixme: add more required fields for validation
}
export function isLensExtensionManifest(src: unknown): src is LensExtensionManifest {
return isObject(src)
&& hasTypedProperty(src, "version", isString)
&& hasOptionalProperty(src, "description", isString)
&& hasOptionalProperty(src, "main", isString)
&& hasOptionalProperty(src, "renderer", isString)
&& hasOptionalProperty(src, "lens", isObject);
}
export class LensExtension {
readonly id: LensExtensionId;
readonly manifest: LensExtensionManifest;