mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Switch to using messaging and startable stoppable from NPM package (#7368)
* Add custom jest resolver to fix requiring "uuid" module Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Update dependencies Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce test utils for rendering and running with thrown mobx reactions Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Extract startable-stoppable to NPM package Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Extract messaging to NPM package Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using startable-stoppable from NPM package Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using messaging from the Feature Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove old implementation of messaging Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make setupping app paths happen earlier in renderer Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix typo Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add kludge to make testing-library work properly from test-utils package Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix code style Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add lint:fix -root script Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix unrelated failing unit tests Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Turn of no-floating-promises from typescript linting for being broken Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make linting not happen for dist -directories Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make linting failures appear as failure Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Stop running prettier twice It already gets ran as eslint-plugin. Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make CI run unit tests for all packages by consolidating name of NPM script Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add missing unit tests for coverage Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Skip coverage for test utils Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove check for coverage in packages which are not ready for it Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Stop collecting coverage from index.ts files them being indirections to the implementation Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Implement sending message to channel in main Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add missing feature dependencies Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add dummy implementations for requesting in main from renderer Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Re-enable communicating from main to cluster frames Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Ignore trivial files from coverage Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Update package-lock Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Extract message-bridge to separate NPM package to prevent dev dependencies being in the production bundle Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Extract computed channel to own NPM package for clear dependencies Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Consolidate electron related stuff to a directory Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add missing publish configurations Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Ignore test implementation from coverage being not interesting Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> --------- Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
1 parent
df49ff9b96
commit
0f1f030a06
261 files changed
+4447
-1795
No files matched your search
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "@k8slens/eslint-config/eslint",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"@k8slens/eslint-config/prettier"
|
||||
@@ -0,0 +1,12 @@
|
||||
export {
|
||||
computedChannelInjectionToken,
|
||||
computedChannelObserverInjectionToken,
|
||||
} from "./src/computed-channel/computed-channel.injectable";
|
||||
|
||||
export type {
|
||||
ChannelObserver,
|
||||
ComputedChannelFactory,
|
||||
JsonifiableObject,
|
||||
JsonifiableArray,
|
||||
Jsonifiable,
|
||||
} from "./src/computed-channel/computed-channel.injectable";
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@k8slens/computed-channel",
|
||||
"private": false,
|
||||
"version": "1.0.0-alpha.1",
|
||||
"description": "MobX-like computed between channels",
|
||||
"type": "commonjs",
|
||||
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
|
||||
"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:unit": "jest --coverage --runInBand",
|
||||
"lint": "lens-lint",
|
||||
"lint:fix": "lens-lint --fix"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@k8slens/application": "^6.5.0-alpha.0",
|
||||
"@k8slens/feature-core": "^6.5.0-alpha.0",
|
||||
"@k8slens/messaging": "^1.0.0-alpha.1",
|
||||
"@ogre-tools/fp": "^15.1.2",
|
||||
"@ogre-tools/injectable": "^15.1.2",
|
||||
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
|
||||
"@ogre-tools/injectable-extension-for-mobx": "^15.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mobx": "^6.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@k8slens/eslint-config": "^6.5.0-alpha.1",
|
||||
"@k8slens/messaging-fake-bridge": "^1.0.0-alpha.1",
|
||||
"type-fest": "^2.14.0"
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { reaction } from "mobx";
|
||||
|
||||
import { getMessageChannelListenerInjectable } from "@k8slens/messaging";
|
||||
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
|
||||
import type { JsonPrimitive } from "type-fest";
|
||||
import { computedChannelObserverInjectionToken } from "./computed-channel.injectable";
|
||||
import { getMessageChannel } from "@k8slens/messaging";
|
||||
|
||||
export type JsonifiableObject = { [Key in string]?: Jsonifiable } | { toJSON: () => Jsonifiable };
|
||||
export type JsonifiableArray = readonly Jsonifiable[];
|
||||
export type Jsonifiable = JsonPrimitive | JsonifiableObject | JsonifiableArray;
|
||||
|
||||
export type ComputedChannelAdminMessage = {
|
||||
channelId: string;
|
||||
status: "became-observed" | "became-unobserved";
|
||||
};
|
||||
|
||||
export const computedChannelAdministrationChannel = getMessageChannel<ComputedChannelAdminMessage>(
|
||||
"computed-channel-administration-channel",
|
||||
);
|
||||
|
||||
export const computedChannelAdministrationListenerInjectable = getMessageChannelListenerInjectable({
|
||||
id: "computed-channel-administration",
|
||||
channel: computedChannelAdministrationChannel,
|
||||
|
||||
getHandler: (di) => {
|
||||
const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
|
||||
|
||||
const disposersByChannelId = new Map<string, () => void>();
|
||||
|
||||
return (message) => {
|
||||
if (message.status === "became-observed") {
|
||||
const result = di
|
||||
.injectMany(computedChannelObserverInjectionToken)
|
||||
.find((channelObserver) => channelObserver.channel.id === message.channelId);
|
||||
|
||||
if (result === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const disposer = reaction(
|
||||
() => result.observer.get(),
|
||||
(observed) =>
|
||||
sendMessageToChannel(
|
||||
{
|
||||
id: message.channelId,
|
||||
},
|
||||
|
||||
observed,
|
||||
),
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
);
|
||||
|
||||
disposersByChannelId.set(message.channelId, disposer);
|
||||
} else {
|
||||
const disposer = disposersByChannelId.get(message.channelId);
|
||||
|
||||
disposer?.();
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
|
||||
import {
|
||||
_getGlobalState,
|
||||
computed,
|
||||
IComputedValue,
|
||||
observable,
|
||||
onBecomeObserved,
|
||||
onBecomeUnobserved,
|
||||
runInAction,
|
||||
} from "mobx";
|
||||
|
||||
import type { MessageChannel } from "@k8slens/messaging";
|
||||
import { getMessageChannelListenerInjectable } from "@k8slens/messaging";
|
||||
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
|
||||
import type { JsonPrimitive } from "type-fest";
|
||||
import { computedChannelAdministrationChannel } from "./computed-channel-administration-channel.injectable";
|
||||
|
||||
export type JsonifiableObject = { [Key in string]?: Jsonifiable } | { toJSON: () => Jsonifiable };
|
||||
export type JsonifiableArray = readonly Jsonifiable[];
|
||||
export type Jsonifiable = JsonPrimitive | JsonifiableObject | JsonifiableArray;
|
||||
|
||||
export type ComputedChannelFactory = <T>(
|
||||
channel: MessageChannel<T>,
|
||||
pendingValue: T,
|
||||
) => IComputedValue<T>;
|
||||
|
||||
export const computedChannelInjectionToken = getInjectionToken<ComputedChannelFactory>({
|
||||
id: "computed-channel-injection-token",
|
||||
});
|
||||
|
||||
export type ChannelObserver<T extends Jsonifiable> = {
|
||||
channel: MessageChannel<T>;
|
||||
observer: IComputedValue<T>;
|
||||
};
|
||||
|
||||
export const computedChannelObserverInjectionToken = getInjectionToken<
|
||||
ChannelObserver<Jsonifiable>
|
||||
>({
|
||||
id: "computed-channel-observer",
|
||||
});
|
||||
|
||||
const computedChannelInjectable = getInjectable({
|
||||
id: "computed-channel",
|
||||
|
||||
instantiate: (di) => {
|
||||
const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
|
||||
|
||||
return ((channel, pendingValue) => {
|
||||
const observableValue = observable.box(pendingValue);
|
||||
|
||||
const computedValue = computed(() => {
|
||||
const { trackingDerivation } = _getGlobalState();
|
||||
|
||||
const contextIsReactive = !!trackingDerivation;
|
||||
|
||||
if (!contextIsReactive) {
|
||||
throw new Error(
|
||||
`Tried to access value of computed channel "${channel.id}" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.`,
|
||||
);
|
||||
}
|
||||
|
||||
return observableValue.get();
|
||||
});
|
||||
|
||||
const valueReceiverInjectable = getMessageChannelListenerInjectable({
|
||||
id: `computed-channel-value-receiver-for-${channel.id}`,
|
||||
channel,
|
||||
|
||||
getHandler: () => (message) => {
|
||||
runInAction(() => {
|
||||
observableValue.set(message);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
di.register(valueReceiverInjectable);
|
||||
});
|
||||
|
||||
onBecomeObserved(computedValue, () => {
|
||||
runInAction(() => {
|
||||
observableValue.set(pendingValue);
|
||||
});
|
||||
|
||||
sendMessageToChannel(computedChannelAdministrationChannel, {
|
||||
channelId: channel.id,
|
||||
status: "became-observed",
|
||||
});
|
||||
});
|
||||
|
||||
onBecomeUnobserved(computedValue, () => {
|
||||
runInAction(() => {
|
||||
observableValue.set(pendingValue);
|
||||
});
|
||||
|
||||
sendMessageToChannel(computedChannelAdministrationChannel, {
|
||||
channelId: channel.id,
|
||||
status: "became-unobserved",
|
||||
});
|
||||
});
|
||||
|
||||
return computedValue;
|
||||
}) as ComputedChannelFactory;
|
||||
},
|
||||
|
||||
injectionToken: computedChannelInjectionToken,
|
||||
});
|
||||
|
||||
export default computedChannelInjectable;
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
import React from "react";
|
||||
import { act } from "@testing-library/react";
|
||||
import { createContainer, DiContainer, getInjectable } from "@ogre-tools/injectable";
|
||||
import { getMessageBridgeFake, MessageBridgeFake } from "@k8slens/messaging-fake-bridge";
|
||||
import { startApplicationInjectionToken } from "@k8slens/application";
|
||||
import {
|
||||
computed,
|
||||
IComputedValue,
|
||||
IObservableValue,
|
||||
observable,
|
||||
reaction,
|
||||
runInAction,
|
||||
} from "mobx";
|
||||
import type { MessageChannel } from "@k8slens/messaging";
|
||||
import { getMessageChannelListenerInjectable } from "@k8slens/messaging";
|
||||
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
||||
import { registerFeature } from "@k8slens/feature-core";
|
||||
import { testUtils } from "@k8slens/messaging";
|
||||
import {
|
||||
computedChannelInjectionToken,
|
||||
computedChannelObserverInjectionToken,
|
||||
} from "./computed-channel.injectable";
|
||||
import { runWithThrownMobxReactions, renderFor } from "@k8slens/test-utils";
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
computedChannelAdministrationChannel,
|
||||
ComputedChannelAdminMessage,
|
||||
} from "./computed-channel-administration-channel.injectable";
|
||||
import { computedChannelFeature } from "../feature";
|
||||
|
||||
const testChannel: MessageChannel<string> = { id: "some-channel-id" };
|
||||
const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
|
||||
|
||||
const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue<string> }) => (
|
||||
<div>{someComputed.get()}</div>
|
||||
));
|
||||
|
||||
[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach(({ scenarioIsAsync }) =>
|
||||
describe(`computed-channel, given running message bridge fake as ${
|
||||
scenarioIsAsync ? "async" : "sync"
|
||||
}`, () => {
|
||||
describe("given multiple dis and a message channel and a channel observer and application has started", () => {
|
||||
let di1: DiContainer;
|
||||
let di2: DiContainer;
|
||||
let latestAdminMessage: ComputedChannelAdminMessage | undefined;
|
||||
let latestValueMessage: string | undefined;
|
||||
let messageBridgeFake: MessageBridgeFake;
|
||||
|
||||
beforeEach(async () => {
|
||||
latestAdminMessage = undefined;
|
||||
latestValueMessage = undefined;
|
||||
|
||||
di1 = createContainer("some-container-1");
|
||||
di2 = createContainer("some-container-2");
|
||||
registerMobX(di1);
|
||||
registerMobX(di2);
|
||||
|
||||
const administrationChannelTestListenerInjectable = getMessageChannelListenerInjectable({
|
||||
id: "administration-channel-test-listener",
|
||||
channel: computedChannelAdministrationChannel,
|
||||
|
||||
getHandler: () => (adminMessage) => {
|
||||
latestAdminMessage = adminMessage;
|
||||
},
|
||||
});
|
||||
|
||||
const channelValueTestListenerInjectable = getMessageChannelListenerInjectable({
|
||||
id: "test-channel-value-listener",
|
||||
channel: testChannel,
|
||||
|
||||
getHandler: () => (message) => {
|
||||
latestValueMessage = message;
|
||||
},
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
const messagingFeature = testUtils.messagingFeatureForUnitTesting;
|
||||
|
||||
registerFeature(di1, messagingFeature, computedChannelFeature);
|
||||
registerFeature(di2, messagingFeature, computedChannelFeature);
|
||||
|
||||
di1.register(channelValueTestListenerInjectable);
|
||||
di2.register(administrationChannelTestListenerInjectable);
|
||||
});
|
||||
|
||||
messageBridgeFake = getMessageBridgeFake();
|
||||
messageBridgeFake.setAsync(scenarioIsAsync);
|
||||
messageBridgeFake.involve(di1, di2);
|
||||
|
||||
await Promise.all([
|
||||
di1.inject(startApplicationInjectionToken)(),
|
||||
di2.inject(startApplicationInjectionToken)(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("given a channel observer and matching computed channel for the channel in di-2", () => {
|
||||
let someObservable: IObservableValue<string>;
|
||||
let computedTestChannel: IComputedValue<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
someObservable = observable.box<string>("some-initial-value");
|
||||
|
||||
const channelObserverInjectable = getInjectable({
|
||||
id: "some-channel-observer",
|
||||
|
||||
instantiate: () => ({
|
||||
channel: testChannel,
|
||||
observer: computed(() => someObservable.get()),
|
||||
}),
|
||||
|
||||
injectionToken: computedChannelObserverInjectionToken,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
di2.register(channelObserverInjectable);
|
||||
});
|
||||
|
||||
const computedChannel = di1.inject(computedChannelInjectionToken);
|
||||
|
||||
computedTestChannel = computedChannel(testChannel, "some-pending-value");
|
||||
});
|
||||
|
||||
it("there is no admin message yet", () => {
|
||||
expect(latestAdminMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("when observing the computed value in a component in di-1", () => {
|
||||
let rendered: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const render = renderFor(di2);
|
||||
|
||||
rendered = render(<TestComponent someComputed={computedTestChannel} />);
|
||||
});
|
||||
|
||||
const scenarioName = scenarioIsAsync ? "when all messages are propagated" : "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioName, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
messageBridgeFake.messagePropagationRecursive(act).then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.container).toHaveTextContent("some-initial-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when observing the computed channel in di-1", () => {
|
||||
let observedValue: string | undefined;
|
||||
let stopObserving: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
observedValue = undefined;
|
||||
|
||||
stopObserving = reaction(
|
||||
() => computedTestChannel.get(),
|
||||
(value) => {
|
||||
observedValue = value;
|
||||
},
|
||||
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
scenarioIsAsync &&
|
||||
it("computed test channel value is observed as the pending value", () => {
|
||||
expect(observedValue).toBe("some-pending-value");
|
||||
});
|
||||
|
||||
const scenarioName = scenarioIsAsync
|
||||
? "when admin messages are propagated"
|
||||
: "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioName, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
void messageBridgeFake.messagePropagation().then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("administration-message to start observing gets listened", () => {
|
||||
expect(latestAdminMessage).toEqual({
|
||||
channelId: "some-channel-id",
|
||||
status: "became-observed",
|
||||
});
|
||||
});
|
||||
|
||||
const scenarioName = scenarioIsAsync
|
||||
? "when returning value-messages propagate"
|
||||
: "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioName, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
void messageBridgeFake.messagePropagation().then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("the computed channel value in di-1 matches the value in di-2", () => {
|
||||
expect(observedValue).toBe("some-initial-value");
|
||||
});
|
||||
|
||||
it("the value gets listened in di-1", () => {
|
||||
expect(latestValueMessage).toBe("some-initial-value");
|
||||
});
|
||||
|
||||
describe("when the observed value changes", () => {
|
||||
beforeEach(async () => {
|
||||
latestValueMessage = undefined;
|
||||
|
||||
runInAction(() => {
|
||||
someObservable.set("some-new-value");
|
||||
});
|
||||
});
|
||||
|
||||
const scenarioName = scenarioIsAsync
|
||||
? "when value-messages propagate"
|
||||
: "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioName, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
void messageBridgeFake.messagePropagation().then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("the computed channel value in di-1 changes", () => {
|
||||
expect(observedValue).toBe("some-new-value");
|
||||
});
|
||||
|
||||
it("the new value gets listened in di-1", () => {
|
||||
expect(latestValueMessage).toBe("some-new-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when stopping observation for the channel in di-1", () => {
|
||||
beforeEach(async () => {
|
||||
latestValueMessage = undefined;
|
||||
|
||||
stopObserving();
|
||||
});
|
||||
|
||||
const scenarioName = scenarioIsAsync
|
||||
? "when admin-messages propagate"
|
||||
: "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioName, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
void messageBridgeFake.messagePropagation().then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("messages administration channel to stop observing", () => {
|
||||
expect(latestAdminMessage).toEqual({
|
||||
channelId: "some-channel-id",
|
||||
status: "became-unobserved",
|
||||
});
|
||||
});
|
||||
|
||||
it("no value gets listened in di-1 anymore", () => {
|
||||
expect(latestValueMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("when the observed value changes", () => {
|
||||
beforeEach(async () => {
|
||||
latestValueMessage = undefined;
|
||||
|
||||
runInAction(() => {
|
||||
someObservable.set("some-new-value-2");
|
||||
});
|
||||
});
|
||||
|
||||
it("when accessing the computed value outside of reactive context, throws", () => {
|
||||
expect(() => {
|
||||
computedTestChannel.get();
|
||||
}).toThrow(
|
||||
'Tried to access value of computed channel "some-channel-id" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.',
|
||||
);
|
||||
});
|
||||
|
||||
it("no value gets listened in di-1 anymore", () => {
|
||||
expect(latestValueMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("when observing the computed channel again", () => {
|
||||
beforeEach(() => {
|
||||
observedValue = undefined;
|
||||
|
||||
reaction(
|
||||
() => computedTestChannel.get(),
|
||||
(value) => {
|
||||
observedValue = value;
|
||||
},
|
||||
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
scenarioIsAsync &&
|
||||
it("computed test channel value is observed as the pending value again", () => {
|
||||
expect(observedValue).toBe("some-pending-value");
|
||||
});
|
||||
|
||||
const scenarioName = scenarioIsAsync
|
||||
? "when admin messages propagate"
|
||||
: "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioName, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
latestAdminMessage = undefined;
|
||||
|
||||
void messageBridgeFake.messagePropagation().then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("administration-message to start observing gets listened again", () => {
|
||||
expect(latestAdminMessage).toEqual({
|
||||
channelId: "some-channel-id",
|
||||
status: "became-observed",
|
||||
});
|
||||
});
|
||||
|
||||
scenarioIsAsync &&
|
||||
it("computed test channel value is still observed as the pending value", () => {
|
||||
expect(observedValue).toBe("some-pending-value");
|
||||
});
|
||||
|
||||
const scenarioTitle = scenarioIsAsync
|
||||
? "when value-messages propagate back"
|
||||
: "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioTitle, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
latestValueMessage = undefined;
|
||||
|
||||
void messageBridgeFake.messagePropagation().then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("the computed channel value changes", () => {
|
||||
expect(observedValue).toBe("some-new-value-2");
|
||||
});
|
||||
|
||||
it("the current value gets listened", () => {
|
||||
expect(latestValueMessage).toBe("some-new-value-2");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("when accessing the computed value outside of reactive context, throws", () => {
|
||||
expect(() => {
|
||||
computedTestChannel.get();
|
||||
}).toThrow(
|
||||
'Tried to access value of computed channel "some-channel-id" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("given observation of unrelated computed channel is stopped, observation of other computed channel still works", async () => {
|
||||
const someOtherObservable = observable.box<string>("");
|
||||
|
||||
const channelObserver2Injectable = getInjectable({
|
||||
id: "some-channel-observer-2",
|
||||
|
||||
instantiate: () => ({
|
||||
channel: testChannel2,
|
||||
observer: computed(() => someOtherObservable.get()),
|
||||
}),
|
||||
|
||||
injectionToken: computedChannelObserverInjectionToken,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
di2.register(channelObserver2Injectable);
|
||||
});
|
||||
|
||||
const computedChannel = di1.inject(computedChannelInjectionToken);
|
||||
|
||||
computedTestChannel = computedChannel(testChannel2, "some-pending-value");
|
||||
|
||||
reaction(
|
||||
() => computedTestChannel.get(),
|
||||
(value) => {
|
||||
observedValue = value;
|
||||
},
|
||||
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
);
|
||||
|
||||
scenarioIsAsync && (await messageBridgeFake.messagePropagation());
|
||||
|
||||
stopObserving();
|
||||
|
||||
scenarioIsAsync && (await messageBridgeFake.messagePropagation());
|
||||
|
||||
runInAction(() => {
|
||||
someOtherObservable.set("some-value");
|
||||
});
|
||||
|
||||
scenarioIsAsync && (await messageBridgeFake.messagePropagation());
|
||||
|
||||
expect(observedValue).toBe("some-value");
|
||||
});
|
||||
|
||||
describe("when observing the computed channel again", () => {
|
||||
beforeEach(() => {
|
||||
latestAdminMessage = undefined;
|
||||
|
||||
reaction(
|
||||
() => computedTestChannel.get(),
|
||||
(value) => {
|
||||
observedValue = value;
|
||||
},
|
||||
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't send second administration message", () => {
|
||||
expect(latestAdminMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it("when one of the observations stops, doesn't send administration message to stop observing", async () => {
|
||||
latestAdminMessage = undefined;
|
||||
|
||||
stopObserving();
|
||||
|
||||
expect(latestAdminMessage).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when accessing the computed value outside of reactive context", () => {
|
||||
let nonReactiveValue: string;
|
||||
|
||||
beforeEach(() => {
|
||||
latestValueMessage = undefined;
|
||||
latestAdminMessage = undefined;
|
||||
|
||||
nonReactiveValue = computedTestChannel.get();
|
||||
});
|
||||
|
||||
it("the non reactive value is what ever happens to be the current value from di-2", () => {
|
||||
expect(nonReactiveValue).toBe("some-initial-value");
|
||||
});
|
||||
|
||||
const scenarioName = scenarioIsAsync
|
||||
? "when messages would be propagated"
|
||||
: "immediately";
|
||||
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(scenarioName, () => {
|
||||
beforeEach((done) => {
|
||||
if (scenarioIsAsync) {
|
||||
void messageBridgeFake.messagePropagation().then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not send new value message", () => {
|
||||
expect(latestValueMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not send new admin message", () => {
|
||||
expect(latestAdminMessage).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("when accessing the computed value outside of reactive context, throws", () => {
|
||||
expect(() => {
|
||||
computedTestChannel.get();
|
||||
}).toThrow(
|
||||
'Tried to access value of computed channel "some-channel-id" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.',
|
||||
);
|
||||
});
|
||||
|
||||
it("given duplicate channel observer for the channel is registered, when the computed channel is observer, throws", () => {
|
||||
const duplicateChannelObserverInjectable = getInjectable({
|
||||
id: "some-duplicate-channel-observer",
|
||||
|
||||
instantiate: () => ({
|
||||
channel: testChannel,
|
||||
observer: computed(() => "irrelevant"),
|
||||
}),
|
||||
|
||||
injectionToken: computedChannelObserverInjectionToken,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
runWithThrownMobxReactions(() => {
|
||||
runInAction(() => {
|
||||
di2.register(duplicateChannelObserverInjectable);
|
||||
});
|
||||
});
|
||||
}).toThrow('Tried to register duplicate channel observer for channels "some-channel-id"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("given no channel observer but still a computed channel", () => {
|
||||
let computedTestChannel: IComputedValue<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
const computedChannel = di1.inject(computedChannelInjectionToken);
|
||||
|
||||
computedTestChannel = computedChannel(testChannel, "some-pending-value");
|
||||
});
|
||||
|
||||
it("when the computed channel is observed, observes as undefined", () => {
|
||||
let observedValue = "some-value-to-never-be-seen-in-unit-test";
|
||||
|
||||
reaction(
|
||||
() => computedTestChannel.get(),
|
||||
|
||||
(value) => {
|
||||
observedValue = value;
|
||||
},
|
||||
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(observedValue).toBe("some-pending-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
|
||||
import { filter, groupBy, nth, map, toPairs } from "lodash/fp";
|
||||
import { reaction } from "mobx";
|
||||
import { computedChannelObserverInjectionToken } from "./computed-channel.injectable";
|
||||
|
||||
export const duplicateChannelObserverGuardInjectable = getInjectable({
|
||||
id: "duplicate-channel-observer-guard",
|
||||
|
||||
instantiate: (di) => {
|
||||
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
||||
|
||||
return {
|
||||
run: () => {
|
||||
reaction(
|
||||
() => computedInjectMany(computedChannelObserverInjectionToken).get(),
|
||||
(observers) => {
|
||||
const duplicateObserverChannelIds = pipeline(
|
||||
observers,
|
||||
groupBy((observer) => observer.channel.id),
|
||||
toPairs,
|
||||
filter(([, channelObservers]) => channelObservers.length > 1),
|
||||
map(nth(0)),
|
||||
);
|
||||
|
||||
if (duplicateObserverChannelIds.length) {
|
||||
throw new Error(
|
||||
`Tried to register duplicate channel observer for channels "${duplicateObserverChannelIds.join(
|
||||
'", "',
|
||||
)}"`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
injectionToken: onLoadOfApplicationInjectionToken,
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
|
||||
import { getFeature } from "@k8slens/feature-core";
|
||||
import { messagingFeature } from "@k8slens/messaging";
|
||||
|
||||
export const computedChannelFeature = getFeature({
|
||||
id: "computed-channel",
|
||||
|
||||
dependencies: [messagingFeature],
|
||||
|
||||
register: (di) => {
|
||||
autoRegister({
|
||||
di,
|
||||
targetModule: module,
|
||||
|
||||
getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@k8slens/typescript/config/base.json",
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("@k8slens/webpack").configForNode;
|
||||
Reference in new issue
Block a user