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:
parent
170e19e7cd
commit
b21d1aa4d6
@ -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 });
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user