Skip to content

Commit

Permalink
Introduce package for Features
Browse files Browse the repository at this point in the history
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
  • Loading branch information
jansav committed Feb 28, 2023
1 parent d29615c commit 5f01374
Show file tree
Hide file tree
Showing 13 changed files with 730 additions and 0 deletions.
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`
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"
}
}
82 changes: 82 additions & 0 deletions packages/technical-features/feature-core/src/deregister-feature.ts
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 };

0 comments on commit 5f01374

Please sign in to comment.