diff --git a/src/common/catalog/catalog-category-registry.ts b/src/common/catalog/catalog-category-registry.ts index 4d87437ff4..1ed990a57a 100644 --- a/src/common/catalog/catalog-category-registry.ts +++ b/src/common/catalog/catalog-category-registry.ts @@ -19,38 +19,42 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, computed, makeObservable, observable } from "mobx"; +import { action, computed, observable, makeObservable } from "mobx"; +import { Disposer, ExtendedMap } from "../utils"; import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity"; export class CatalogCategoryRegistry { - protected categories = observable.array(); + protected categories = observable.set(); constructor() { makeObservable(this); } - @action - add(category: CatalogCategory) { - this.categories.push(category); + @action add(category: CatalogCategory): Disposer { + this.categories.add(category); + + return () => this.categories.delete(category); } - @action - remove(category: CatalogCategory) { - const categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind); + @computed private get groupKindLookup(): Map> { + // ExtendedMap has the convenience methods `getOrInsert` and `strictSet` + const res = new ExtendedMap>(); - this.categories.replace(categories); + for (const category of this.categories) { + res + .getOrInsert(category.spec.group, ExtendedMap.new) + .strictSet(category.spec.names.kind, category); + } + + return res; } - @computed get items(): CatalogCategory[] { + @computed get items() { return Array.from(this.categories); } - getById(id: string): CatalogCategory{ - return this.categories.find(category => category.getId() === id); - } - - getForGroupKind(group: string, kind: string) { - return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T; + getForGroupKind(group: string, kind: string): T | undefined { + return this.groupKindLookup.get(group)?.get(kind) as T; } getEntityForData(data: CatalogEntityData & CatalogEntityKindData) { @@ -72,17 +76,11 @@ export class CatalogCategoryRegistry { return new specVersion.entityClass(data); } - getCategoryForEntity(data: CatalogEntityData & CatalogEntityKindData) { + getCategoryForEntity(data: CatalogEntityData & CatalogEntityKindData): T | undefined { const splitApiVersion = data.apiVersion.split("/"); const group = splitApiVersion[0]; - const category = this.categories.find((category) => { - return category.spec.group === group && category.spec.names.kind === data.kind; - }); - - if (!category) return null; - - return category as T; + return this.getForGroupKind(group, data.kind); } } diff --git a/src/common/utils/extended-map.ts b/src/common/utils/extended-map.ts index 5551d84429..86ff291fad 100644 --- a/src/common/utils/extended-map.ts +++ b/src/common/utils/extended-map.ts @@ -21,19 +21,17 @@ export class ExtendedMap extends Map { - constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) { - super(entries); + static new(entries?: readonly (readonly [K, V])[] | null): ExtendedMap { + return new ExtendedMap(entries); } - getOrInsert(key: K, val: V): V { - if (this.has(key)) { - return this.get(key); - } - - return this.set(key, val).get(key); - } - - getOrInsertWith(key: K, getVal: () => V): V { + /** + * Get the value behind `key`. If it was not pressent, first insert the value returned by `getVal` + * @param key The key to insert into the map with + * @param getVal A function that returns a new instance of `V`. + * @returns The value in the map + */ + getOrInsert(key: K, getVal: () => V): V { if (this.has(key)) { return this.get(key); } @@ -41,11 +39,28 @@ export class ExtendedMap extends Map { return this.set(key, getVal()).get(key); } - getOrDefault(key: K): V { + /** + * Set the value associated with `key` iff there was not a previous value + * @throws if `key` already in map + * @returns `this` so that `strictSet` can be chained + */ + strictSet(key: K, val: V): this { if (this.has(key)) { - return this.get(key); + throw new TypeError("Duplicate key in map"); } - return this.set(key, this.getDefault()).get(key); + return this.set(key, val); + } + + /** + * Get the value associated with `key` + * @throws if `key` did not a value associated with it + */ + strictGet(key: K): V { + if (!this.has(key)) { + throw new TypeError("key not in map"); + } + + return this.get(key); } } diff --git a/src/renderer/api/__tests__/catalog-entity-registry.test.ts b/src/renderer/api/__tests__/catalog-entity-registry.test.ts index 7cc208ad55..359b293993 100644 --- a/src/renderer/api/__tests__/catalog-entity-registry.test.ts +++ b/src/renderer/api/__tests__/catalog-entity-registry.test.ts @@ -42,7 +42,7 @@ describe("CatalogEntityRegistry", () => { spec: {} }]; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); items.push({ @@ -60,7 +60,7 @@ describe("CatalogEntityRegistry", () => { spec: {} }); - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(2); }); @@ -81,13 +81,13 @@ describe("CatalogEntityRegistry", () => { spec: {} }]; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].status.phase).toEqual("disconnected"); items[0].status.phase = "connected"; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].status.phase).toEqual("connected"); }); @@ -125,9 +125,9 @@ describe("CatalogEntityRegistry", () => { } ]; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); items.splice(0, 1); - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].metadata.uid).toEqual("456"); }); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index ec537c4fc1..abcf555c2b 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -19,14 +19,14 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, makeObservable, observable } from "mobx"; +import { computed, observable, makeObservable } from "mobx"; import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc"; -import { CatalogCategory, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../../common/catalog"; +import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; -import logger from "../../main/logger"; +import { iter } from "../utils"; export class CatalogEntityRegistry { - protected _items = observable.array(); + protected rawItems = observable.array([], { deep: true }); @observable protected _activeEntity: CatalogEntity; constructor(private categoryRegistry: CatalogCategoryRegistry) { @@ -34,19 +34,12 @@ export class CatalogEntityRegistry { } init() { - subscribeToBroadcast("catalog:items", (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => { - logger.debug(`[CatalogEntityRegistry]: received new catalog items`, { items }); - this.updateItems(items); + subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => { + this.rawItems.replace(items); }); broadcastMessage("catalog:broadcast"); } - @action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) { - this._items.replace( - items.map(data => this.categoryRegistry.getEntityForData(data)) - ); - } - set activeEntity(entity: CatalogEntity) { this._activeEntity = entity; } @@ -55,23 +48,27 @@ export class CatalogEntityRegistry { return this._activeEntity; } - get items() { - return this._items; + @computed get items() { + return Array.from(iter.filterMap(this.rawItems, rawItem => this.categoryRegistry.getEntityForData(rawItem))); + } + + @computed get entities(): Map { + return new Map(this.items.map(item => [item.metadata.uid, item])); } getById(id: string) { - return this._items.find((entity) => entity.metadata.uid === id); + return this.entities.get(id); } getItemsForApiKind(apiVersion: string, kind: string): T[] { - const items = this._items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); + const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); return items as T[]; } getItemsForCategory(category: CatalogCategory): T[] { const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`); - const items = this._items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind); + const items = this.items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind); return items as T[]; }