mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Extract messaging to NPM package
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
5c766367c5
commit
d7bacec7bd
1708
package-lock.json
generated
1708
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2
packages/technical-features/messaging/agnostic/index.ts
Normal file
2
packages/technical-features/messaging/agnostic/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './src/features/actual';
|
||||||
|
export * as testUtils from './src/features/unit-testing';
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
module.exports =
|
||||||
|
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
|
||||||
45
packages/technical-features/messaging/agnostic/package.json
Normal file
45
packages/technical-features/messaging/agnostic/package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "@k8slens/messaging",
|
||||||
|
"private": false,
|
||||||
|
"version": "6.5.0-alpha.0",
|
||||||
|
"description": "An abstraction for messaging between Lens environments",
|
||||||
|
"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",
|
||||||
|
"lint:fix": "lens-lint --fix",
|
||||||
|
"lint": "lens-lint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@k8slens/application": "^6.5.0-alpha.0",
|
||||||
|
"@k8slens/startable-stoppable": "^6.5.0-alpha.0",
|
||||||
|
"@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",
|
||||||
|
"@ogre-tools/test-utils": "^15.1.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mobx": "^6.7.0"
|
||||||
|
},
|
||||||
|
|
||||||
|
"devDependencies": {
|
||||||
|
"@async-fn/jest": "^1.6.4",
|
||||||
|
"type-fest": "^2.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export interface Channel<MessageTemplate = void, ReturnTemplate = void> {
|
||||||
|
id: string;
|
||||||
|
_messageTemplate?: MessageTemplate;
|
||||||
|
_returnTemplate?: ReturnTemplate;
|
||||||
|
}
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import {
|
||||||
|
_getGlobalState,
|
||||||
|
computed,
|
||||||
|
IComputedValue,
|
||||||
|
observable,
|
||||||
|
onBecomeObserved,
|
||||||
|
onBecomeUnobserved,
|
||||||
|
reaction,
|
||||||
|
runInAction,
|
||||||
|
} from "mobx";
|
||||||
|
|
||||||
|
import type { MessageChannel } from "../message/message-channel-listener-injection-token";
|
||||||
|
import { getMessageChannelListenerInjectable } from "../message/message-channel-listener-injection-token";
|
||||||
|
import { sendMessageToChannelInjectionToken } from "../message/message-to-channel-injection-token.no-coverage";
|
||||||
|
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { filter, groupBy, map, nth, toPairs } from "lodash/fp";
|
||||||
|
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
|
import type { JsonPrimitive } from "type-fest";
|
||||||
|
|
||||||
|
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 type ComputedChannelAdminMessage = {
|
||||||
|
channelId: string;
|
||||||
|
status: "became-observed" | "became-unobserved";
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const computedChannelAdministrationChannel: MessageChannel<ComputedChannelAdminMessage> =
|
||||||
|
{
|
||||||
|
id: "computed-channel-administration-channel",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computedChannelAdministrationListenerInjectable =
|
||||||
|
getMessageChannelListenerInjectable({
|
||||||
|
id: "computed-channel-administration",
|
||||||
|
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!();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
channel: computedChannelAdministrationChannel,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default computedChannelInjectable;
|
||||||
@ -0,0 +1,614 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { act } from "@testing-library/react";
|
||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
DiContainer,
|
||||||
|
getInjectable,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
import {
|
||||||
|
getMessageBridgeFake,
|
||||||
|
MessageBridgeFake,
|
||||||
|
} from "../../unit-testing/get-message-bridge-fake/get-message-bridge-fake";
|
||||||
|
import { startApplicationInjectionToken } from "@k8slens/application";
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
IComputedValue,
|
||||||
|
IObservableValue,
|
||||||
|
observable,
|
||||||
|
reaction,
|
||||||
|
runInAction,
|
||||||
|
} from "mobx";
|
||||||
|
import type { MessageChannel } from "../message/message-channel-listener-injection-token";
|
||||||
|
import { getMessageChannelListenerInjectable } from "../message/message-channel-listener-injection-token";
|
||||||
|
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { messagingFeature } from "../feature";
|
||||||
|
import {
|
||||||
|
computedChannelAdministrationChannel,
|
||||||
|
ComputedChannelAdminMessage,
|
||||||
|
computedChannelInjectionToken,
|
||||||
|
computedChannelObserverInjectionToken,
|
||||||
|
} from "./computed-channel.injectable";
|
||||||
|
import { runWithThrownMobxReactions, renderFor } from "@k8slens/test-utils";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
|
const testChannel: MessageChannel<string> = { id: "some-channel-id" };
|
||||||
|
const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
|
||||||
|
|
||||||
|
[{ 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(() => {
|
||||||
|
registerFeature(di1, messagingFeature);
|
||||||
|
registerFeature(di2, messagingFeature);
|
||||||
|
|
||||||
|
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} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when all messages are propagated"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when admin messages are propagated"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
messageBridgeFake.messagePropagation().then(done);
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("administration-message to start observing gets listened", () => {
|
||||||
|
expect(latestAdminMessage).toEqual({
|
||||||
|
channelId: "some-channel-id",
|
||||||
|
status: "became-observed",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when returning value-messages propagate"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when value-messages propagate"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when admin-messages propagate"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when admin messages propagate"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
latestAdminMessage = undefined;
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when value-messages propagate back"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
latestValueMessage = undefined;
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when messages would be propagated"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const TestComponent = observer(
|
||||||
|
({ someComputed }: { someComputed: IComputedValue<string> }) => (
|
||||||
|
<div>{someComputed.get()}</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
|
||||||
|
import { applicationFeature } from "@k8slens/application";
|
||||||
|
import { getFeature } from "@k8slens/feature-core";
|
||||||
|
|
||||||
|
export const messagingFeature = getFeature({
|
||||||
|
id: "messaging",
|
||||||
|
|
||||||
|
dependencies: [applicationFeature],
|
||||||
|
|
||||||
|
register: (di) => {
|
||||||
|
autoRegister({
|
||||||
|
di,
|
||||||
|
targetModule: module,
|
||||||
|
|
||||||
|
getRequireContexts: () => [
|
||||||
|
require.context("./", true, /\.injectable\.(ts|tsx)$/),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
export { messagingFeature } from "./feature";
|
||||||
|
|
||||||
|
export { getRequestChannel } from "./request/get-request-channel";
|
||||||
|
export { getMessageChannel } from "./message/get-message-channel";
|
||||||
|
|
||||||
|
export {
|
||||||
|
computedChannelInjectionToken,
|
||||||
|
computedChannelObserverInjectionToken,
|
||||||
|
} from "./computed-channel/computed-channel.injectable";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ChannelObserver,
|
||||||
|
ComputedChannelFactory,
|
||||||
|
JsonifiableObject,
|
||||||
|
JsonifiableArray,
|
||||||
|
Jsonifiable,
|
||||||
|
} from "./computed-channel/computed-channel.injectable";
|
||||||
|
|
||||||
|
export { requestFromChannelInjectionToken } from "./request/request-from-channel-injection-token.no-coverage";
|
||||||
|
|
||||||
|
export type { Channel } from "./channel.no-coverage";
|
||||||
|
|
||||||
|
export { sendMessageToChannelInjectionToken } from "./message/message-to-channel-injection-token.no-coverage";
|
||||||
|
export type { SendMessageToChannel } from "./message/message-to-channel-injection-token.no-coverage";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
GetMessageChannelListenerInfo,
|
||||||
|
MessageChannel,
|
||||||
|
MessageChannelListener,
|
||||||
|
} from "./message/message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export {
|
||||||
|
messageChannelListenerInjectionToken,
|
||||||
|
getMessageChannelListenerInjectable,
|
||||||
|
} from "./message/message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
RequestChannel,
|
||||||
|
RequestChannelHandler,
|
||||||
|
} from "./request/request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export type { RequestFromChannel } from "./request/request-from-channel-injection-token.no-coverage";
|
||||||
|
|
||||||
|
export type { EnlistMessageChannelListener } from "./message/enlist-message-channel-listener-injection-token";
|
||||||
|
export { enlistMessageChannelListenerInjectionToken } from "./message/enlist-message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export type { EnlistRequestChannelListener } from "./request/enlist-request-channel-listener-injection-token";
|
||||||
|
export { enlistRequestChannelListenerInjectionToken } from "./request/enlist-request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export type { ListeningOfChannels } from "./listening-of-channels/listening-of-channels.injectable";
|
||||||
|
export { listeningOfChannelsInjectionToken } from "./listening-of-channels/listening-of-channels.injectable";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
GetRequestChannelListenerInjectableInfo,
|
||||||
|
RequestChannelListener,
|
||||||
|
} from "./request/request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export {
|
||||||
|
getRequestChannelListenerInjectable,
|
||||||
|
requestChannelListenerInjectionToken,
|
||||||
|
} from "./request/request-channel-listener-injection-token";
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "../message/enlist-message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getStartableStoppable,
|
||||||
|
StartableStoppable,
|
||||||
|
} from "@k8slens/startable-stoppable";
|
||||||
|
|
||||||
|
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
|
import { IComputedValue, reaction } from "mobx";
|
||||||
|
|
||||||
|
import { messageChannelListenerInjectionToken } from "../message/message-channel-listener-injection-token";
|
||||||
|
import { requestChannelListenerInjectionToken } from "../request/request-channel-listener-injection-token";
|
||||||
|
import { enlistRequestChannelListenerInjectionToken } from "../request/enlist-request-channel-listener-injection-token";
|
||||||
|
import type { Channel } from "../channel.no-coverage";
|
||||||
|
|
||||||
|
export type ListeningOfChannels = StartableStoppable;
|
||||||
|
export const listeningOfChannelsInjectionToken =
|
||||||
|
getInjectionToken<ListeningOfChannels>({
|
||||||
|
id: "listening-of-channels-injection-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
const listeningOfChannelsInjectable = getInjectable({
|
||||||
|
id: "listening-of-channels",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const enlistMessageChannelListener = di.inject(
|
||||||
|
enlistMessageChannelListenerInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const enlistRequestChannelListener = di.inject(
|
||||||
|
enlistRequestChannelListenerInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
||||||
|
|
||||||
|
const messageChannelListeners = computedInjectMany(
|
||||||
|
messageChannelListenerInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestChannelListeners = computedInjectMany(
|
||||||
|
requestChannelListenerInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
return getStartableStoppable("listening-of-channels", () => {
|
||||||
|
const stopListeningOfMessageChannels = listening(
|
||||||
|
messageChannelListeners,
|
||||||
|
enlistMessageChannelListener,
|
||||||
|
(x) => x.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopListeningOfRequestChannels = listening(
|
||||||
|
requestChannelListeners,
|
||||||
|
enlistRequestChannelListener,
|
||||||
|
(x) => x.channel.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopListeningOfMessageChannels();
|
||||||
|
stopListeningOfRequestChannels();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: listeningOfChannelsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default listeningOfChannelsInjectable;
|
||||||
|
|
||||||
|
const listening = <T extends { id: string; channel: Channel<unknown> }>(
|
||||||
|
channelListeners: IComputedValue<T[]>,
|
||||||
|
enlistChannelListener: (listener: T) => () => void,
|
||||||
|
getId: (listener: T) => string
|
||||||
|
) => {
|
||||||
|
const listenerDisposers = new Map<string, () => void>();
|
||||||
|
|
||||||
|
const reactionDisposer = reaction(
|
||||||
|
() => channelListeners.get(),
|
||||||
|
(newValues, oldValues = []) => {
|
||||||
|
const addedListeners = newValues.filter(
|
||||||
|
(newValue) => !oldValues.some((oldValue) => oldValue.id === newValue.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const removedListeners = oldValues.filter(
|
||||||
|
(oldValue) => !newValues.some((newValue) => newValue.id === oldValue.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
addedListeners.forEach((listener) => {
|
||||||
|
const id = getId(listener);
|
||||||
|
|
||||||
|
if (listenerDisposers.has(id)) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to add listener for channel "${listener.channel.id}" but listener already exists.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const disposer = enlistChannelListener(listener);
|
||||||
|
|
||||||
|
listenerDisposers.set(id, disposer);
|
||||||
|
});
|
||||||
|
|
||||||
|
removedListeners.forEach((listener) => {
|
||||||
|
const dispose = listenerDisposers.get(getId(listener));
|
||||||
|
|
||||||
|
dispose?.();
|
||||||
|
|
||||||
|
listenerDisposers.delete(getId(listener));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
{ fireImmediately: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reactionDisposer();
|
||||||
|
listenerDisposers.forEach((dispose) => dispose());
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
|
||||||
|
import { listeningOfChannelsInjectionToken } from "./listening-of-channels.injectable";
|
||||||
|
|
||||||
|
const startListeningOfChannelsInjectable = getInjectable({
|
||||||
|
id: "start-listening-of-channels",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const listeningOfChannels = di.inject(listeningOfChannelsInjectionToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: async () => {
|
||||||
|
await listeningOfChannels.start();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: onLoadOfApplicationInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default startListeningOfChannelsInjectable;
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
MessageChannel,
|
||||||
|
MessageChannelListener,
|
||||||
|
} from "./message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export type EnlistMessageChannelListener = (
|
||||||
|
listener: MessageChannelListener<MessageChannel<unknown>>
|
||||||
|
) => () => void;
|
||||||
|
|
||||||
|
export const enlistMessageChannelListenerInjectionToken =
|
||||||
|
getInjectionToken<EnlistMessageChannelListener>({
|
||||||
|
id: "listening-to-a-message-channel",
|
||||||
|
});
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import type { MessageChannel } from "./message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export const getMessageChannel = <Request>(id: string): MessageChannel<Request> => ({
|
||||||
|
id,
|
||||||
|
});
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import type { DiContainerForInjection } from "@ogre-tools/injectable";
|
||||||
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
export interface MessageChannel<Message> {
|
||||||
|
id: string;
|
||||||
|
_messageSignature?: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageChannelHandler<Channel> = Channel extends MessageChannel<
|
||||||
|
infer Message
|
||||||
|
>
|
||||||
|
? (message: Message) => void
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export interface MessageChannelListener<Channel> {
|
||||||
|
id: string;
|
||||||
|
channel: Channel;
|
||||||
|
handler: MessageChannelHandler<Channel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messageChannelListenerInjectionToken = getInjectionToken<
|
||||||
|
MessageChannelListener<MessageChannel<unknown>>
|
||||||
|
>({
|
||||||
|
id: "message-channel-listener",
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface GetMessageChannelListenerInfo<
|
||||||
|
Channel extends MessageChannel<Message>,
|
||||||
|
Message
|
||||||
|
> {
|
||||||
|
id: string;
|
||||||
|
channel: Channel;
|
||||||
|
getHandler: (di: DiContainerForInjection) => MessageChannelHandler<Channel>;
|
||||||
|
causesSideEffects?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMessageChannelListenerInjectable = <
|
||||||
|
Channel extends MessageChannel<Message>,
|
||||||
|
Message
|
||||||
|
>(
|
||||||
|
info: GetMessageChannelListenerInfo<Channel, Message>
|
||||||
|
) =>
|
||||||
|
getInjectable({
|
||||||
|
id: `${info.channel.id}-message-listener-${info.id}`,
|
||||||
|
|
||||||
|
instantiate: (di): MessageChannelListener<Channel> => ({
|
||||||
|
id: `${info.channel.id}-message-listener-${info.id}`,
|
||||||
|
channel: info.channel,
|
||||||
|
handler: info.getHandler(di),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: messageChannelListenerInjectionToken,
|
||||||
|
causesSideEffects: info.causesSideEffects,
|
||||||
|
});
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import type { MessageChannel } from "./message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export interface SendMessageToChannel {
|
||||||
|
(channel: MessageChannel<void>): void;
|
||||||
|
<Message>(channel: MessageChannel<Message>, message: Message): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendMessageToChannelInjectionToken =
|
||||||
|
getInjectionToken<SendMessageToChannel>({
|
||||||
|
id: "send-message-to-message-channel",
|
||||||
|
});
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RequestChannel,
|
||||||
|
RequestChannelListener,
|
||||||
|
} from "./request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export type EnlistRequestChannelListener = (
|
||||||
|
listener: RequestChannelListener<RequestChannel<unknown, unknown>>
|
||||||
|
) => () => void;
|
||||||
|
|
||||||
|
export const enlistRequestChannelListenerInjectionToken =
|
||||||
|
getInjectionToken<EnlistRequestChannelListener>({
|
||||||
|
id: "listening-to-a-request-channel",
|
||||||
|
});
|
||||||
@ -1,7 +1,3 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
||||||
|
|
||||||
export const getRequestChannel = <Request, Response>(id: string): RequestChannel<Request, Response> => ({
|
export const getRequestChannel = <Request, Response>(id: string): RequestChannel<Request, Response> => ({
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import type { DiContainerForInjection } from "@ogre-tools/injectable";
|
||||||
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
export interface RequestChannel<Request, Response> {
|
||||||
|
id: string;
|
||||||
|
_requestSignature?: Request;
|
||||||
|
_responseSignature?: Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestChannelHandler<Channel> = Channel extends RequestChannel<
|
||||||
|
infer Request,
|
||||||
|
infer Response
|
||||||
|
>
|
||||||
|
? (req: Request) => Promise<Response> | Response
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export interface RequestChannelListener<Channel> {
|
||||||
|
id: string;
|
||||||
|
channel: Channel;
|
||||||
|
handler: RequestChannelHandler<Channel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestChannelListenerInjectionToken = getInjectionToken<
|
||||||
|
RequestChannelListener<RequestChannel<unknown, unknown>>
|
||||||
|
>({
|
||||||
|
id: "request-channel-listener",
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface GetRequestChannelListenerInjectableInfo<
|
||||||
|
Channel extends RequestChannel<Request, Response>,
|
||||||
|
Request,
|
||||||
|
Response
|
||||||
|
> {
|
||||||
|
id: string;
|
||||||
|
channel: Channel;
|
||||||
|
getHandler: (di: DiContainerForInjection) => RequestChannelHandler<Channel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRequestChannelListenerInjectable = <
|
||||||
|
Channel extends RequestChannel<Request, Response>,
|
||||||
|
Request,
|
||||||
|
Response
|
||||||
|
>(
|
||||||
|
info: GetRequestChannelListenerInjectableInfo<Channel, Request, Response>
|
||||||
|
) =>
|
||||||
|
getInjectable({
|
||||||
|
id: `${info.channel.id}-request-listener-${info.id}`,
|
||||||
|
|
||||||
|
instantiate: (di) => ({
|
||||||
|
id: `${info.channel.id}-request-listener-${info.id}`,
|
||||||
|
channel: info.channel,
|
||||||
|
handler: info.getHandler(di),
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: requestChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export interface RequestFromChannel {
|
||||||
|
<Request, Response>(
|
||||||
|
channel: RequestChannel<Request, Response>,
|
||||||
|
request: Request
|
||||||
|
): Promise<Response>;
|
||||||
|
<Response>(channel: RequestChannel<void, Response>): Promise<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestFromChannelInjectionToken =
|
||||||
|
getInjectionToken<RequestFromChannel>({
|
||||||
|
id: "request-from-request-channel",
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
|
||||||
|
import { getFeature } from "@k8slens/feature-core";
|
||||||
|
import { messagingFeature } from "../actual/feature";
|
||||||
|
|
||||||
|
export const messagingFeatureForUnitTesting = getFeature({
|
||||||
|
id: "messaging-for-unit-testing",
|
||||||
|
|
||||||
|
dependencies: [messagingFeature],
|
||||||
|
|
||||||
|
register: (di) => {
|
||||||
|
autoRegister({
|
||||||
|
di,
|
||||||
|
targetModule: module,
|
||||||
|
|
||||||
|
getRequireContexts: () => [
|
||||||
|
require.context("./", true, /\.injectable\.(ts|tsx)$/),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,430 @@
|
|||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
DiContainer,
|
||||||
|
Injectable,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core/src/register-feature";
|
||||||
|
import {
|
||||||
|
getMessageChannelListenerInjectable,
|
||||||
|
MessageChannel,
|
||||||
|
} from "../../actual/message/message-channel-listener-injection-token";
|
||||||
|
import { sendMessageToChannelInjectionToken } from "../../actual/message/message-to-channel-injection-token.no-coverage";
|
||||||
|
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
|
import { runInAction } from "mobx";
|
||||||
|
import {
|
||||||
|
getRequestChannelListenerInjectable,
|
||||||
|
RequestChannel,
|
||||||
|
} from "../../actual/request/request-channel-listener-injection-token";
|
||||||
|
import { requestFromChannelInjectionToken } from "../../actual/request/request-from-channel-injection-token.no-coverage";
|
||||||
|
import { getPromiseStatus } from "@k8slens/test-utils";
|
||||||
|
import { getMessageBridgeFake } from "./get-message-bridge-fake";
|
||||||
|
import { getMessageChannel } from "../../actual/message/get-message-channel";
|
||||||
|
import { getRequestChannel } from "../../actual/request/get-request-channel";
|
||||||
|
import { startApplicationInjectionToken } from "@k8slens/application";
|
||||||
|
import { messagingFeatureForUnitTesting } from "../feature";
|
||||||
|
|
||||||
|
[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach(
|
||||||
|
({ scenarioIsAsync }) =>
|
||||||
|
describe(`get-message-bridge-fake, given running as ${
|
||||||
|
scenarioIsAsync ? "async" : "sync"
|
||||||
|
}`, () => {
|
||||||
|
let messageBridgeFake: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
messageBridgeFake = getMessageBridgeFake();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given multiple DIs are involved", () => {
|
||||||
|
let someDi1: DiContainer;
|
||||||
|
let someDi2: DiContainer;
|
||||||
|
let someDiWithoutListeners: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
someDi1 = createContainer("some-di-1");
|
||||||
|
someDi2 = createContainer("some-di-2");
|
||||||
|
|
||||||
|
someDiWithoutListeners = createContainer("some-di-3");
|
||||||
|
|
||||||
|
registerMobX(someDi1);
|
||||||
|
registerMobX(someDi2);
|
||||||
|
registerMobX(someDiWithoutListeners);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
registerFeature(someDi1, messagingFeatureForUnitTesting);
|
||||||
|
registerFeature(someDi2, messagingFeatureForUnitTesting);
|
||||||
|
registerFeature(
|
||||||
|
someDiWithoutListeners,
|
||||||
|
messagingFeatureForUnitTesting
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
messageBridgeFake.involve(someDi1, someDi2, someDiWithoutListeners);
|
||||||
|
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
messageBridgeFake.setAsync(scenarioIsAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
someDi1.inject(startApplicationInjectionToken)(),
|
||||||
|
someDi2.inject(startApplicationInjectionToken)(),
|
||||||
|
someDiWithoutListeners.inject(startApplicationInjectionToken)(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given there are message listeners", () => {
|
||||||
|
let someHandler1MockInDi1: jest.Mock;
|
||||||
|
let someHandler1MockInDi2: jest.Mock;
|
||||||
|
let someHandler2MockInDi2: jest.Mock;
|
||||||
|
let someListener1InDi2: Injectable<unknown, unknown>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
someHandler1MockInDi1 = jest.fn();
|
||||||
|
someHandler1MockInDi2 = jest.fn();
|
||||||
|
someHandler2MockInDi2 = jest.fn();
|
||||||
|
|
||||||
|
const someListener1InDi1 = getMessageChannelListenerInjectable({
|
||||||
|
id: "some-listener-in-di-1",
|
||||||
|
channel: someMessageChannel,
|
||||||
|
getHandler: () => someHandler1MockInDi1,
|
||||||
|
});
|
||||||
|
|
||||||
|
someListener1InDi2 = getMessageChannelListenerInjectable({
|
||||||
|
id: "some-listener-in-di-2",
|
||||||
|
channel: someMessageChannel,
|
||||||
|
getHandler: () => someHandler1MockInDi2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const someListener2InDi2 = getMessageChannelListenerInjectable({
|
||||||
|
id: "some-listener-2-in-di-2",
|
||||||
|
channel: someMessageChannel,
|
||||||
|
getHandler: () => someHandler2MockInDi2,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
someDi1.register(someListener1InDi1);
|
||||||
|
someDi2.register(someListener1InDi2);
|
||||||
|
someDi2.register(someListener2InDi2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given there is a listener in di-2 that responds to a message with a message", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const someResponder = getMessageChannelListenerInjectable({
|
||||||
|
id: "some-responder-di-2",
|
||||||
|
channel: someMessageChannel,
|
||||||
|
|
||||||
|
getHandler: (di) => {
|
||||||
|
const sendMessage = di.inject(
|
||||||
|
sendMessageToChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
return (message) => {
|
||||||
|
sendMessage(
|
||||||
|
someMessageChannel,
|
||||||
|
`some-response-to: ${message}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
someDi2.register(someResponder);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given a message is sent in di-1", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const sendMessageToChannelFromDi1 = someDi1.inject(
|
||||||
|
sendMessageToChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
sendMessageToChannelFromDi1(someMessageChannel, "some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when all message steps are propagated using a wrapper"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
let someWrapper: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
someWrapper = jest.fn((propagation) => propagation());
|
||||||
|
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
messageBridgeFake
|
||||||
|
.messagePropagationRecursive(someWrapper)
|
||||||
|
.then(done);
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the response gets handled in di-1", () => {
|
||||||
|
expect(someHandler1MockInDi1).toHaveBeenCalledWith(
|
||||||
|
"some-response-to: some-message"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
scenarioIsAsync &&
|
||||||
|
it("the wrapper gets called with the both propagations", () => {
|
||||||
|
expect(someWrapper).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync
|
||||||
|
? "when all message steps are propagated not using a wrapper"
|
||||||
|
: "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
messageBridgeFake
|
||||||
|
.messagePropagationRecursive()
|
||||||
|
.then(done);
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the response gets handled in di-1", () => {
|
||||||
|
expect(someHandler1MockInDi1).toHaveBeenCalledWith(
|
||||||
|
"some-response-to: some-message"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when sending message in a DI", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const sendMessageToChannelFromDi1 = someDi1.inject(
|
||||||
|
sendMessageToChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
sendMessageToChannelFromDi1(someMessageChannel, "some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listener in sending DI does not handle the message", () => {
|
||||||
|
expect(someHandler1MockInDi1).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
scenarioIsAsync &&
|
||||||
|
it("listeners in other than sending DIs do not handle the message yet", () => {
|
||||||
|
expect(someHandler1MockInDi2).not.toHaveBeenCalled();
|
||||||
|
expect(someHandler2MockInDi2).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(
|
||||||
|
scenarioIsAsync ? "when messages are propagated" : "immediately",
|
||||||
|
() => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
if (scenarioIsAsync) {
|
||||||
|
messageBridgeFake.messagePropagation().then(done);
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listeners in other than sending DIs handle the message", () => {
|
||||||
|
expect(someHandler1MockInDi2).toHaveBeenCalledWith(
|
||||||
|
"some-message"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(someHandler2MockInDi2).toHaveBeenCalledWith(
|
||||||
|
"some-message"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
scenarioIsAsync &&
|
||||||
|
describe("when messages are propagated using a wrapper, such as act() in react testing lib", () => {
|
||||||
|
let someWrapper: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
someWrapper = jest.fn((observation) => observation());
|
||||||
|
|
||||||
|
await messageBridgeFake.messagePropagation(someWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the wrapper gets called with the related propagation", async () => {
|
||||||
|
expect(someWrapper).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listeners still handle the message", () => {
|
||||||
|
expect(someHandler1MockInDi2).toHaveBeenCalledWith(
|
||||||
|
"some-message"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(someHandler2MockInDi2).toHaveBeenCalledWith(
|
||||||
|
"some-message"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given a listener is deregistered, when sending message, deregistered listener does not handle the message", () => {
|
||||||
|
runInAction(() => {
|
||||||
|
someDi2.deregister(someListener1InDi2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMessageToChannelFromDi1 = someDi1.inject(
|
||||||
|
sendMessageToChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
someHandler1MockInDi2.mockClear();
|
||||||
|
|
||||||
|
sendMessageToChannelFromDi1(someMessageChannel, "irrelevant");
|
||||||
|
|
||||||
|
expect(someHandler1MockInDi2).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given there are request listeners", () => {
|
||||||
|
let someHandler1MockInDi1: AsyncFnMock<
|
||||||
|
(message: string) => Promise<number>
|
||||||
|
>;
|
||||||
|
|
||||||
|
let someHandler1MockInDi2: AsyncFnMock<
|
||||||
|
(message: string) => Promise<number>
|
||||||
|
>;
|
||||||
|
|
||||||
|
let someListener1InDi2: Injectable<unknown, unknown>;
|
||||||
|
let actualPromise: Promise<number>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
someHandler1MockInDi1 = asyncFn();
|
||||||
|
someHandler1MockInDi2 = asyncFn();
|
||||||
|
|
||||||
|
const someListener1InDi1 = getRequestChannelListenerInjectable({
|
||||||
|
id: "some-request-listener-in-di-1",
|
||||||
|
channel: someOtherRequestChannel,
|
||||||
|
getHandler: () => someHandler1MockInDi1,
|
||||||
|
});
|
||||||
|
|
||||||
|
someListener1InDi2 = getRequestChannelListenerInjectable({
|
||||||
|
id: "some-request-listener-in-di-2",
|
||||||
|
channel: someRequestChannel,
|
||||||
|
getHandler: () => someHandler1MockInDi2,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
someDi1.register(someListener1InDi1);
|
||||||
|
someDi2.register(someListener1InDi2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when requesting from a channel in a DI", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const requestFromChannelFromDi1 = someDi1.inject(
|
||||||
|
requestFromChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
actualPromise = requestFromChannelFromDi1(
|
||||||
|
someRequestChannel,
|
||||||
|
"some-request"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listener in requesting DI does not handle the request", () => {
|
||||||
|
expect(someHandler1MockInDi1).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the listener in other than requesting DIs handle the request", () => {
|
||||||
|
expect(someHandler1MockInDi2).toHaveBeenCalledWith(
|
||||||
|
"some-request"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when handle resolves, resolves with response", async () => {
|
||||||
|
await someHandler1MockInDi2.resolve(42);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given a listener is deregistered, when requesting, deregistered listener does not handle the request", () => {
|
||||||
|
runInAction(() => {
|
||||||
|
someDi2.deregister(someListener1InDi2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMessageToChannelFromDi1 = someDi1.inject(
|
||||||
|
sendMessageToChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
someHandler1MockInDi2.mockClear();
|
||||||
|
|
||||||
|
sendMessageToChannelFromDi1(someMessageChannel, "irrelevant");
|
||||||
|
|
||||||
|
expect(someHandler1MockInDi2).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given there are multiple listeners between different DIs for same channel, when requesting, throws", () => {
|
||||||
|
const someConflictingListenerInjectable =
|
||||||
|
getRequestChannelListenerInjectable({
|
||||||
|
id: "conflicting-listener",
|
||||||
|
channel: someRequestChannel,
|
||||||
|
getHandler: () => () => 84,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
someDi1.register(someConflictingListenerInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestFromChannelFromDi2 = someDi2.inject(
|
||||||
|
requestFromChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
return expect(() =>
|
||||||
|
requestFromChannelFromDi2(someRequestChannel, "irrelevant")
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Tried to make a request but multiple listeners were discovered for channel "some-request-channel" in multiple DIs.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when requesting from channel without listener, throws", () => {
|
||||||
|
const requestFromChannel = someDi1.inject(
|
||||||
|
requestFromChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
return expect(() =>
|
||||||
|
requestFromChannel(
|
||||||
|
someRequestChannelWithoutListeners,
|
||||||
|
"irrelevant"
|
||||||
|
)
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Tried to make a request but no listeners for channel "some-request-channel-without-listeners" was discovered in any DIs'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
type SomeMessageChannel = MessageChannel<string>;
|
||||||
|
type SomeRequestChannel = RequestChannel<string, number>;
|
||||||
|
|
||||||
|
const someMessageChannel: SomeMessageChannel = getMessageChannel(
|
||||||
|
"some-message-channel"
|
||||||
|
);
|
||||||
|
const someRequestChannel: SomeRequestChannel = getRequestChannel(
|
||||||
|
"some-request-channel"
|
||||||
|
);
|
||||||
|
const someOtherRequestChannel: SomeRequestChannel = {
|
||||||
|
id: "some-other-request-channel",
|
||||||
|
};
|
||||||
|
const someRequestChannelWithoutListeners: SomeRequestChannel = {
|
||||||
|
id: "some-request-channel-without-listeners",
|
||||||
|
};
|
||||||
@ -0,0 +1,210 @@
|
|||||||
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import type { Channel } from "../../actual/channel.no-coverage";
|
||||||
|
import type { MessageChannelHandler } from "../../actual/message/message-channel-listener-injection-token";
|
||||||
|
import type { RequestChannelHandler } from "../../actual/request/request-channel-listener-injection-token";
|
||||||
|
import { sendMessageToChannelInjectionToken } from "../../actual/message/message-to-channel-injection-token.no-coverage";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "../../actual/message/enlist-message-channel-listener-injection-token";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { filter, map } from "lodash/fp";
|
||||||
|
import {
|
||||||
|
RequestFromChannel,
|
||||||
|
requestFromChannelInjectionToken,
|
||||||
|
} from "../../actual/request/request-from-channel-injection-token.no-coverage";
|
||||||
|
import { enlistRequestChannelListenerInjectionToken } from "../../actual/request/enlist-request-channel-listener-injection-token";
|
||||||
|
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
|
||||||
|
export type MessageBridgeFake = {
|
||||||
|
involve: (...dis: DiContainer[]) => void;
|
||||||
|
messagePropagation: () => Promise<void>;
|
||||||
|
messagePropagationRecursive: (callback: any) => any;
|
||||||
|
setAsync: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessageBridgeFake = (): MessageBridgeFake => {
|
||||||
|
const messageListenersByDi = new Map<
|
||||||
|
DiContainer,
|
||||||
|
Map<string, Set<MessageChannelHandler<Channel>>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
const requestListenersByDi = new Map<
|
||||||
|
DiContainer,
|
||||||
|
Map<string, Set<RequestChannelHandler<Channel>>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
const messagePropagationBuffer = new Set<AsyncFnMock<() => void>>();
|
||||||
|
|
||||||
|
const messagePropagation = async (
|
||||||
|
wrapper: (callback: any) => any = (callback) => callback()
|
||||||
|
) => {
|
||||||
|
const oldMessages = [...messagePropagationBuffer.values()];
|
||||||
|
messagePropagationBuffer.clear();
|
||||||
|
await Promise.all(oldMessages.map((x) => wrapper(x.resolve)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const messagePropagationRecursive = async (
|
||||||
|
wrapper: (callback: any) => any = (callback) => callback()
|
||||||
|
) => {
|
||||||
|
while (messagePropagationBuffer.size) {
|
||||||
|
await messagePropagation(wrapper);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let asyncModeStatus = false;
|
||||||
|
const getAsyncModeStatus = () => asyncModeStatus;
|
||||||
|
|
||||||
|
return {
|
||||||
|
involve: (...dis: DiContainer[]) => {
|
||||||
|
dis.forEach((di) => {
|
||||||
|
overrideRequesting({ di, requestListenersByDi });
|
||||||
|
|
||||||
|
overrideMessaging({
|
||||||
|
di,
|
||||||
|
messageListenersByDi,
|
||||||
|
messagePropagationBuffer,
|
||||||
|
getAsyncModeStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
messagePropagation,
|
||||||
|
|
||||||
|
messagePropagationRecursive,
|
||||||
|
|
||||||
|
setAsync: (value) => {
|
||||||
|
asyncModeStatus = value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const overrideMessaging = ({
|
||||||
|
di,
|
||||||
|
messageListenersByDi,
|
||||||
|
messagePropagationBuffer,
|
||||||
|
getAsyncModeStatus,
|
||||||
|
}: {
|
||||||
|
di: DiContainer;
|
||||||
|
|
||||||
|
messageListenersByDi: Map<
|
||||||
|
DiContainer,
|
||||||
|
Map<string, Set<MessageChannelHandler<Channel>>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
messagePropagationBuffer: Set<{ resolve: () => Promise<void> }>;
|
||||||
|
|
||||||
|
getAsyncModeStatus: () => boolean;
|
||||||
|
}) => {
|
||||||
|
const messageHandlersByChannel = new Map<
|
||||||
|
string,
|
||||||
|
Set<MessageChannelHandler<Channel>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
messageListenersByDi.set(di, messageHandlersByChannel);
|
||||||
|
|
||||||
|
di.override(sendMessageToChannelInjectionToken, () => (channel, message) => {
|
||||||
|
const allOtherDis = [...messageListenersByDi.keys()].filter(
|
||||||
|
(x) => x !== di
|
||||||
|
);
|
||||||
|
|
||||||
|
allOtherDis.forEach((otherDi) => {
|
||||||
|
const listeners = messageListenersByDi.get(otherDi);
|
||||||
|
|
||||||
|
const handlersForChannel = listeners!.get(channel.id);
|
||||||
|
|
||||||
|
if (!handlersForChannel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getAsyncModeStatus()) {
|
||||||
|
const resolvableHandlePromise = asyncFn();
|
||||||
|
|
||||||
|
resolvableHandlePromise().then(() => {
|
||||||
|
handlersForChannel.forEach((handler) => handler(message));
|
||||||
|
});
|
||||||
|
|
||||||
|
messagePropagationBuffer.add(resolvableHandlePromise);
|
||||||
|
} else {
|
||||||
|
handlersForChannel.forEach((handler) => handler(message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
di.override(enlistMessageChannelListenerInjectionToken, () => (listener) => {
|
||||||
|
if (!messageHandlersByChannel.has(listener.channel.id)) {
|
||||||
|
messageHandlersByChannel.set(listener.channel.id, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerSet = messageHandlersByChannel.get(listener.channel.id);
|
||||||
|
|
||||||
|
handlerSet!.add(listener.handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
handlerSet!.delete(listener.handler);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const overrideRequesting = ({
|
||||||
|
di,
|
||||||
|
requestListenersByDi,
|
||||||
|
}: {
|
||||||
|
di: DiContainer;
|
||||||
|
|
||||||
|
requestListenersByDi: Map<
|
||||||
|
DiContainer,
|
||||||
|
Map<string, Set<MessageChannelHandler<Channel>>>
|
||||||
|
>;
|
||||||
|
}) => {
|
||||||
|
const requestHandlersByChannel = new Map<
|
||||||
|
string,
|
||||||
|
Set<RequestChannelHandler<Channel>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
requestListenersByDi.set(di, requestHandlersByChannel);
|
||||||
|
|
||||||
|
di.override(
|
||||||
|
requestFromChannelInjectionToken,
|
||||||
|
() =>
|
||||||
|
(async (channel, request) =>
|
||||||
|
await pipeline(
|
||||||
|
[...requestListenersByDi.values()],
|
||||||
|
map((listenersByChannel) => listenersByChannel!.get(channel.id)),
|
||||||
|
filter((x) => !!x),
|
||||||
|
|
||||||
|
(channelSpecificListeners) => {
|
||||||
|
if (channelSpecificListeners.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to make a request but no listeners for channel "${channel.id}" was discovered in any DIs`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelSpecificListeners.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to make a request but multiple listeners were discovered for channel "${channel.id}" in multiple DIs.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners = channelSpecificListeners[0];
|
||||||
|
|
||||||
|
const [handler] = listeners!;
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
},
|
||||||
|
|
||||||
|
async (handler) => await handler(request)
|
||||||
|
)) as RequestFromChannel
|
||||||
|
);
|
||||||
|
|
||||||
|
di.override(enlistRequestChannelListenerInjectionToken, () => (listener) => {
|
||||||
|
if (!requestHandlersByChannel.has(listener.channel.id)) {
|
||||||
|
requestHandlersByChannel.set(listener.channel.id, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerSet = requestHandlersByChannel.get(listener.channel.id);
|
||||||
|
|
||||||
|
handlerSet!.add(listener.handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
handlerSet!.delete(listener.handler);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export { messagingFeatureForUnitTesting } from './feature';
|
||||||
|
export { getMessageBridgeFake } from "./get-message-bridge-fake/get-message-bridge-fake";
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { sendMessageToChannelInjectionToken } from "../actual/message/message-to-channel-injection-token.no-coverage";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "../actual/message/enlist-message-channel-listener-injection-token";
|
||||||
|
import { requestFromChannelInjectionToken } from "../actual/request/request-from-channel-injection-token.no-coverage";
|
||||||
|
import { enlistRequestChannelListenerInjectionToken } from "../actual/request/enlist-request-channel-listener-injection-token";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
export const sendMessageToChannelStubInjectable = getInjectable({
|
||||||
|
id: "send-message-to-channel-stub",
|
||||||
|
instantiate: () => () => {},
|
||||||
|
injectionToken: sendMessageToChannelInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const enlistMessageChannelListenerStubInjectable = getInjectable({
|
||||||
|
id: "enlist-message-channel-listener-stub",
|
||||||
|
instantiate: () => () => () => {},
|
||||||
|
injectionToken: enlistMessageChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const requestFromChannelStubInjectable = getInjectable({
|
||||||
|
id: "request-from-channel-stub",
|
||||||
|
instantiate: () => () => Promise.resolve(),
|
||||||
|
injectionToken: requestFromChannelInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const enlistRequestChannelListenerStubInjectable = getInjectable({
|
||||||
|
id: "enlist-request-channel-listener-stub",
|
||||||
|
instantiate: () => () => () => {},
|
||||||
|
injectionToken: enlistRequestChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
DiContainer,
|
||||||
|
getInjectable,
|
||||||
|
Injectable,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
|
import { runInAction } from "mobx";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EnlistMessageChannelListener,
|
||||||
|
enlistMessageChannelListenerInjectionToken,
|
||||||
|
} from "./features/actual/message/enlist-message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
import { messagingFeature } from "./features/actual/feature";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getMessageChannelListenerInjectable,
|
||||||
|
MessageChannel,
|
||||||
|
MessageChannelListener,
|
||||||
|
} from "./features/actual/message/message-channel-listener-injection-token";
|
||||||
|
|
||||||
|
import { listeningOfChannelsInjectionToken } from "./features/actual/listening-of-channels/listening-of-channels.injectable";
|
||||||
|
import { enlistRequestChannelListenerInjectionToken } from "./features/actual/request/enlist-request-channel-listener-injection-token";
|
||||||
|
import { sendMessageToChannelInjectionToken } from "./features/actual/message/message-to-channel-injection-token.no-coverage";
|
||||||
|
import { getMessageChannel } from "./features/actual/message/get-message-channel";
|
||||||
|
|
||||||
|
describe("listening-of-messages", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let enlistMessageChannelListenerMock: jest.MockedFunction<EnlistMessageChannelListener>;
|
||||||
|
let disposeSomeListenerMock: jest.Mock;
|
||||||
|
let disposeSomeUnrelatedListenerMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerMobX(di);
|
||||||
|
|
||||||
|
disposeSomeListenerMock = jest.fn();
|
||||||
|
disposeSomeUnrelatedListenerMock = jest.fn();
|
||||||
|
|
||||||
|
enlistMessageChannelListenerMock = jest.fn((listener) =>
|
||||||
|
listener.id === "some-listener"
|
||||||
|
? disposeSomeListenerMock
|
||||||
|
: disposeSomeUnrelatedListenerMock
|
||||||
|
);
|
||||||
|
|
||||||
|
const someEnlistMessageChannelListenerInjectable = getInjectable({
|
||||||
|
id: "some-enlist-message-channel-listener",
|
||||||
|
instantiate: () => enlistMessageChannelListenerMock,
|
||||||
|
injectionToken: enlistMessageChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const someEnlistRequestChannelListenerInjectable = getInjectable({
|
||||||
|
id: "some-enlist-request-channel-listener",
|
||||||
|
instantiate: () => () => () => {},
|
||||||
|
injectionToken: enlistRequestChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMessageToChannelDummyInjectable = getInjectable({
|
||||||
|
id: "send-message-to-channel-dummy",
|
||||||
|
instantiate: () => () => {},
|
||||||
|
injectionToken: sendMessageToChannelInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(
|
||||||
|
someEnlistMessageChannelListenerInjectable,
|
||||||
|
someEnlistRequestChannelListenerInjectable,
|
||||||
|
sendMessageToChannelDummyInjectable
|
||||||
|
);
|
||||||
|
|
||||||
|
registerFeature(di, messagingFeature);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given listening of channels has not started yet", () => {
|
||||||
|
describe("when a new listener gets registered", () => {
|
||||||
|
let someChannel: MessageChannel<string>;
|
||||||
|
let someMessageHandler: () => void;
|
||||||
|
|
||||||
|
let someListenerInjectable: Injectable<
|
||||||
|
MessageChannelListener<MessageChannel<string>>,
|
||||||
|
MessageChannelListener<MessageChannel<unknown>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
someChannel = getMessageChannel("some-channel-id");
|
||||||
|
|
||||||
|
someMessageHandler = () => {};
|
||||||
|
|
||||||
|
someListenerInjectable = getMessageChannelListenerInjectable({
|
||||||
|
id: "some-listener",
|
||||||
|
channel: someChannel,
|
||||||
|
getHandler: () => someMessageHandler,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(someListenerInjectable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Todo: make starting automatic by using a runnable with a timeslot.
|
||||||
|
describe("when listening of channels is started", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const listeningOnMessageChannels = di.inject(
|
||||||
|
listeningOfChannelsInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
listeningOnMessageChannels.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("it enlists a listener for the channel", () => {
|
||||||
|
expect(enlistMessageChannelListenerMock).toHaveBeenCalledWith({
|
||||||
|
id: "some-listener",
|
||||||
|
channel: someChannel,
|
||||||
|
handler: someMessageHandler,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when another listener gets registered", () => {
|
||||||
|
let someOtherListenerInjectable: Injectable<
|
||||||
|
MessageChannelListener<MessageChannel<string>>,
|
||||||
|
MessageChannelListener<MessageChannel<unknown>>,
|
||||||
|
void
|
||||||
|
>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const handler = () => someMessageHandler;
|
||||||
|
|
||||||
|
someOtherListenerInjectable = getMessageChannelListenerInjectable({
|
||||||
|
id: "some-other-listener",
|
||||||
|
channel: someChannel,
|
||||||
|
getHandler: handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
enlistMessageChannelListenerMock.mockClear();
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(someOtherListenerInjectable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only enlists it as well", () => {
|
||||||
|
expect(enlistMessageChannelListenerMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "some-other-listener",
|
||||||
|
channel: someChannel,
|
||||||
|
handler: someMessageHandler,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when one of the listeners gets deregistered", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
di.deregister(someListenerInjectable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the listener gets disposed", () => {
|
||||||
|
expect(disposeSomeListenerMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the unrelated listener does not get disposed", () => {
|
||||||
|
expect(disposeSomeUnrelatedListenerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when listening of channels stops", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const listening = di.inject(listeningOfChannelsInjectionToken);
|
||||||
|
|
||||||
|
listening.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remaining listeners get disposed", () => {
|
||||||
|
expect(disposeSomeUnrelatedListenerMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when yet another listener gets registered, does not enlist it", () => {
|
||||||
|
enlistMessageChannelListenerMock.mockClear();
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(someListenerInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enlistMessageChannelListenerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,236 @@
|
|||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
DiContainer,
|
||||||
|
getInjectable,
|
||||||
|
Injectable,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
|
import { _resetGlobalState, configure, runInAction } from "mobx";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EnlistRequestChannelListener,
|
||||||
|
enlistRequestChannelListenerInjectionToken,
|
||||||
|
} from "./features/actual/request/enlist-request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
import { messagingFeature } from "./features/actual/feature";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRequestChannelListenerInjectable,
|
||||||
|
RequestChannel,
|
||||||
|
RequestChannelListener,
|
||||||
|
} from "./features/actual/request/request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
import { listeningOfChannelsInjectionToken } from "./features/actual/listening-of-channels/listening-of-channels.injectable";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "./features/actual/message/enlist-message-channel-listener-injection-token";
|
||||||
|
import { noop } from "lodash/fp";
|
||||||
|
import { sendMessageToChannelInjectionToken } from "./features/actual/message/message-to-channel-injection-token.no-coverage";
|
||||||
|
import { getRequestChannel } from "./features/actual/request/get-request-channel";
|
||||||
|
|
||||||
|
describe("listening-of-requests", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let enlistRequestChannelListenerMock: jest.MockedFunction<EnlistRequestChannelListener>;
|
||||||
|
let disposeSomeListenerMock: jest.Mock;
|
||||||
|
let disposeSomeUnrelatedListenerMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configure({
|
||||||
|
disableErrorBoundaries: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
_resetGlobalState();
|
||||||
|
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerMobX(di);
|
||||||
|
|
||||||
|
disposeSomeListenerMock = jest.fn();
|
||||||
|
disposeSomeUnrelatedListenerMock = jest.fn();
|
||||||
|
|
||||||
|
enlistRequestChannelListenerMock = jest.fn((listener) =>
|
||||||
|
listener.id === "some-listener"
|
||||||
|
? disposeSomeListenerMock
|
||||||
|
: disposeSomeUnrelatedListenerMock
|
||||||
|
);
|
||||||
|
|
||||||
|
const someEnlistMessageChannelListenerInjectable = getInjectable({
|
||||||
|
id: "some-enlist-message-channel-listener",
|
||||||
|
instantiate: () => () => () => {},
|
||||||
|
injectionToken: enlistMessageChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const someEnlistRequestChannelListenerInjectable = getInjectable({
|
||||||
|
id: "some-enlist-request-channel-listener",
|
||||||
|
instantiate: () => enlistRequestChannelListenerMock,
|
||||||
|
injectionToken: enlistRequestChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMessageToChannelDummyInjectable = getInjectable({
|
||||||
|
id: "send-message-to-channel-dummy",
|
||||||
|
instantiate: () => () => {},
|
||||||
|
injectionToken: sendMessageToChannelInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(
|
||||||
|
someEnlistMessageChannelListenerInjectable,
|
||||||
|
someEnlistRequestChannelListenerInjectable,
|
||||||
|
sendMessageToChannelDummyInjectable
|
||||||
|
);
|
||||||
|
|
||||||
|
registerFeature(di, messagingFeature);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given listening of channels has not started yet", () => {
|
||||||
|
describe("when a new listener gets registered", () => {
|
||||||
|
let someChannel: RequestChannel<string, string>;
|
||||||
|
let someOtherChannel: RequestChannel<string, string>;
|
||||||
|
let someRequestHandler: () => string;
|
||||||
|
|
||||||
|
let someListenerInjectable: Injectable<
|
||||||
|
RequestChannelListener<RequestChannel<string, string>>,
|
||||||
|
RequestChannelListener<RequestChannel<unknown, unknown>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
someChannel = getRequestChannel("some-channel-id");
|
||||||
|
someOtherChannel = getRequestChannel("some-other-channel-id");
|
||||||
|
|
||||||
|
someRequestHandler = () => "some-response";
|
||||||
|
|
||||||
|
someListenerInjectable = getRequestChannelListenerInjectable({
|
||||||
|
id: "some-listener",
|
||||||
|
channel: someChannel,
|
||||||
|
getHandler: () => someRequestHandler,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(someListenerInjectable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Todo: make starting automatic by using a runnable with a timeslot.
|
||||||
|
describe("when listening of channels is started", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const listeningOnRequestChannels = di.inject(
|
||||||
|
listeningOfChannelsInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
listeningOnRequestChannels.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("it enlists a listener for the channel", () => {
|
||||||
|
expect(enlistRequestChannelListenerMock).toHaveBeenCalledWith({
|
||||||
|
id: "some-listener",
|
||||||
|
channel: someChannel,
|
||||||
|
handler: someRequestHandler,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when another listener for same channel gets registered, throws", () => {
|
||||||
|
const originalConsoleWarn = console.warn;
|
||||||
|
|
||||||
|
console.warn = noop;
|
||||||
|
|
||||||
|
configure({
|
||||||
|
disableErrorBoundaries: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn = originalConsoleWarn;
|
||||||
|
|
||||||
|
const handler = () => someRequestHandler;
|
||||||
|
|
||||||
|
const someConflictingListenerInjectable =
|
||||||
|
getRequestChannelListenerInjectable({
|
||||||
|
id: "some-other-listener",
|
||||||
|
channel: someChannel,
|
||||||
|
getHandler: handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(someConflictingListenerInjectable);
|
||||||
|
});
|
||||||
|
}).toThrow(
|
||||||
|
'Tried to add listener for channel "some-channel-id" but listener already exists.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when another listener gets registered", () => {
|
||||||
|
let someOtherListenerInjectable: Injectable<
|
||||||
|
RequestChannelListener<RequestChannel<string, string>>,
|
||||||
|
RequestChannelListener<RequestChannel<unknown, unknown>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const handler = () => someRequestHandler;
|
||||||
|
|
||||||
|
someOtherListenerInjectable = getRequestChannelListenerInjectable({
|
||||||
|
id: "some-other-listener",
|
||||||
|
channel: someOtherChannel,
|
||||||
|
getHandler: handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
enlistRequestChannelListenerMock.mockClear();
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(someOtherListenerInjectable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only enlists it as well", () => {
|
||||||
|
expect(enlistRequestChannelListenerMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "some-other-listener",
|
||||||
|
channel: someOtherChannel,
|
||||||
|
handler: someRequestHandler,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when one of the listeners gets deregistered", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
di.deregister(someListenerInjectable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the listener gets disposed", () => {
|
||||||
|
expect(disposeSomeListenerMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the unrelated listener does not get disposed", () => {
|
||||||
|
expect(disposeSomeUnrelatedListenerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when listening of channels stops", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const listening = di.inject(listeningOfChannelsInjectionToken);
|
||||||
|
|
||||||
|
listening.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remaining listeners get disposed", () => {
|
||||||
|
expect(disposeSomeUnrelatedListenerMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when yet another listener gets registered, does not enlist it", () => {
|
||||||
|
enlistRequestChannelListenerMock.mockClear();
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
di.register(someListenerInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enlistRequestChannelListenerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@k8slens/typescript/config/base.json"
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@k8slens/webpack").configForNode;
|
||||||
3
packages/technical-features/messaging/main/index.ts
Normal file
3
packages/technical-features/messaging/main/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { feature } from "./src/feature";
|
||||||
|
|
||||||
|
export const messagingFeatureForMain = feature;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
module.exports =
|
||||||
|
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;
|
||||||
38
packages/technical-features/messaging/main/package.json
Normal file
38
packages/technical-features/messaging/main/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "@k8slens/messaging-for-main",
|
||||||
|
"private": false,
|
||||||
|
"version": "6.5.0-alpha.0",
|
||||||
|
"description": "Implementations for 'messaging' in Electron main",
|
||||||
|
"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",
|
||||||
|
"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": "^6.5.0-alpha.0",
|
||||||
|
"@ogre-tools/injectable": "^15.1.2",
|
||||||
|
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
|
||||||
|
"electron": "^19.1.8",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { IpcMainEvent } from "electron";
|
||||||
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
|
||||||
|
|
||||||
|
const enlistMessageChannelListenerInjectable = getInjectable({
|
||||||
|
id: "enlist-message-channel-listener",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const ipcMain = di.inject(ipcMainInjectable);
|
||||||
|
|
||||||
|
return ({ channel, handler }) => {
|
||||||
|
const nativeOnCallback = (_: IpcMainEvent, message: unknown) => {
|
||||||
|
handler(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcMain.on(channel.id, nativeOnCallback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcMain.off(channel.id, nativeOnCallback);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: enlistMessageChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enlistMessageChannelListenerInjectable;
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
|
import type { IpcMain, IpcMainEvent } from "electron";
|
||||||
|
import {
|
||||||
|
EnlistMessageChannelListener,
|
||||||
|
enlistMessageChannelListenerInjectionToken,
|
||||||
|
} from "@k8slens/messaging";
|
||||||
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
|
||||||
|
describe("enlist message channel listener in main", () => {
|
||||||
|
let enlistMessageChannelListener: EnlistMessageChannelListener;
|
||||||
|
let ipcMainStub: IpcMain;
|
||||||
|
let onMock: jest.Mock;
|
||||||
|
let offMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
|
||||||
|
onMock = jest.fn();
|
||||||
|
offMock = jest.fn();
|
||||||
|
|
||||||
|
ipcMainStub = {
|
||||||
|
on: onMock,
|
||||||
|
off: offMock,
|
||||||
|
} as unknown as IpcMain;
|
||||||
|
|
||||||
|
di.override(ipcMainInjectable, () => ipcMainStub);
|
||||||
|
|
||||||
|
enlistMessageChannelListener = di.inject(
|
||||||
|
enlistMessageChannelListenerInjectionToken
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let handlerMock: jest.Mock;
|
||||||
|
let disposer: () => void;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handlerMock = jest.fn();
|
||||||
|
|
||||||
|
disposer = enlistMessageChannelListener({
|
||||||
|
id: "some-listener",
|
||||||
|
channel: { id: "some-channel-id" },
|
||||||
|
handler: handlerMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call handler yet", () => {
|
||||||
|
expect(handlerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the listener", () => {
|
||||||
|
expect(onMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not de-register the listener yet", () => {
|
||||||
|
expect(offMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when message arrives", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, "some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the handler with the message", () => {
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith("some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disposing the listener, de-registers the listener", () => {
|
||||||
|
disposer();
|
||||||
|
|
||||||
|
expect(offMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, 42);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, true);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcMainEvent, { some: "object" });
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { IpcMainInvokeEvent } from "electron";
|
||||||
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
|
import type {
|
||||||
|
RequestChannel,
|
||||||
|
RequestChannelListener,
|
||||||
|
} from "@k8slens/messaging";
|
||||||
|
import { enlistRequestChannelListenerInjectionToken } from "@k8slens/messaging";
|
||||||
|
|
||||||
|
export type EnlistRequestChannelListener = <
|
||||||
|
TChannel extends RequestChannel<unknown, unknown>
|
||||||
|
>(
|
||||||
|
listener: RequestChannelListener<TChannel>
|
||||||
|
) => () => void;
|
||||||
|
|
||||||
|
const enlistRequestChannelListenerInjectable = getInjectable({
|
||||||
|
id: "enlist-request-channel-listener-for-main",
|
||||||
|
|
||||||
|
instantiate: (di): EnlistRequestChannelListener => {
|
||||||
|
const ipcMain = di.inject(ipcMainInjectable);
|
||||||
|
|
||||||
|
return ({ channel, handler }) => {
|
||||||
|
const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) =>
|
||||||
|
handler(request);
|
||||||
|
|
||||||
|
ipcMain.handle(channel.id, nativeHandleCallback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcMain.off(channel.id, nativeHandleCallback);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: enlistRequestChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enlistRequestChannelListenerInjectable;
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
|
||||||
|
import type { IpcMain, IpcMainInvokeEvent } from "electron";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
import type { EnlistRequestChannelListener } from "./enlist-request-channel-listener.injectable";
|
||||||
|
import enlistRequestChannelListenerInjectable from "./enlist-request-channel-listener.injectable";
|
||||||
|
import type { RequestChannel, RequestChannelHandler } from "@k8slens/messaging";
|
||||||
|
import { getPromiseStatus } from "@k8slens/test-utils";
|
||||||
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
|
||||||
|
type TestRequestChannel = RequestChannel<unknown, unknown>;
|
||||||
|
|
||||||
|
const testRequestChannel: TestRequestChannel = {
|
||||||
|
id: "some-channel-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("enlist request channel listener in main", () => {
|
||||||
|
let enlistRequestChannelListener: EnlistRequestChannelListener;
|
||||||
|
let ipcMainStub: IpcMain;
|
||||||
|
let handleMock: jest.Mock;
|
||||||
|
let offMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
|
||||||
|
handleMock = jest.fn();
|
||||||
|
offMock = jest.fn();
|
||||||
|
|
||||||
|
ipcMainStub = {
|
||||||
|
handle: handleMock,
|
||||||
|
off: offMock,
|
||||||
|
} as unknown as IpcMain;
|
||||||
|
|
||||||
|
di.override(ipcMainInjectable, () => ipcMainStub);
|
||||||
|
|
||||||
|
enlistRequestChannelListener = di.inject(
|
||||||
|
enlistRequestChannelListenerInjectable
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let handlerMock: AsyncFnMock<RequestChannelHandler<TestRequestChannel>>;
|
||||||
|
let disposer: () => void;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handlerMock = asyncFn();
|
||||||
|
|
||||||
|
disposer = enlistRequestChannelListener({
|
||||||
|
id: "some-listener",
|
||||||
|
channel: testRequestChannel,
|
||||||
|
handler: handlerMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call handler yet", () => {
|
||||||
|
expect(handlerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the listener", () => {
|
||||||
|
expect(handleMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not de-register the listener yet", () => {
|
||||||
|
expect(offMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when request arrives", () => {
|
||||||
|
let actualPromise: Promise<any>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actualPromise = handleMock.mock.calls[0][1](
|
||||||
|
{} as IpcMainInvokeEvent,
|
||||||
|
"some-request"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the handler with the request", () => {
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith("some-request");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when handler resolves with response, listener resolves with the response", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await handlerMock.resolve("some-response");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves with the response", async () => {
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe("some-response");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disposing the listener, de-registers the listener", () => {
|
||||||
|
disposer();
|
||||||
|
|
||||||
|
expect(offMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as response, when handler resolves with response, listener resolves with stringified response", async () => {
|
||||||
|
await handlerMock.resolve(42);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as response, when handler resolves with response, listener resolves with stringified response", async () => {
|
||||||
|
await handlerMock.resolve(true);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as response, when handler resolves with response, listener resolves with response", async () => {
|
||||||
|
await handlerMock.resolve({ some: "object" });
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as request, when request arrives, calls the handler with the request", () => {
|
||||||
|
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, 42);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as request, when request arrives, calls the handler with the request", () => {
|
||||||
|
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, true);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as request, when request arrives, calls the handler with the request", () => {
|
||||||
|
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, { some: "object" });
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
packages/technical-features/messaging/main/src/feature.ts
Normal file
17
packages/technical-features/messaging/main/src/feature.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
|
||||||
|
import { getFeature } from "@k8slens/feature-core";
|
||||||
|
|
||||||
|
export const feature = getFeature({
|
||||||
|
id: "messaging-for-main",
|
||||||
|
|
||||||
|
register: (di) => {
|
||||||
|
autoRegister({
|
||||||
|
di,
|
||||||
|
targetModule: module,
|
||||||
|
|
||||||
|
getRequireContexts: () => [
|
||||||
|
require.context("./", true, /\.injectable\.(ts|tsx)$/),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
|
const ipcMainInjectable = getInjectable({
|
||||||
|
id: "ipc-main",
|
||||||
|
instantiate: () => ipcMain,
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ipcMainInjectable;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { createContainer, DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import ipcMainInjectable from "./ipc-main.injectable";
|
||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
|
||||||
|
describe("ipc-main", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is the actual IPC-main of Electron", () => {
|
||||||
|
const actual = di.inject(ipcMainInjectable);
|
||||||
|
|
||||||
|
expect(actual).toBe(ipcMain);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { createContainer, getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { feature } from "./feature";
|
||||||
|
import { listeningOfChannelsInjectionToken } from "@k8slens/messaging";
|
||||||
|
import { getStartableStoppable } from "@k8slens/startable-stoppable";
|
||||||
|
import { runManyFor } from "@k8slens/run-many";
|
||||||
|
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
|
||||||
|
|
||||||
|
describe("listening-of-channels", () => {
|
||||||
|
it("when on application load, starts listening of channels", async () => {
|
||||||
|
const di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
|
||||||
|
const listeningOfChannelsMock = jest.fn(() => () => {});
|
||||||
|
|
||||||
|
const listeningOfChannelsInjectableStub = getInjectable({
|
||||||
|
id: "some-runnable",
|
||||||
|
|
||||||
|
instantiate: () =>
|
||||||
|
getStartableStoppable("some-listening-of-channels-implementation", () =>
|
||||||
|
listeningOfChannelsMock()
|
||||||
|
),
|
||||||
|
|
||||||
|
injectionToken: listeningOfChannelsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
di.register(listeningOfChannelsInjectableStub);
|
||||||
|
|
||||||
|
const onLoadOfApplication = runManyFor(di)(
|
||||||
|
onLoadOfApplicationInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
await onLoadOfApplication();
|
||||||
|
|
||||||
|
expect(listeningOfChannelsMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
3
packages/technical-features/messaging/main/tsconfig.json
Normal file
3
packages/technical-features/messaging/main/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@k8slens/typescript/config/base.json"
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@k8slens/webpack").configForNode;
|
||||||
3
packages/technical-features/messaging/renderer/index.ts
Normal file
3
packages/technical-features/messaging/renderer/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { feature } from "./src/feature";
|
||||||
|
|
||||||
|
export const messagingFeatureForRenderer = feature;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
module.exports =
|
||||||
|
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;
|
||||||
39
packages/technical-features/messaging/renderer/package.json
Normal file
39
packages/technical-features/messaging/renderer/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@k8slens/messaging-for-renderer",
|
||||||
|
"private": false,
|
||||||
|
"version": "6.5.0-alpha.0",
|
||||||
|
"description": "Implementations for 'messaging' in Electron renderer",
|
||||||
|
"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",
|
||||||
|
"lint": "lens-lint",
|
||||||
|
"lint:fix": "lens-lint --fix"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@k8slens/application": "^6.5.0-alpha.0",
|
||||||
|
"@k8slens/messaging": "^6.5.0-alpha.0",
|
||||||
|
"@k8slens/run-many": "^1.0.0",
|
||||||
|
"@k8slens/startable-stoppable": "^6.5.0-alpha.0",
|
||||||
|
"@ogre-tools/injectable": "^15.1.2",
|
||||||
|
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
|
||||||
|
"electron": "^19.1.8",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
|
||||||
|
import { getFeature } from "@k8slens/feature-core";
|
||||||
|
|
||||||
|
export const feature = getFeature({
|
||||||
|
id: "messaging-for-renderer",
|
||||||
|
|
||||||
|
register: (di) => {
|
||||||
|
autoRegister({
|
||||||
|
di,
|
||||||
|
targetModule: module,
|
||||||
|
|
||||||
|
getRequireContexts: () => [
|
||||||
|
require.context("./", true, /\.injectable\.(ts|tsx)$/),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
const ipcRendererInjectable = getInjectable({
|
||||||
|
id: "ipc-renderer",
|
||||||
|
instantiate: () => ipcRenderer,
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ipcRendererInjectable;
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { createContainer, DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
describe("ipc-renderer", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is not undefined", () => {
|
||||||
|
const actual = di.inject(ipcRendererInjectable);
|
||||||
|
|
||||||
|
expect(actual).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is IPC-renderer of Electron", () => {
|
||||||
|
const actual = di.inject(ipcRendererInjectable);
|
||||||
|
|
||||||
|
expect(actual).toBe(ipcRenderer);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import ipcRendererInjectable from "../ipc/ipc-renderer.injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { IpcRendererEvent } from "electron";
|
||||||
|
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
|
||||||
|
|
||||||
|
const enlistMessageChannelListenerInjectable = getInjectable({
|
||||||
|
id: "enlist-message-channel-listener-for-renderer",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const ipcRenderer = di.inject(ipcRendererInjectable);
|
||||||
|
|
||||||
|
return ({ channel, handler }) => {
|
||||||
|
const nativeCallback = (_: IpcRendererEvent, message: unknown) => {
|
||||||
|
handler(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcRenderer.on(channel.id, nativeCallback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.off(channel.id, nativeCallback);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: enlistMessageChannelListenerInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enlistMessageChannelListenerInjectable;
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
import type { IpcRendererEvent, IpcRenderer } from "electron";
|
||||||
|
import ipcRendererInjectable from "../ipc/ipc-renderer.injectable";
|
||||||
|
import {
|
||||||
|
EnlistMessageChannelListener,
|
||||||
|
enlistMessageChannelListenerInjectionToken,
|
||||||
|
} from "@k8slens/messaging";
|
||||||
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
|
||||||
|
describe("enlist message channel listener in renderer", () => {
|
||||||
|
let enlistMessageChannelListener: EnlistMessageChannelListener;
|
||||||
|
let ipcRendererStub: IpcRenderer;
|
||||||
|
let onMock: jest.Mock;
|
||||||
|
let offMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
|
||||||
|
onMock = jest.fn();
|
||||||
|
offMock = jest.fn();
|
||||||
|
|
||||||
|
ipcRendererStub = {
|
||||||
|
on: onMock,
|
||||||
|
off: offMock,
|
||||||
|
} as unknown as IpcRenderer;
|
||||||
|
|
||||||
|
di.override(ipcRendererInjectable, () => ipcRendererStub);
|
||||||
|
|
||||||
|
enlistMessageChannelListener = di.inject(
|
||||||
|
enlistMessageChannelListenerInjectionToken
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let handlerMock: jest.Mock;
|
||||||
|
let disposer: () => void;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handlerMock = jest.fn();
|
||||||
|
|
||||||
|
disposer = enlistMessageChannelListener({
|
||||||
|
id: "some-listener",
|
||||||
|
channel: { id: "some-channel-id" },
|
||||||
|
handler: handlerMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call handler yet", () => {
|
||||||
|
expect(handlerMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the listener", () => {
|
||||||
|
expect(onMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not de-register the listener yet", () => {
|
||||||
|
expect(offMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when message arrives", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, "some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the handler with the message", () => {
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith("some-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disposing the listener, de-registers the listener", () => {
|
||||||
|
disposer();
|
||||||
|
|
||||||
|
expect(offMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given number as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, 42);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given boolean as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, true);
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given object as message, when message arrives, calls the handler with the message", () => {
|
||||||
|
onMock.mock.calls[0][1]({} as IpcRendererEvent, { some: "object" });
|
||||||
|
|
||||||
|
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { createContainer, getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
import { listeningOfChannelsInjectionToken } from "@k8slens/messaging";
|
||||||
|
import { getStartableStoppable } from "@k8slens/startable-stoppable";
|
||||||
|
import { runManyFor } from "@k8slens/run-many";
|
||||||
|
import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
|
||||||
|
|
||||||
|
describe("listening-of-channels", () => {
|
||||||
|
it("when before frame starts, starts listening of channels", async () => {
|
||||||
|
const di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
|
||||||
|
const listeningOfChannelsMock = jest.fn(() => () => {});
|
||||||
|
|
||||||
|
const listeningOfChannelsInjectableStub = getInjectable({
|
||||||
|
id: "some-runnable",
|
||||||
|
|
||||||
|
instantiate: () =>
|
||||||
|
getStartableStoppable("some-listening-of-channels-implementation", () =>
|
||||||
|
listeningOfChannelsMock()
|
||||||
|
),
|
||||||
|
|
||||||
|
injectionToken: listeningOfChannelsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
di.register(listeningOfChannelsInjectableStub);
|
||||||
|
|
||||||
|
const onLoadOfApplication = runManyFor(di)(onLoadOfApplicationInjectionToken);
|
||||||
|
|
||||||
|
await onLoadOfApplication();
|
||||||
|
|
||||||
|
expect(listeningOfChannelsMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import ipcRendererInjectable from "../ipc/ipc-renderer.injectable";
|
||||||
|
|
||||||
|
const invokeIpcInjectable = getInjectable({
|
||||||
|
id: "invoke-ipc",
|
||||||
|
|
||||||
|
instantiate: (di) => di.inject(ipcRendererInjectable).invoke,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default invokeIpcInjectable;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { createContainer, DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import invokeIpcInjectable from "./invoke-ipc.injectable";
|
||||||
|
|
||||||
|
describe("ipc-renderer", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is IPC-renderer invoke of Electron", () => {
|
||||||
|
const actual = di.inject(invokeIpcInjectable);
|
||||||
|
|
||||||
|
expect(actual).toBe(ipcRenderer.invoke);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { RequestFromChannel } from "@k8slens/messaging";
|
||||||
|
import { requestFromChannelInjectionToken } from "@k8slens/messaging";
|
||||||
|
import invokeIpcInjectable from "./invoke-ipc.injectable";
|
||||||
|
|
||||||
|
const requestFromChannelInjectable = getInjectable({
|
||||||
|
id: "request-from-channel",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const invokeIpc = di.inject(invokeIpcInjectable);
|
||||||
|
|
||||||
|
return ((channel, request) =>
|
||||||
|
invokeIpc(channel.id, request)) as RequestFromChannel;
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: requestFromChannelInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default requestFromChannelInjectable;
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { createContainer, DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { requestFromChannelInjectionToken } from "@k8slens/messaging";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
import type { RequestChannel } from "@k8slens/messaging";
|
||||||
|
import invokeIpcInjectable from "./invoke-ipc.injectable";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
|
||||||
|
describe("request-from-channel", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let invokeIpcMock: AsyncFnMock<() => Promise<number>>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
|
||||||
|
invokeIpcMock = asyncFn();
|
||||||
|
di.override(invokeIpcInjectable, () => invokeIpcMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let actualPromise: Promise<number>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
|
||||||
|
|
||||||
|
const someChannel: RequestChannel<string, number> = {
|
||||||
|
id: "some-channel-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
actualPromise = requestFromChannel(someChannel, "some-request-payload");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes ipcRenderer of Electron", () => {
|
||||||
|
expect(invokeIpcMock).toHaveBeenCalledWith(
|
||||||
|
"some-channel-id",
|
||||||
|
"some-request-payload"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when invoke resolves with response, resolves with said response", async () => {
|
||||||
|
await invokeIpcMock.resolve(42);
|
||||||
|
|
||||||
|
expect(await actualPromise).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import sendToIpcInjectable from "./send-to-ipc.injectable";
|
||||||
|
import {
|
||||||
|
SendMessageToChannel,
|
||||||
|
sendMessageToChannelInjectionToken,
|
||||||
|
} from "@k8slens/messaging";
|
||||||
|
|
||||||
|
const messageToChannelInjectable = getInjectable({
|
||||||
|
id: "message-to-channel",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const sendToIpc = di.inject(sendToIpcInjectable);
|
||||||
|
|
||||||
|
return ((channel, message) => {
|
||||||
|
sendToIpc(channel.id, message);
|
||||||
|
}) as SendMessageToChannel;
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionToken: sendMessageToChannelInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messageToChannelInjectable;
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { createContainer, DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
import type { MessageChannel } from "@k8slens/messaging";
|
||||||
|
import sendToIpcInjectable from "./send-to-ipc.injectable";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
|
||||||
|
describe("message-from-channel", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
let sendToIpcMock: AsyncFnMock<() => Promise<number>>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
|
||||||
|
sendToIpcMock = asyncFn();
|
||||||
|
di.override(sendToIpcInjectable, () => sendToIpcMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const sendMessageToChannel = di.inject(
|
||||||
|
sendMessageToChannelInjectionToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const someChannel: MessageChannel<number> = {
|
||||||
|
id: "some-channel-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
sendMessageToChannel(someChannel, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends to ipcRenderer of Electron", () => {
|
||||||
|
expect(sendToIpcMock).toHaveBeenCalledWith("some-channel-id", 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import ipcRendererInjectable from "../ipc/ipc-renderer.injectable";
|
||||||
|
|
||||||
|
const sendToIpcInjectable = getInjectable({
|
||||||
|
id: "send-to-ipc",
|
||||||
|
|
||||||
|
instantiate: (di) => di.inject(ipcRendererInjectable).send,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default sendToIpcInjectable;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { createContainer, DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
|
import { feature } from "../feature";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import sendToIpcInjectable from "./send-to-ipc.injectable";
|
||||||
|
|
||||||
|
describe("ipc-renderer", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
|
||||||
|
registerFeature(di, feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is IPC-renderer send of Electron", () => {
|
||||||
|
const actual = di.inject(sendToIpcInjectable);
|
||||||
|
|
||||||
|
expect(actual).toBe(ipcRenderer.send);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@k8slens/typescript/config/base.json"
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@k8slens/webpack").configForNode;
|
||||||
@ -25,7 +25,9 @@
|
|||||||
"test": "jest --coverage --runInBand"
|
"test": "jest --coverage --runInBand"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"mobx": "^6.8.0",
|
"mobx": "^6.8.0"
|
||||||
"type-fest": "^2.19.0"
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"type-fest": "^2.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user