Skip to content

Commit

Permalink
fix: Prevent nested computedInjectMany's from appearing as cycles
Browse files Browse the repository at this point in the history
  • Loading branch information
Iku-turso committed Jun 20, 2022
1 parent 6968fda commit 24cfa1f
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 87 deletions.
16 changes: 8 additions & 8 deletions packages/injectable-extension-for-mobx/src/computedInjectMany.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
deregistrationDecoratorToken,
getInjectable,
lifecycleEnum,
registrationDecoratorToken,
deregistrationDecoratorToken,
} from '@ogre-tools/injectable';

import { computed, createAtom, runInAction } from 'mobx';
Expand Down Expand Up @@ -64,14 +64,12 @@ const invalidateReactiveInstancesOnDeregisterDecorator = getInjectable({
export const computedInjectManyInjectable = getInjectable({
id: 'computed-inject-many',

instantiate: di => {
const getReactiveInstances = injectionToken =>
di.inject(reactiveInstancesInjectable, injectionToken);
instantiate: di => injectionToken =>
di.inject(reactiveInstancesInjectable, injectionToken),

return injectionToken => {
return getReactiveInstances(injectionToken);
};
},
lifecycle: lifecycleEnum.transient,

cannotCauseCycles: true,
});

const reactiveInstancesInjectable = getInjectable({
Expand All @@ -93,6 +91,8 @@ const reactiveInstancesInjectable = getInjectable({
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, injectionToken) => injectionToken,
}),

cannotCauseCycles: true,
});

export const registerMobX = di => {
Expand Down
267 changes: 188 additions & 79 deletions packages/injectable-extension-for-mobx/src/computedInjectMany.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {
createContainer,
getInjectable,
getInjectionToken,
injectionDecoratorToken,
} from '@ogre-tools/injectable';

import {
computedInjectManyInjectable,
registerMobX,
} from './computedInjectMany';

import { configure, observe } from 'mobx';
import { computed, configure, observe } from 'mobx';

configure({
enforceActions: 'always',
Expand All @@ -27,21 +28,46 @@ describe('registerMobx', () => {
let someFirstInjectionToken;
let someOtherInjectable;
let reactionCountForFirstToken;
let computedInjectMany;
let someInjectable;
let contextsOfSomeInjectable;

beforeEach(() => {
contextsOfSomeInjectable = [];
reactionCountForFirstToken = 0;

someFirstInjectionToken = getInjectionToken({
id: 'some-injection-token',
});

const someInjectable = getInjectable({
someInjectable = getInjectable({
id: 'some-injectable',
instantiate: () => 'some-instance',
injectionToken: someFirstInjectionToken,
});

const contextSpyDecorator = getInjectable({
id: 'context-spy-decorator',

instantiate: () => ({
target: someInjectable,

decorate:
toBeDecorated =>
(alias, instantiationParameter, context = []) => {
contextsOfSomeInjectable.push([
...context.map(x => x.injectable.id),
alias.id,
]);

return toBeDecorated(alias, instantiationParameter, context);
},
}),

decorable: false,

injectionToken: injectionDecoratorToken,
});

someOtherInjectable = getInjectable({
id: 'some-other-injectable',
instantiate: () => 'some-other-instance',
Expand All @@ -50,131 +76,214 @@ describe('registerMobx', () => {

di = createContainer();

di.register(contextSpyDecorator);

registerMobX(di);
});

computedInjectMany = di.inject(computedInjectManyInjectable);
describe('given observed as computedInjectMany, when registered', () => {
beforeEach(() => {
const computedInjectMany = di.inject(computedInjectManyInjectable);

actual = computedInjectMany(someFirstInjectionToken);
actual = computedInjectMany(someFirstInjectionToken);

observe(
actual,
observe(
actual,

change => {
reactionCountForFirstToken++;
reactiveInstances = change.newValue;
},
);
change => {
reactionCountForFirstToken++;
reactiveInstances = change.newValue;
},
);

di.register(someInjectable, someOtherInjectable);
});
di.register(someInjectable, someOtherInjectable);
});

it('injects reactive instances', () => {
expect(reactiveInstances).toEqual([
'some-instance',
'some-other-instance',
]);
});
it('injects reactive instances', () => {
expect(reactiveInstances).toEqual([
'some-instance',
'some-other-instance',
]);
});

it('when injected again, returns same instance of computed', () => {
const actual1 = computedInjectMany(someFirstInjectionToken);
const actual2 = computedInjectMany(someFirstInjectionToken);
it('when injected again, returns same instance of computed', () => {
const computedInjectMany = di.inject(computedInjectManyInjectable);

expect(actual1).toBe(actual2);
});
const actual1 = computedInjectMany(someFirstInjectionToken);
const actual2 = computedInjectMany(someFirstInjectionToken);

it('when a new implementation gets registered, the reactive instances react', () => {
const someIrrelevantInjectable = getInjectable({
id: 'some-irrelevant-injectable',
instantiate: () => 'irrelevant',
expect(actual1).toBe(actual2);
});

const someAnotherInjectable = getInjectable({
id: 'some-another-injectable',
instantiate: () => 'some-another-instance',
injectionToken: someFirstInjectionToken,
});
it('when a new implementation gets registered, the reactive instances react', () => {
const someIrrelevantInjectable = getInjectable({
id: 'some-irrelevant-injectable',
instantiate: () => 'irrelevant',
});

di.register(someIrrelevantInjectable, someAnotherInjectable);
const someAnotherInjectable = getInjectable({
id: 'some-another-injectable',
instantiate: () => 'some-another-instance',
injectionToken: someFirstInjectionToken,
});

expect(reactiveInstances).toEqual([
'some-instance',
'some-other-instance',
'some-another-instance',
]);
});
di.register(someIrrelevantInjectable, someAnotherInjectable);

it('when an existing implementation gets deregistered, the reactive instances react', () => {
const someIrrelevantInjectable = getInjectable({
id: 'some-irrelevant-injectable',
instantiate: () => 'irrelevant',
expect(reactiveInstances).toEqual([
'some-instance',
'some-other-instance',
'some-another-instance',
]);
});

di.register(someIrrelevantInjectable);
it('when an existing implementation gets deregistered, the reactive instances react', () => {
const someIrrelevantInjectable = getInjectable({
id: 'some-irrelevant-injectable',
instantiate: () => 'irrelevant',
});

di.deregister(someIrrelevantInjectable, someOtherInjectable);
di.register(someIrrelevantInjectable);

expect(reactiveInstances).toEqual(['some-instance']);
});
di.deregister(someIrrelevantInjectable, someOtherInjectable);

it('when registering multiple new implementations for the token, causes only one reaction', () => {
const someNewInjectable1 = getInjectable({
id: 'some-injectable-1',
instantiate: () => 'irrelevant',
injectionToken: someFirstInjectionToken,
expect(reactiveInstances).toEqual(['some-instance']);
});

const someNewInjectable2 = getInjectable({
id: 'some-injectable-2',
instantiate: () => 'irrelevant',
injectionToken: someFirstInjectionToken,
it('when registering multiple new implementations for the token, causes only one reaction', () => {
const someNewInjectable1 = getInjectable({
id: 'some-injectable-1',
instantiate: () => 'irrelevant',
injectionToken: someFirstInjectionToken,
});

const someNewInjectable2 = getInjectable({
id: 'some-injectable-2',
instantiate: () => 'irrelevant',
injectionToken: someFirstInjectionToken,
});

reactionCountForFirstToken = 0;

di.register(someNewInjectable1, someNewInjectable2);

expect(reactionCountForFirstToken).toBe(1);
});

reactionCountForFirstToken = 0;
describe('given second injection token and implementations, when injected as reactive', () => {
let reactiveInstancesForSecondToken;

beforeEach(() => {
const someSecondInjectionToken = getInjectionToken({
id: 'some-second-injection-token',
});

const someInjectableForSecondInjectionToken = getInjectable({
id: 'some-injectable-for-second-injection-token',
instantiate: () => 'some-instance-for-second-token',
injectionToken: someSecondInjectionToken,
});

const computedInjectMany = di.inject(computedInjectManyInjectable);

const actual = computedInjectMany(someSecondInjectionToken);

observe(
actual,

change => {
reactiveInstancesForSecondToken = change.newValue;
},
);

reactionCountForFirstToken = 0;

di.register(someInjectableForSecondInjectionToken);
});

di.register(someNewInjectable1, someNewInjectable2);
it('injects only related implementations', () => {
expect(reactiveInstancesForSecondToken).toEqual([
'some-instance-for-second-token',
]);
});

expect(reactionCountForFirstToken).toBe(1);
it('does not cause reaction in reactive instances of unrelated injection token', () => {
expect(reactionCountForFirstToken).toBe(0);
});
});
});

describe('given second injection token and implementations, when injected as reactive', () => {
let reactiveInstancesForSecondToken;
describe('given nested injection token and implementations, when injected as reactive', () => {
let observedRootValue;

beforeEach(() => {
const someSecondInjectionToken = getInjectionToken({
id: 'some-second-injection-token',
const someRootInjectionToken = getInjectionToken({
id: 'some-root-injection-token',
});

const someInjectableForSecondInjectionToken = getInjectable({
id: 'some-injectable-for-second-injection-token',
instantiate: () => 'some-instance-for-second-token',
injectionToken: someSecondInjectionToken,
const someRootInjectable = getInjectable({
id: 'some-root-injectable',

instantiate: di => {
const nestedComputedInjectMany = di.inject(
computedInjectManyInjectable,
);

const nestedInstances = nestedComputedInjectMany(
someFirstInjectionToken,
);

return computed(() => {
const childInstancesString = nestedInstances.get().join(', ');

return `some-root-instance(${childInstancesString})`;
});
},

injectionToken: someRootInjectionToken,
});

const computedInjectMany = di.inject(computedInjectManyInjectable);

const actual = computedInjectMany(someSecondInjectionToken);
const reactiveRootInstances = computedInjectMany(
someRootInjectionToken,
);

const actual = computed(() =>
reactiveRootInstances
.get()
.flatMap(reactiveChildInstance => reactiveChildInstance.get()),
);

observe(
actual,

change => {
reactiveInstancesForSecondToken = change.newValue;
observedRootValue = change.newValue;
},
);

reactionCountForFirstToken = 0;

di.register(someInjectableForSecondInjectionToken);
di.register(someRootInjectable, someInjectable, someOtherInjectable);
});

it('injects only related implementations', () => {
expect(reactiveInstancesForSecondToken).toEqual([
'some-instance-for-second-token',
it('observes root and nested values', () => {
expect(observedRootValue).toEqual([
'some-root-instance(some-instance, some-other-instance)',
]);
});

it('does not cause reaction in reactive instances of unrelated injection token', () => {
expect(reactionCountForFirstToken).toBe(0);
it('a deeply nested injectable has full context', () => {
expect(contextsOfSomeInjectable).toEqual([
[
'computed-inject-many',
'reactive-instances',
'some-root-injection-token',
'some-root-injectable',
'computed-inject-many',
'reactive-instances',
'some-injection-token',
'some-injectable',
],
]);
});
});
});
Expand Down
Loading

0 comments on commit 24cfa1f

Please sign in to comment.