Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 0 additions & 35 deletions .semaphore/semaphore.yml

This file was deleted.

26,050 changes: 26,003 additions & 47 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@
"lodash-es": "^4.17.20"
},
"peerDependencies": {
"rxjs": "^6.5.5",
"core-js": "^3.6.5"
"core-js": "^3.6.5",
"rxjs": "^6.5.5"
},
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions src/hyperdash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './model/editor/editor-library';
export * from './model/events/model-changed-event';
export * from './model/events/model-created-event';
export * from './model/events/model-destroyed-event';
export * from './model/events/before-model-destroyed-event';
export * from './model/events/model-event-installer';
export * from './model/manager/model-lifecycle-hooks';
export * from './model/manager/model-manager';
Expand Down
52 changes: 52 additions & 0 deletions src/model/events/before-model-destroyed-event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DashboardEventManager } from '../../communication/dashboard-event-manager';
import { getTestScheduler } from '../../test/rxjs-jest-test-scheduler';
import { BeforeModelDestroyedEvent } from './before-model-destroyed-event';

describe('Before model destroyed event', () => {
let mockEventManager: Partial<DashboardEventManager>;
let beforeModelDestroyedEvent: BeforeModelDestroyedEvent;

beforeEach(() => {
mockEventManager = {};
beforeModelDestroyedEvent = new BeforeModelDestroyedEvent(mockEventManager as DashboardEventManager);
});
test('relays all publishes to manager', () => {
mockEventManager.publishEvent = jest.fn();
const first = {};
const second = {};
beforeModelDestroyedEvent.publish(first);
beforeModelDestroyedEvent.publish(second);

expect(mockEventManager.publishEvent).toHaveBeenCalledTimes(2);

expect(mockEventManager.publishEvent).nthCalledWith(1, beforeModelDestroyedEvent.getKey(), first);

expect(mockEventManager.publishEvent).nthCalledWith(2, beforeModelDestroyedEvent.getKey(), second);
});

test('gets observable from manager for this event', () => {
mockEventManager.getObservableForEvent = jest.fn();

beforeModelDestroyedEvent.getObservable();

expect(mockEventManager.getObservableForEvent).toHaveBeenCalledWith(beforeModelDestroyedEvent.getKey());
});

test('before destruction observables notifies on the provided model then completes', () => {
getTestScheduler().run(({ cold, expectObservable }) => {
const mockModels = {
a: {},
b: {}
};
beforeModelDestroyedEvent.getObservable = jest.fn().mockReturnValue(cold('a-b-', mockModels));

expectObservable(beforeModelDestroyedEvent.getBeforeDestructionObservable(mockModels.a)).toBe('(a|)', {
a: undefined
});

expectObservable(beforeModelDestroyedEvent.getBeforeDestructionObservable(mockModels.b)).toBe('--(b|)', {
b: undefined
});
});
});
});
27 changes: 27 additions & 0 deletions src/model/events/before-model-destroyed-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Observable } from 'rxjs';
import { filter, mapTo, take } from 'rxjs/operators';
import { DashboardEvent } from '../../communication/dashboard-event';
import { DashboardEventManager } from '../../communication/dashboard-event-manager';

export const beforeModelDestroyedEventKey = Symbol('Before model destroyed');

/**
* Fired before a model is destroyed and any destroy hooks are called.
*/
export class BeforeModelDestroyedEvent extends DashboardEvent<object> {
public constructor(dashboardEventManager: DashboardEventManager) {
super(dashboardEventManager, beforeModelDestroyedEventKey);
}

/**
* Returns a void observable that will notify once when the provided model is
* destroyed, then complete.
*/
public getBeforeDestructionObservable(model: object): Observable<void> {
return this.getObservable().pipe(
filter(destroyedModel => destroyedModel === model),
mapTo(undefined),
take(1)
);
}
}
26 changes: 23 additions & 3 deletions src/model/manager/model-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ import { PartialObjectMock } from '../../test/partial-object-mock';
import { Logger } from '../../util/logging/logger';
import { ModelApiBuilder } from '../api/builder/model-api-builder';
import { ModelApi } from '../api/model-api';
import { BeforeModelDestroyedEvent } from '../events/before-model-destroyed-event';
import { ModelCreatedEvent } from '../events/model-created-event';
import { ModelDestroyedEvent } from '../events/model-destroyed-event';
import { ModelOnDestroy, ModelOnInit } from './model-lifecycle-hooks';
import { ModelManager } from './model-manager';

