From c96c32afc6b6da87f73e2991a0c7175c25404bd8 Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Tue, 14 Mar 2023 14:52:25 -0700 Subject: [PATCH 1/4] feat: add way to get all instance of a type of model from dashboard --- src/dashboard/dashboard-manager.test.ts | 15 +++++++++++++++ src/dashboard/dashboard.ts | 8 +++++++- src/dashboard/default-dashboard.ts | 7 +++++++ src/model/manager/model-manager.test.ts | 17 +++++++++++++++++ src/model/manager/model-manager.ts | 12 ++++++++++-- 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/dashboard/dashboard-manager.test.ts b/src/dashboard/dashboard-manager.test.ts index eaa2869a..b94062b6 100644 --- a/src/dashboard/dashboard-manager.test.ts +++ b/src/dashboard/dashboard-manager.test.ts @@ -175,4 +175,19 @@ describe('Dashboard manager', () => { expect(mockModelManager.destroy).toHaveBeenCalledWith(mockOriginalDataSource); expect(mockDataSourceManager.setRootDataSource).toHaveBeenCalledWith(mockNewDataSource, dashboard.root); }); + + test('can retrieve model instances', () => { + const dashboard = dashboardManager.create({ + type: 'serialized-model' + }); + + const mockDataSource = class MockDataSource { + public readonly value: number = Math.random(); + }; + + mockModelManager.getModelInstances = jest.fn().mockReturnValue([new mockDataSource()]); + + expect(dashboard.getModelInstances(mockDataSource).length).toBe(1); + expect(mockModelManager.getModelInstances).toHaveBeenCalledWith(mockDataSource); + }); }); diff --git a/src/dashboard/dashboard.ts b/src/dashboard/dashboard.ts index 534fcf00..22b8934c 100644 --- a/src/dashboard/dashboard.ts +++ b/src/dashboard/dashboard.ts @@ -23,7 +23,7 @@ export interface Dashboard { setTimeRange(timeRange: TimeRange): this; /** - * Returns a serialized from of this dashboard. Note this does not + * Returns a serialized form of this dashboard. Note this does not * affect the dashboard in any way, it must be explicitly destroyed * if it is no longer in use. */ @@ -56,4 +56,10 @@ export interface Dashboard { */ // tslint:disable-next-line: no-any getRootDataSource>(): T | undefined; + + /** + * Returns a shallow copy array of model instances within a dashboard that match the + * argument model class + */ + getModelInstances(modelClass: Constructable): object[]; } diff --git a/src/dashboard/default-dashboard.ts b/src/dashboard/default-dashboard.ts index 6aed41b7..93ef0d12 100644 --- a/src/dashboard/default-dashboard.ts +++ b/src/dashboard/default-dashboard.ts @@ -86,4 +86,11 @@ export class DefaultDashboard implements Dashboard public getRootDataSource>(): T | undefined { return this.dataSourceManager.getRootDataSource(this.root) as T | undefined; } + + /** + * @inheritdoc + */ + public getModelInstances(modelClass: Constructable): object[] { + return this.modelManager.getModelInstances(modelClass); + } } diff --git a/src/model/manager/model-manager.test.ts b/src/model/manager/model-manager.test.ts index 9d6c848f..9d5a93bb 100644 --- a/src/model/manager/model-manager.test.ts +++ b/src/model/manager/model-manager.test.ts @@ -14,6 +14,14 @@ describe('Model manager', () => { // Value used to disambiguate equality between instances public readonly value: number = Math.random(); }; + const testClass1 = class TestClass1 { + // Value used to disambiguate equality between instances + public readonly value: number = Math.random(); + }; + const testClass2 = class TestClass2 { + // Value used to disambiguate equality between instances + public readonly value: number = Math.random(); + }; let manager: ModelManager; let mockLogger: PartialObjectMock; let mockApiBuilder: PartialObjectMock>; @@ -59,6 +67,15 @@ describe('Model manager', () => { manager.registerModelApiBuilder(mockApiBuilder as ModelApiBuilder); }); + test('returns array of instances matching arg', () => { + manager.construct(testClass); + manager.construct(testClass); + manager.construct(testClass1); + expect(manager.getModelInstances(testClass).length).toBe(2); + expect(manager.getModelInstances(testClass1).length).toBe(1); + expect(manager.getModelInstances(testClass2).length).toBe(0); + }); + test('allows constructing new models', () => { const instance = manager.construct(testClass); expect(instance.constructor).toBe(testClass); diff --git a/src/model/manager/model-manager.ts b/src/model/manager/model-manager.ts index 0004bcfd..23241ca3 100644 --- a/src/model/manager/model-manager.ts +++ b/src/model/manager/model-manager.ts @@ -13,7 +13,7 @@ import { ModelOnDestroy, ModelOnInit } from './model-lifecycle-hooks'; * models. */ export class ModelManager { - private readonly modelInstanceMap: WeakMap = new WeakMap(); + private readonly modelInstanceMap: Map = new Map(); private readonly apiBuilders: ModelApiBuilder[] = []; private readonly decorators: ModelDecorator[] = []; @@ -24,6 +24,13 @@ export class ModelManager { private readonly beforeModelDestroyedEvent: BeforeModelDestroyedEvent ) {} + /** + * Returns a shallow copy array of model instances that match the argument model class + */ + public getModelInstances(modelClass: Constructable): object[] { + return Array.from(this.modelInstanceMap.keys()).filter(modelInstance => modelInstance instanceof modelClass); + } + /** * Constructs (@see `ModelManager.construct`) then initializes (@see `ModelManager.initialize`) it * @@ -250,7 +257,8 @@ export class ModelManager { } } -interface ModelInstanceData { +// tslint:disable-next-line:completed-docs +export interface ModelInstanceData { /** * Parent of tracked model */ From f9d1599253ae6b55fb04c3bb64b82ede4d7b9d21 Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Tue, 14 Mar 2023 14:56:43 -0700 Subject: [PATCH 2/4] feat: return interface access to private --- src/model/manager/model-manager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/model/manager/model-manager.ts b/src/model/manager/model-manager.ts index 23241ca3..9793f3ef 100644 --- a/src/model/manager/model-manager.ts +++ b/src/model/manager/model-manager.ts @@ -257,8 +257,7 @@ export class ModelManager { } } -// tslint:disable-next-line:completed-docs -export interface ModelInstanceData { +interface ModelInstanceData { /** * Parent of tracked model */ From dbf3cb57f73aa1941800bd1a480a39076ae7f155 Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Wed, 15 Mar 2023 11:27:29 -0700 Subject: [PATCH 3/4] feat: added check for root model --- src/dashboard/dashboard-manager.test.ts | 2 +- src/dashboard/default-dashboard.ts | 2 +- src/model/manager/model-manager.test.ts | 1 + src/model/manager/model-manager.ts | 6 ++++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/dashboard/dashboard-manager.test.ts b/src/dashboard/dashboard-manager.test.ts index b94062b6..89f72129 100644 --- a/src/dashboard/dashboard-manager.test.ts +++ b/src/dashboard/dashboard-manager.test.ts @@ -188,6 +188,6 @@ describe('Dashboard manager', () => { mockModelManager.getModelInstances = jest.fn().mockReturnValue([new mockDataSource()]); expect(dashboard.getModelInstances(mockDataSource).length).toBe(1); - expect(mockModelManager.getModelInstances).toHaveBeenCalledWith(mockDataSource); + expect(mockModelManager.getModelInstances).toHaveBeenCalledWith(mockDataSource, {}); }); }); diff --git a/src/dashboard/default-dashboard.ts b/src/dashboard/default-dashboard.ts index 93ef0d12..e763a9f6 100644 --- a/src/dashboard/default-dashboard.ts +++ b/src/dashboard/default-dashboard.ts @@ -91,6 +91,6 @@ export class DefaultDashboard implements Dashboard * @inheritdoc */ public getModelInstances(modelClass: Constructable): object[] { - return this.modelManager.getModelInstances(modelClass); + return this.modelManager.getModelInstances(modelClass, this.root); } } diff --git a/src/model/manager/model-manager.test.ts b/src/model/manager/model-manager.test.ts index 9d5a93bb..e0267aee 100644 --- a/src/model/manager/model-manager.test.ts +++ b/src/model/manager/model-manager.test.ts @@ -74,6 +74,7 @@ describe('Model manager', () => { expect(manager.getModelInstances(testClass).length).toBe(2); expect(manager.getModelInstances(testClass1).length).toBe(1); expect(manager.getModelInstances(testClass2).length).toBe(0); + expect(manager.getModelInstances(testClass, {}).length).toBe(0); }); test('allows constructing new models', () => { diff --git a/src/model/manager/model-manager.ts b/src/model/manager/model-manager.ts index 9793f3ef..71e11428 100644 --- a/src/model/manager/model-manager.ts +++ b/src/model/manager/model-manager.ts @@ -27,8 +27,10 @@ export class ModelManager { /** * Returns a shallow copy array of model instances that match the argument model class */ - public getModelInstances(modelClass: Constructable): object[] { - return Array.from(this.modelInstanceMap.keys()).filter(modelInstance => modelInstance instanceof modelClass); + public getModelInstances(modelClass: Constructable, root?: object): object[] { + return Array.from(this.modelInstanceMap.keys()) + .filter(modelInstance => root === undefined || this.getRoot(modelInstance) === root) + .filter(modelInstance => modelInstance instanceof modelClass); } /** From da872443cc6f044480da0e2a8ba7fb6bc5b876ce Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Wed, 15 Mar 2023 13:45:29 -0700 Subject: [PATCH 4/4] feat: root arg mandatory and back to weakmap --- src/model/manager/model-manager.test.ts | 16 +++++++++------- src/model/manager/model-manager.ts | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/model/manager/model-manager.test.ts b/src/model/manager/model-manager.test.ts index e0267aee..8dedc160 100644 --- a/src/model/manager/model-manager.test.ts +++ b/src/model/manager/model-manager.test.ts @@ -68,13 +68,15 @@ describe('Model manager', () => { }); test('returns array of instances matching arg', () => { - manager.construct(testClass); - manager.construct(testClass); - manager.construct(testClass1); - expect(manager.getModelInstances(testClass).length).toBe(2); - expect(manager.getModelInstances(testClass1).length).toBe(1); - expect(manager.getModelInstances(testClass2).length).toBe(0); - expect(manager.getModelInstances(testClass, {}).length).toBe(0); + expect(manager.getModelInstances(testClass, {})).toStrictEqual([]); + + const instance = manager.construct(testClass); + const instance1 = manager.construct(testClass1, instance); + const instance2 = manager.construct(testClass2, instance1); + expect(manager.getModelInstances(testClass, instance)).toStrictEqual([instance]); + expect(manager.getModelInstances(testClass1, instance)).toStrictEqual([instance1]); + expect(manager.getModelInstances(testClass2, instance)).toStrictEqual([instance2]); + expect(manager.getModelInstances(testClass2, instance1)).toStrictEqual([instance2]); }); test('allows constructing new models', () => { diff --git a/src/model/manager/model-manager.ts b/src/model/manager/model-manager.ts index 71e11428..9fc04e93 100644 --- a/src/model/manager/model-manager.ts +++ b/src/model/manager/model-manager.ts @@ -13,7 +13,7 @@ import { ModelOnDestroy, ModelOnInit } from './model-lifecycle-hooks'; * models. */ export class ModelManager { - private readonly modelInstanceMap: Map = new Map(); + private readonly modelInstanceMap: WeakMap = new WeakMap(); private readonly apiBuilders: ModelApiBuilder[] = []; private readonly decorators: ModelDecorator[] = []; @@ -27,10 +27,18 @@ export class ModelManager { /** * Returns a shallow copy array of model instances that match the argument model class */ - public getModelInstances(modelClass: Constructable, root?: object): object[] { - return Array.from(this.modelInstanceMap.keys()) - .filter(modelInstance => root === undefined || this.getRoot(modelInstance) === root) - .filter(modelInstance => modelInstance instanceof modelClass); + public getModelInstances(modelClass: Constructable, root: object): object[] { + let found: object[] = []; + + if (root instanceof modelClass) { + found = [...found, root]; + } + + this.modelInstanceMap + .get(root) + ?.children.forEach(child => (found = [...found, ...this.getModelInstances(modelClass, child)])); + + return found; } /**