Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce package for Features #7242

Merged
merged 1 commit into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions packages/technical-features/feature-core/README.md
Original file line number Diff line number Diff line change
@@ -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`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can the example above show this then. Not the best if the best practice is not shown in the only example code.

Copy link
Contributor Author

@jansav jansav Feb 28, 2023

Choose a reason for hiding this comment

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

That's just example which shows how Feature is created. We will have scaffolding for Features which will demonstrate both bullet points. And link for that will be updated here.

3 changes: 3 additions & 0 deletions packages/technical-features/feature-core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { getFeature } from "./src/feature";
export { registerFeature } from "./src/register-feature";
export type { Feature, GetFeatureArgs } from "./src/feature";
2 changes: 2 additions & 0 deletions packages/technical-features/feature-core/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports =
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
30 changes: 30 additions & 0 deletions packages/technical-features/feature-core/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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 };