describe('Model manager', () => {
const testClass = class TestClass {};
const testClass = class TestClass {
// Value used to disambiguate equality between instances
public readonly value: number = Math.random();
};
let manager: ModelManager;
let mockLogger: PartialObjectMock<Logger>;
let mockApiBuilder: PartialObjectMock<ModelApiBuilder<ModelApi>>;

let mockCreatedEvent: PartialObjectMock<ModelCreatedEvent>;
let mockDestroyedEvent: PartialObjectMock<ModelDestroyedEvent>;
let mockBeforeDestroyedEvent: PartialObjectMock<BeforeModelDestroyedEvent>;

beforeEach(() => {
mockCreatedEvent = {
Expand All @@ -26,6 +31,10 @@ describe('Model manager', () => {
publish: jest.fn()
};

mockBeforeDestroyedEvent = {
publish: jest.fn()
};

mockLogger = {
warn: jest.fn((message: string) => ({
throw: jest.fn(() => {
Expand All @@ -43,7 +52,8 @@ describe('Model manager', () => {
manager = new ModelManager(
mockLogger as Logger,
mockCreatedEvent as ModelCreatedEvent,
mockDestroyedEvent as ModelDestroyedEvent
mockDestroyedEvent as ModelDestroyedEvent,
mockBeforeDestroyedEvent as BeforeModelDestroyedEvent
);

manager.registerModelApiBuilder(mockApiBuilder as ModelApiBuilder<ModelApi>);
Expand Down Expand Up @@ -233,8 +243,17 @@ describe('Model manager', () => {
expect(mockCreatedEvent.publish).toHaveBeenNthCalledWith(1, root);
expect(mockCreatedEvent.publish).toHaveBeenNthCalledWith(2, firstChild);

mockBeforeDestroyedEvent.publish = jest.fn(model => {
expect(manager.isTrackedModel(model)).toBe(true);
expect(mockDestroyedEvent.publish).not.toHaveBeenCalledWith(model);
});

manager.destroy(root);

expect(mockBeforeDestroyedEvent.publish).toHaveBeenCalledTimes(2);
expect(mockBeforeDestroyedEvent.publish).toHaveBeenNthCalledWith(1, firstChild);
expect(mockBeforeDestroyedEvent.publish).toHaveBeenNthCalledWith(2, root);

expect(mockDestroyedEvent.publish).toHaveBeenCalledTimes(2);
expect(mockDestroyedEvent.publish).toHaveBeenNthCalledWith(1, firstChild);
expect(mockDestroyedEvent.publish).toHaveBeenNthCalledWith(2, root);
Expand Down Expand Up @@ -292,7 +311,8 @@ describe('Model manager', () => {
manager = new ModelManager(
mockLogger as Logger,
mockCreatedEvent as ModelCreatedEvent,
mockDestroyedEvent as ModelDestroyedEvent
mockDestroyedEvent as ModelDestroyedEvent,
mockBeforeDestroyedEvent as BeforeModelDestroyedEvent
); // Rebuild so we don't use the mock api build from other tests

const modelAClass = class {};
Expand Down
6 changes: 5 additions & 1 deletion src/model/manager/model-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Constructable } from '../../util/constructable';
import { Logger } from '../../util/logging/logger';
import { ModelApiBuilder } from '../api/builder/model-api-builder';
import { ModelApi } from '../api/model-api';
import { BeforeModelDestroyedEvent } from '../events/before-model-destroyed-event';
import { ModelCreatedEvent } from '../events/model-created-event';
import { ModelDestroyedEvent } from '../events/model-destroyed-event';
import { ModelOnDestroy, ModelOnInit } from './model-lifecycle-hooks';
Expand All @@ -19,7 +20,8 @@ export class ModelManager {
public constructor(
private readonly logger: Logger,
private readonly modelCreatedEvent: ModelCreatedEvent,
private readonly modelDestroyedEvent: ModelDestroyedEvent
private readonly modelDestroyedEvent: ModelDestroyedEvent,
private readonly beforeModelDestroyedEvent: BeforeModelDestroyedEvent
) {}

/**
Expand Down Expand Up @@ -233,6 +235,8 @@ export class ModelManager {
// Depth first, destroy children before self
instanceData.children.forEach(child => this.destroy(child));

this.beforeModelDestroyedEvent.publish(modelInstance);

if (this.modelHasDestroyHook(modelInstance)) {
modelInstance.modelOnDestroy();
}
Expand Down
41 changes: 39 additions & 2 deletions src/variable/manager/variable-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// tslint:disable:no-invalid-template-strings
import { EMPTY, Subject } from 'rxjs';
import { BeforeModelDestroyedEvent } from '../../model/events/before-model-destroyed-event';
import { ModelChangedEvent } from '../../model/events/model-changed-event';
import { ModelManager } from '../../model/manager/model-manager';
import { PropertyLocation } from '../../model/property/property-location';
Expand All @@ -11,6 +13,7 @@ describe('Variable manager', () => {
let mockLogger: Partial<Logger>;
let mockModelManager: Partial<ModelManager>;
let mockModelChangedEvent: Partial<ModelChangedEvent>;
let mockBeforeModelDestroyedEvent: Partial<BeforeModelDestroyedEvent>;
const model = {};
const parent = {};
const root = {};
Expand All @@ -35,10 +38,15 @@ describe('Variable manager', () => {
})
};

mockBeforeModelDestroyedEvent = {
getBeforeDestructionObservable: jest.fn()
};

manager = new VariableManager(
mockLogger as Logger,
mockModelManager as ModelManager,
mockModelChangedEvent as ModelChangedEvent
mockModelChangedEvent as ModelChangedEvent,
mockBeforeModelDestroyedEvent as BeforeModelDestroyedEvent
);
});

Expand Down Expand Up @@ -106,6 +114,7 @@ describe('Variable manager reference tracking', () => {
let mockLogger: PartialObjectMock<Logger>;
let mockModelManager: PartialObjectMock<ModelManager>;
let mockModelChangedEvent: PartialObjectMock<ModelChangedEvent>;
let mockBeforeModelDestroyedEvent: Partial<BeforeModelDestroyedEvent>;

let mockParentLocation: PartialObjectMock<PropertyLocation>;
const parent = {};
Expand All @@ -132,6 +141,10 @@ describe('Variable manager reference tracking', () => {
)
};

mockBeforeModelDestroyedEvent = {
getBeforeDestructionObservable: jest.fn().mockReturnValue(EMPTY)
};

mockModelLocation = {
parentModel: model,
setProperty: jest.fn(),
Expand All @@ -147,7 +160,8 @@ describe('Variable manager reference tracking', () => {
manager = new VariableManager(
mockLogger as Logger,
mockModelManager as ModelManager,
mockModelChangedEvent as ModelChangedEvent
mockModelChangedEvent as ModelChangedEvent,
mockBeforeModelDestroyedEvent as BeforeModelDestroyedEvent
);
});

Expand Down Expand Up @@ -323,4 +337,27 @@ describe('Variable manager reference tracking', () => {
'Attempted to resolve reference at modelProp which does not contain a registered reference'
);
});

test('removes dangling references after a model is destroyed', () => {
const destroySubject = new Subject();
mockBeforeModelDestroyedEvent.getBeforeDestructionObservable = jest
.fn()
.mockReturnValue(destroySubject.asObservable());

manager.registerReference(mockModelLocation as PropertyLocation, '${test}');
manager.set('test', 'foo', parent);

destroySubject.next(model);
destroySubject.complete();

// Reference should no longer be tracked
expect(manager.isVariableReference(mockModelLocation as PropertyLocation)).toBe(false);

// Setting a value should not call model manager or set property
(mockModelManager.getParent as jest.Mock).mockClear();
manager.set('test', 'bar', parent);

expect(mockModelManager.getParent).not.toHaveBeenCalled();
expect(mockModelLocation.setProperty).not.toHaveBeenCalledWith('bar');
});
});
8 changes: 7 additions & 1 deletion src/variable/manager/variable-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fromPairs } from 'lodash-es';
import { BeforeModelDestroyedEvent } from '../../model/events/before-model-destroyed-event';
import { ModelChangedEvent } from '../../model/events/model-changed-event';
import { ModelManager } from '../../model/manager/model-manager';
import { PropertyLocation } from '../../model/property/property-location';
Expand All @@ -22,7 +23,8 @@ export class VariableManager {
public constructor(
private readonly logger: Logger,
private readonly modelManager: ModelManager,
private readonly modelChangedEvent: ModelChangedEvent
private readonly modelChangedEvent: ModelChangedEvent,
private readonly beforeModelDestroyedEvent: BeforeModelDestroyedEvent
) {}

/**
Expand Down Expand Up @@ -87,6 +89,10 @@ export class VariableManager {
referenceMap.set(location.toString(), reference);
}

this.beforeModelDestroyedEvent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to unsubscribe here because the observable gets completed with the take(1), correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct - this observable emits exactly once, right before the destruction. This does remind me though - the reference may no longer exist at that point, so let me ensure that case doesn't error out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test case and handled this situation (it was logging but not erroring - now it cleans up the subscription in this case, so it never outlasts its potential utility).

.getBeforeDestructionObservable(location.parentModel)
.subscribe(() => this.deregisterReference(location));

return this.updateReference(reference);
}

Expand Down