diff --git a/src/common/catalog/catalog-category-registry.ts b/src/common/catalog/catalog-category-registry.ts index 886db3e90a..a0319d773b 100644 --- a/src/common/catalog/catalog-category-registry.ts +++ b/src/common/catalog/catalog-category-registry.ts @@ -19,26 +19,38 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, computed, observable, toJS } from "mobx"; +import { action, computed, observable } from "mobx"; +import { Disposer, ExtendedMap } from "../utils"; import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity"; export class CatalogCategoryRegistry { - @observable protected categories: CatalogCategory[] = []; + protected categories = observable.set(); - @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) { - this.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>(); + + for (const category of this.categories) { + res + .getOrInsert(category.spec.group, ExtendedMap.new) + .strictSet(category.spec.names.kind, category); + } + + return res; } @computed get items() { - return toJS(this.categories); + return Array.from(this.categories); } - 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) { @@ -60,17 +72,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 9d3a0dc7b3..c8b7ff4c65 100644 --- a/src/common/utils/extended-map.ts +++ b/src/common/utils/extended-map.ts @@ -22,19 +22,17 @@ import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx"; 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); } @@ -42,12 +40,29 @@ 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 42c9b6f224..ac4e6748dc 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -19,28 +19,25 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, observable } from "mobx"; +import { computed, observable } from "mobx"; import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; +import { iter } from "../utils"; export class CatalogEntityRegistry { - @observable protected _items: CatalogEntity[] = observable.array([], { deep: true }); + protected rawItems = observable.array([], { deep: true }); @observable protected _activeEntity: CatalogEntity; constructor(private categoryRegistry: CatalogCategoryRegistry) {} init() { subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => { - this.updateItems(items); + this.rawItems.replace(items); }); broadcastMessage("catalog:broadcast"); } - @action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) { - this._items = items.map(data => this.categoryRegistry.getEntityForData(data)); - } - set activeEntity(entity: CatalogEntity) { this._activeEntity = entity; } @@ -49,23 +46,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[]; }