mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce package for Features (#7242)
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
1b808cf7df
commit
c174965708
11
package-lock.json
generated
11
package-lock.json
generated
@ -3301,6 +3301,10 @@
|
|||||||
"resolved": "packages/extension-api",
|
"resolved": "packages/extension-api",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@k8slens/feature-core": {
|
||||||
|
"resolved": "packages/technical-features/feature-core",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@k8slens/generate-tray-icons": {
|
"node_modules/@k8slens/generate-tray-icons": {
|
||||||
"resolved": "packages/generate-tray-icons",
|
"resolved": "packages/generate-tray-icons",
|
||||||
"link": true
|
"link": true
|
||||||
@ -34811,6 +34815,13 @@
|
|||||||
"@ogre-tools/injectable": "^15.1.1",
|
"@ogre-tools/injectable": "^15.1.1",
|
||||||
"lodash": "^4.17.15"
|
"lodash": "^4.17.15"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"packages/technical-features/feature-core": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ogre-tools/injectable": "^15.1.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
packages/technical-features/feature-core/README.md
Normal file
41
packages/technical-features/feature-core/README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# @k8slens/feature-core
|
||||||
|
|
||||||
|
Feature is set of injectables that are registered and deregistered simultaneously.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
```bash
|
||||||
|
$ npm install @k8slens/feature-core
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createContainer } from "@ogre-tools/injectable"
|
||||||
|
import { getFeature, registerFeature, deregisterFeature } from "@k8slens/feature-core"
|
||||||
|
|
||||||
|
// Notice that this Feature is usually exported from another NPM package.
|
||||||
|
const someFeature = getFeature({
|
||||||
|
id: "some-feature",
|
||||||
|
|
||||||
|
register: (di) => {
|
||||||
|
di.register(someInjectable, someOtherInjectable);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Feature dependencies are automatically registered and
|
||||||
|
// deregistered when necessary.
|
||||||
|
dependencies: [someOtherFeature]
|
||||||
|
});
|
||||||
|
|
||||||
|
const di = createContainer("some-container");
|
||||||
|
|
||||||
|
registerFeature(di, someFeature);
|
||||||
|
|
||||||
|
// Or perhaps you want to deregister?
|
||||||
|
deregisterFeature(di, someFeature);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Need to know
|
||||||
|
|
||||||
|
#### NPM packages exporting a Feature
|
||||||
|
- Prefer `peerDependencies` since they are installed from the application and are not allowed to be in the built bundle.
|
||||||
|
- Prefer exporting `injectionToken` instead of `injectable` for not allowing other features to access technical details like the `injectable`
|
||||||
3
packages/technical-features/feature-core/index.ts
Normal file
3
packages/technical-features/feature-core/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { getFeature } from "./src/feature";
|
||||||
|
export { registerFeature } from "./src/register-feature";
|
||||||
|
export type { Feature, GetFeatureArgs } from "./src/feature";
|
||||||
2
packages/technical-features/feature-core/jest.config.js
Normal file
2
packages/technical-features/feature-core/jest.config.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
module.exports =
|
||||||
|
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
|
||||||
30
packages/technical-features/feature-core/package.json
Normal file
30
packages/technical-features/feature-core/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@k8slens/feature-core",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Code that is common to all Features and those registering them.",
|
||||||
|
"type": "commonjs",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/lensapp/lens.git"
|
||||||
|
},
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"author": {
|
||||||
|
"name": "OpenLens Authors",
|
||||||
|
"email": "info@k8slens.dev"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack",
|
||||||
|
"dev": "webpack --mode=development --watch",
|
||||||
|
"test": "jest --coverage --runInBand"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ogre-tools/injectable": "^15.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import type { Feature } from "./feature";
|
||||||
|
import { featureContextMapInjectable } from "./feature-context-map-injectable";
|
||||||
|
|
||||||
|
export const deregisterFeature = (di: DiContainer, ...features: Feature[]) => {
|
||||||
|
features.forEach((feature) => {
|
||||||
|
deregisterFeatureRecursed(di, feature);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deregisterFeatureRecursed = (
|
||||||
|
di: DiContainer,
|
||||||
|
feature: Feature,
|
||||||
|
dependedBy?: Feature
|
||||||
|
) => {
|
||||||
|
const featureContextMap = di.inject(featureContextMapInjectable);
|
||||||
|
|
||||||
|
const featureContext = featureContextMap.get(feature);
|
||||||
|
|
||||||
|
if (!featureContext) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to deregister feature "${feature.id}", but it was not registered.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
featureContext.numberOfRegistrations--;
|
||||||
|
|
||||||
|
const getDependingFeatures = getDependingFeaturesFor(featureContextMap);
|
||||||
|
|
||||||
|
const dependingFeatures = getDependingFeatures(feature);
|
||||||
|
|
||||||
|
if (!dependedBy && dependingFeatures.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to deregister Feature "${
|
||||||
|
feature.id
|
||||||
|
}", but it is the dependency of Features "${dependingFeatures.join(
|
||||||
|
", "
|
||||||
|
)}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependedBy) {
|
||||||
|
const oldNumberOfDependents = featureContext.dependedBy.get(dependedBy)!;
|
||||||
|
const newNumberOfDependants = oldNumberOfDependents - 1;
|
||||||
|
featureContext.dependedBy.set(dependedBy, newNumberOfDependants);
|
||||||
|
|
||||||
|
if (newNumberOfDependants === 0) {
|
||||||
|
featureContext.dependedBy.delete(dependedBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featureContext.numberOfRegistrations === 0) {
|
||||||
|
featureContextMap.delete(feature);
|
||||||
|
|
||||||
|
featureContext.deregister();
|
||||||
|
}
|
||||||
|
|
||||||
|
feature.dependencies?.forEach((dependency) => {
|
||||||
|
deregisterFeatureRecursed(di, dependency, feature);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDependingFeaturesFor = (
|
||||||
|
featureContextMap: Map<Feature, { dependedBy: Map<Feature, number> }>
|
||||||
|
) => {
|
||||||
|
const getDependingFeaturesForRecursion = (
|
||||||
|
feature: Feature,
|
||||||
|
atRoot = true
|
||||||
|
): string[] => {
|
||||||
|
const context = featureContextMap.get(feature);
|
||||||
|
|
||||||
|
if (context?.dependedBy.size) {
|
||||||
|
return [...context!.dependedBy.entries()].flatMap(([dependant]) =>
|
||||||
|
getDependingFeaturesForRecursion(dependant, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return atRoot ? [] : [feature.id];
|
||||||
|
};
|
||||||
|
|
||||||
|
return getDependingFeaturesForRecursion;
|
||||||
|
};
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import type { Feature } from "./feature";
|
||||||
|
|
||||||
|
export type FeatureContextMap = Map<
|
||||||
|
Feature,
|
||||||
|
{
|
||||||
|
register: () => void;
|
||||||
|
deregister: () => void;
|
||||||
|
dependedBy: Map<Feature, number>;
|
||||||
|
numberOfRegistrations: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const featureContextMapInjectionToken =
|
||||||
|
getInjectionToken<FeatureContextMap>({
|
||||||
|
id: "feature-context-map-injection-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
const featureContextMapInjectable = getInjectable({
|
||||||
|
id: "feature-store",
|
||||||
|
|
||||||
|
instantiate: (): FeatureContextMap => new Map(),
|
||||||
|
|
||||||
|
injectionToken: featureContextMapInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { featureContextMapInjectable };
|
||||||
@ -0,0 +1,281 @@
|
|||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
DiContainer,
|
||||||
|
getInjectable,
|
||||||
|
Injectable,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import type { Feature } from "./feature";
|
||||||
|
import { registerFeature } from "./register-feature";
|
||||||
|
import { deregisterFeature } from "./deregister-feature";
|
||||||
|
import { getFeature } from "./feature" ;
|
||||||
|
|
||||||
|
describe("feature-dependencies", () => {
|
||||||
|
describe("given a parent Feature with another Features as dependency", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let someInjectable: Injectable<string>;
|
||||||
|
let someInjectableInDependencyFeature: Injectable<string>;
|
||||||
|
let someParentFeature: Feature;
|
||||||
|
let someDependencyFeature: Feature;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
someInjectable = getInjectable({
|
||||||
|
id: "some-injectable-2",
|
||||||
|
instantiate: () => "some-instance",
|
||||||
|
});
|
||||||
|
|
||||||
|
someInjectableInDependencyFeature = getInjectable({
|
||||||
|
id: "some-injectable",
|
||||||
|
instantiate: () => "some-instance-2",
|
||||||
|
});
|
||||||
|
|
||||||
|
someDependencyFeature = getFeature({
|
||||||
|
id: "some-dependency-feature",
|
||||||
|
register: (di) => di.register(someInjectableInDependencyFeature),
|
||||||
|
});
|
||||||
|
|
||||||
|
someParentFeature = getFeature({
|
||||||
|
id: "some-feature",
|
||||||
|
register: (di) => di.register(someInjectable),
|
||||||
|
dependencies: [someDependencyFeature],
|
||||||
|
});
|
||||||
|
|
||||||
|
registerFeature(di, someParentFeature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when an injectable from the dependency Feature is injected, does so", () => {
|
||||||
|
const actual = di.inject(someInjectableInDependencyFeature);
|
||||||
|
|
||||||
|
expect(actual).toBe("some-instance-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when the dependency Feature is deregistered, throws", () => {
|
||||||
|
expect(() => {
|
||||||
|
deregisterFeature(di, someDependencyFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to deregister Feature "some-dependency-feature", but it is the dependency of Features "some-feature"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given the parent Feature is already deregistered, when also the dependency Feature is deregistered, throws", () => {
|
||||||
|
deregisterFeature(di, someParentFeature);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
deregisterFeature(di, someDependencyFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to deregister feature "some-dependency-feature", but it was not registered.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given the parent Feature is deregistered, when injecting an injectable from the dependency Feature, throws", () => {
|
||||||
|
deregisterFeature(di, someParentFeature);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
di.inject(someInjectableInDependencyFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable".'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a first Feature is registered, when second Feature using the first Feature as dependency gets registered", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let someInjectable: Injectable<string>;
|
||||||
|
let someFeature2: Feature;
|
||||||
|
let someFeature1: Feature;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
someInjectable = getInjectable({
|
||||||
|
id: "some-injectable",
|
||||||
|
instantiate: () => "some-instance",
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature1 = getFeature({
|
||||||
|
id: "some-feature-1",
|
||||||
|
register: (di) => di.register(someInjectable),
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature2 = getFeature({
|
||||||
|
id: "some-feature-2",
|
||||||
|
register: () => {},
|
||||||
|
dependencies: [someFeature1],
|
||||||
|
});
|
||||||
|
|
||||||
|
registerFeature(di, someFeature1, someFeature2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when the first Feature is deregistered, throws", () => {
|
||||||
|
expect(() => {
|
||||||
|
deregisterFeature(di, someFeature1);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to deregister Feature "some-feature-1", but it is the dependency of Features "some-feature-2"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given the second Feature is deregistered, when injecting an injectable from the first Feature, still does so", () => {
|
||||||
|
deregisterFeature(di, someFeature2);
|
||||||
|
|
||||||
|
const actual = di.inject(someInjectable);
|
||||||
|
|
||||||
|
expect(actual).toBe("some-instance");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given parent Features with a shared Feature as dependency", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let someInjectableInDependencyFeature: Injectable<string>;
|
||||||
|
let someFeature1: Feature;
|
||||||
|
let someFeature2: Feature;
|
||||||
|
let someSharedDependencyFeature: Feature;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
someInjectableInDependencyFeature = getInjectable({
|
||||||
|
id: "some-injectable-in-dependency-feature",
|
||||||
|
instantiate: () => "some-instance",
|
||||||
|
});
|
||||||
|
|
||||||
|
someSharedDependencyFeature = getFeature({
|
||||||
|
id: "some-dependency-feature",
|
||||||
|
register: (di) => di.register(someInjectableInDependencyFeature),
|
||||||
|
});
|
||||||
|
|
||||||
|
const someFeatureForAdditionalHierarchy = getFeature({
|
||||||
|
id: "some-feature-for-additional-hierarchy",
|
||||||
|
register: () => {},
|
||||||
|
dependencies: [someSharedDependencyFeature],
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature1 = getFeature({
|
||||||
|
id: "some-feature-1",
|
||||||
|
register: () => {},
|
||||||
|
dependencies: [someFeatureForAdditionalHierarchy],
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature2 = getFeature({
|
||||||
|
id: "some-feature-2",
|
||||||
|
register: () => {},
|
||||||
|
dependencies: [someFeatureForAdditionalHierarchy],
|
||||||
|
});
|
||||||
|
|
||||||
|
registerFeature(di, someFeature1, someFeature2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when the shared Feature is deregistered, throws", () => {
|
||||||
|
expect(() => {
|
||||||
|
deregisterFeature(di, someSharedDependencyFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to deregister Feature "some-dependency-feature", but it is the dependency of Features "some-feature-1, some-feature-2"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given only part of the parent Features get deregistered, when injecting an injectable from the shared Feature, does so", () => {
|
||||||
|
deregisterFeature(di, someFeature1);
|
||||||
|
|
||||||
|
const actual = di.inject(someInjectableInDependencyFeature);
|
||||||
|
|
||||||
|
expect(actual).toBe("some-instance");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given all of the parent Features get deregistered, when injecting an injectable from the shared Feature, throws", () => {
|
||||||
|
deregisterFeature(di, someFeature1, someFeature2);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
di.inject(someInjectableInDependencyFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given parent Features with a shared Feature as dependency and registered, when the shared Feature gets registered again", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let someInjectableInDependencyFeature: Injectable<string>;
|
||||||
|
let someFeature1: Feature;
|
||||||
|
let someFeature2: Feature;
|
||||||
|
let someSharedDependencyFeature: Feature;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
someInjectableInDependencyFeature = getInjectable({
|
||||||
|
id: "some-injectable-in-dependency-feature",
|
||||||
|
instantiate: () => "some-instance",
|
||||||
|
});
|
||||||
|
|
||||||
|
someSharedDependencyFeature = getFeature({
|
||||||
|
id: "some-dependency-feature",
|
||||||
|
register: (di) => di.register(someInjectableInDependencyFeature),
|
||||||
|
});
|
||||||
|
|
||||||
|
const someFeatureForAdditionalHierarchy = getFeature({
|
||||||
|
id: "some-feature-for-additional-hierarchy",
|
||||||
|
register: () => {},
|
||||||
|
dependencies: [someSharedDependencyFeature],
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature1 = getFeature({
|
||||||
|
id: "some-feature-1",
|
||||||
|
register: () => {},
|
||||||
|
dependencies: [someFeatureForAdditionalHierarchy],
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature2 = getFeature({
|
||||||
|
id: "some-feature-2",
|
||||||
|
register: () => {},
|
||||||
|
dependencies: [someFeatureForAdditionalHierarchy],
|
||||||
|
});
|
||||||
|
|
||||||
|
registerFeature(
|
||||||
|
di,
|
||||||
|
someFeature1,
|
||||||
|
someFeature2,
|
||||||
|
someSharedDependencyFeature
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when the shared Feature is deregistered, throws", () => {
|
||||||
|
expect(() => {
|
||||||
|
deregisterFeature(di, someSharedDependencyFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to deregister Feature "some-dependency-feature", but it is the dependency of Features "some-feature-1, some-feature-2"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given only part of the parent Features get deregistered, when injecting an injectable from the shared Feature, does so", () => {
|
||||||
|
deregisterFeature(di, someFeature1);
|
||||||
|
|
||||||
|
const actual = di.inject(someInjectableInDependencyFeature);
|
||||||
|
|
||||||
|
expect(actual).toBe("some-instance");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given all of the parent Features get deregistered, when injecting an injectable from the shared Feature, still does so", () => {
|
||||||
|
deregisterFeature(di, someFeature1, someFeature2);
|
||||||
|
|
||||||
|
const actual = di.inject(someInjectableInDependencyFeature);
|
||||||
|
|
||||||
|
expect(actual).toBe("some-instance");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given all of the Features get deregistered, when injecting an injectable from the shared Feature, throws", () => {
|
||||||
|
deregisterFeature(
|
||||||
|
di,
|
||||||
|
someFeature1,
|
||||||
|
someFeature2,
|
||||||
|
someSharedDependencyFeature
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
di.inject(someInjectableInDependencyFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
packages/technical-features/feature-core/src/feature.ts
Normal file
12
packages/technical-features/feature-core/src/feature.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type { DiContainerForInjection } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
export interface Feature {
|
||||||
|
id: string;
|
||||||
|
register: (di: DiContainerForInjection) => void;
|
||||||
|
dependencies?: Feature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetFeatureArgs extends Feature {}
|
||||||
|
|
||||||
|
export const getFeature = (getFeatureArgs: GetFeatureArgs): Feature =>
|
||||||
|
getFeatureArgs;
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { Feature } from "./feature";
|
||||||
|
import {
|
||||||
|
featureContextMapInjectable,
|
||||||
|
featureContextMapInjectionToken,
|
||||||
|
} from "./feature-context-map-injectable";
|
||||||
|
|
||||||
|
export const registerFeature = (di: DiContainer, ...features: Feature[]) => {
|
||||||
|
features.forEach((feature) => {
|
||||||
|
registerFeatureRecursed(di, feature);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerFeatureRecursed = (
|
||||||
|
di: DiContainer,
|
||||||
|
feature: Feature,
|
||||||
|
dependedBy?: Feature
|
||||||
|
) => {
|
||||||
|
const featureContextMaps = di.injectMany(featureContextMapInjectionToken);
|
||||||
|
|
||||||
|
if (featureContextMaps.length === 0) {
|
||||||
|
di.register(featureContextMapInjectable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureContextMap = di.inject(featureContextMapInjectable);
|
||||||
|
|
||||||
|
const existingFeatureContext = featureContextMap.get(feature);
|
||||||
|
if (
|
||||||
|
!dependedBy &&
|
||||||
|
existingFeatureContext &&
|
||||||
|
existingFeatureContext.dependedBy.size === 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to register feature "${feature.id}", but it was already registered.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureContext =
|
||||||
|
existingFeatureContext || createFeatureContext(feature, di);
|
||||||
|
|
||||||
|
featureContext.numberOfRegistrations++;
|
||||||
|
|
||||||
|
if (dependedBy) {
|
||||||
|
const oldNumberOfDependents =
|
||||||
|
featureContext.dependedBy.get(dependedBy) || 0;
|
||||||
|
|
||||||
|
const newNumberOfDependants = oldNumberOfDependents + 1;
|
||||||
|
featureContext.dependedBy.set(dependedBy, newNumberOfDependants);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingFeatureContext) {
|
||||||
|
featureContext.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
feature.dependencies?.forEach((dependency) => {
|
||||||
|
registerFeatureRecursed(di, dependency, feature);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFeatureContext = (feature: Feature, di: DiContainer) => {
|
||||||
|
const featureContextInjectable = getInjectable({
|
||||||
|
id: feature.id,
|
||||||
|
|
||||||
|
instantiate: (diForContextOfFeature) => ({
|
||||||
|
register: () => {
|
||||||
|
feature.register(diForContextOfFeature);
|
||||||
|
},
|
||||||
|
|
||||||
|
deregister: () => {
|
||||||
|
diForContextOfFeature.deregister(featureContextInjectable);
|
||||||
|
},
|
||||||
|
|
||||||
|
dependedBy: new Map<Feature, number>(),
|
||||||
|
|
||||||
|
numberOfRegistrations: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
scope: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
di.register(featureContextInjectable);
|
||||||
|
|
||||||
|
const featureContextMap = di.inject(featureContextMapInjectable);
|
||||||
|
const featureContext = di.inject(featureContextInjectable);
|
||||||
|
|
||||||
|
featureContextMap.set(feature, featureContext);
|
||||||
|
|
||||||
|
return featureContext;
|
||||||
|
};
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
import { registerFeature } from "./register-feature";
|
||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
DiContainer,
|
||||||
|
getInjectable,
|
||||||
|
Injectable,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
import type { Feature } from "./feature";
|
||||||
|
import { getFeature } from "./feature";
|
||||||
|
import { deregisterFeature } from "./deregister-feature";
|
||||||
|
|
||||||
|
describe("register-feature", () => {
|
||||||
|
describe("given di-container and a Features with injectables, and given Features are registered", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let someInjectable: Injectable<string>;
|
||||||
|
let someInjectable2: Injectable<string>;
|
||||||
|
let someFeature: Feature;
|
||||||
|
let someFeature2: Feature;
|
||||||
|
let instance: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
someInjectable = getInjectable({
|
||||||
|
id: "some-injectable",
|
||||||
|
instantiate: () => "some-instance",
|
||||||
|
});
|
||||||
|
|
||||||
|
someInjectable2 = getInjectable({
|
||||||
|
id: "some-injectable-2",
|
||||||
|
instantiate: () => "some-instance-2",
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature = getFeature({
|
||||||
|
id: "some-feature-1",
|
||||||
|
register: (di) => di.register(someInjectable),
|
||||||
|
});
|
||||||
|
|
||||||
|
someFeature2 = getFeature({
|
||||||
|
id: "some-feature-2",
|
||||||
|
register: (di) => di.register(someInjectable2),
|
||||||
|
});
|
||||||
|
|
||||||
|
registerFeature(di, someFeature);
|
||||||
|
registerFeature(di, someFeature2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when an injectable is injected, does so", () => {
|
||||||
|
instance = di.inject(someInjectable);
|
||||||
|
|
||||||
|
expect(instance).toBe("some-instance");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a Feature is deregistered", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
deregisterFeature(di, someFeature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when injecting a related injectable, throws", () => {
|
||||||
|
expect(() => {
|
||||||
|
di.inject(someInjectable);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when injecting an unrelated injectable, does so", () => {
|
||||||
|
const instance = di.inject(someInjectable2);
|
||||||
|
|
||||||
|
expect(instance).toBe("some-instance-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given the Feature is registered again", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
registerFeature(di, someFeature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when injecting a related injectable, does so", () => {
|
||||||
|
const instance = di.inject(someInjectable);
|
||||||
|
|
||||||
|
expect(instance).toBe("some-instance");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when injecting an unrelated injectable, does so", () => {
|
||||||
|
const instance = di.inject(someInjectable2);
|
||||||
|
|
||||||
|
expect(instance).toBe("some-instance-2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when a Feature is registered again, throws", () => {
|
||||||
|
expect(() => {
|
||||||
|
registerFeature(di, someFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to register feature "some-feature-1", but it was already registered.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given a Feature deregistered, when deregistered again, throws", () => {
|
||||||
|
deregisterFeature(di, someFeature);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
deregisterFeature(di, someFeature);
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to deregister feature "some-feature-1", but it was not registered.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given di-container and registered Features with injectables forming a cycle, when an injectable is injected, throws with namespaced error about cycle", () => {
|
||||||
|
const someInjectable: Injectable<any> = getInjectable({
|
||||||
|
id: "some-injectable-1",
|
||||||
|
instantiate: (di) => di.inject(someInjectable2),
|
||||||
|
});
|
||||||
|
|
||||||
|
const someInjectable2: Injectable<any> = getInjectable({
|
||||||
|
id: "some-injectable-2",
|
||||||
|
instantiate: (di) => di.inject(someInjectable),
|
||||||
|
});
|
||||||
|
|
||||||
|
const di = createContainer("some-container");
|
||||||
|
|
||||||
|
const someFeature = getFeature({
|
||||||
|
id: "some-feature-1",
|
||||||
|
|
||||||
|
register: (di) => {
|
||||||
|
di.register(someInjectable);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const someFeature2 = getFeature({
|
||||||
|
id: "some-feature-2",
|
||||||
|
|
||||||
|
register: (di) => {
|
||||||
|
di.register(someInjectable2);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registerFeature(di, someFeature, someFeature2);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
di.inject(someInjectable);
|
||||||
|
}).toThrow(
|
||||||
|
// 'Cycle of injectables encountered: "some-container" -> "some-feature-1:some-injectable-1" -> "some-feature-2:some-injectable-2" -> "some-feature-1:some-injectable-1"'
|
||||||
|
'Maximum call stack size exceeded'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
3
packages/technical-features/feature-core/tsconfig.json
Normal file
3
packages/technical-features/feature-core/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@k8slens/typescript/config/base.json"
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@k8slens/webpack").configForNode;
|
||||||
Loading…
Reference in New Issue
Block a user