Switch to using messaging and startable stoppable from NPM package (#7368)

* Add custom jest resolver to fix requiring "uuid" module

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update dependencies

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce test utils for rendering and running with thrown mobx reactions

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract startable-stoppable to NPM package

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract messaging to NPM package

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using startable-stoppable from NPM package

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using messaging from the Feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove old implementation of messaging

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make setupping app paths happen earlier in renderer

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix typo

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add kludge to make testing-library work properly from test-utils package

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix code style

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add lint:fix -root script

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix unrelated failing unit tests

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Turn of no-floating-promises from typescript linting for being broken

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make linting not happen for dist -directories

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make linting failures appear as failure

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Stop running prettier twice

It already gets ran as eslint-plugin.

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make CI run unit tests for all packages by consolidating name of NPM script

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing unit tests for coverage

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Skip coverage for test utils

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove check for coverage in packages which are not ready for it

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Stop collecting coverage from index.ts files them being indirections to the implementation

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement sending message to channel in main

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing feature dependencies

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add dummy implementations for requesting in main from renderer

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Re-enable communicating from main to cluster frames

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Ignore trivial files from coverage

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update package-lock

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract message-bridge to separate NPM package to prevent dev dependencies being in the production bundle

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract computed channel to own NPM package for clear dependencies

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate electron related stuff to a directory

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing publish configurations

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Ignore test implementation from coverage being not interesting

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

---------

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne SavolainenandMikko Aspiala authored and GitHub committed 2023-03-21 11:38:43 +02:00
1 parent df49ff9b96
commit 0f1f030a06
261 files changed
+4447 -1795

No files matched your search

@@ -0,0 +1,397 @@
import { createContainer, DiContainer, Injectable } from "@ogre-tools/injectable";
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
import { registerFeature } from "@k8slens/feature-core";
import {
getMessageChannel,
getMessageChannelListenerInjectable,
getRequestChannel,
getRequestChannelListenerInjectable,
MessageChannel,
testUtils,
RequestChannel,
requestFromChannelInjectionToken,
sendMessageToChannelInjectionToken,
} from "@k8slens/messaging";
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import { runInAction } from "mobx";
import { getPromiseStatus } from "@k8slens/test-utils";
import { getMessageBridgeFake } from "./get-message-bridge-fake";
import { startApplicationInjectionToken } from "@k8slens/application";
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",
};
[{ 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(() => {
const feature = testUtils.messagingFeatureForUnitTesting;
registerFeature(someDi1, feature);
registerFeature(someDi2, feature);
registerFeature(someDiWithoutListeners, feature);
});
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");
});
const scenarioTitle = scenarioIsAsync
? "when all message steps are propagated using a wrapper"
: "immediately";
// eslint-disable-next-line jest/valid-title
describe(scenarioTitle, () => {
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",
{
frameId: 42,
processId: 42,
},
);
});
scenarioIsAsync &&
it("the wrapper gets called with the both propagations", () => {
expect(someWrapper).toHaveBeenCalledTimes(2);
});
});
const scenarioName: string = scenarioIsAsync
? "when all message steps are propagated not using a wrapper"
: "immediately";
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
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",
{
frameId: 42,
processId: 42,
},
);
});
});
});
});
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();
});
const scenarioName = scenarioIsAsync ? "when messages are propagated" : "immediately";
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
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", {
frameId: 42,
processId: 42,
});
expect(someHandler2MockInDi2).toHaveBeenCalledWith("some-message", {
frameId: 42,
processId: 42,
});
});
});
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", {
frameId: 42,
processId: 42,
});
expect(someHandler2MockInDi2).toHaveBeenCalledWith("some-message", {
frameId: 42,
processId: 42,
});
});
});
});
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',
);
});
});
});
}),
);
@@ -0,0 +1,196 @@
import type { DiContainer } from "@ogre-tools/injectable";
import type { Channel, MessageChannelHandler, RequestChannelHandler } from "@k8slens/messaging";
import {
enlistMessageChannelListenerInjectionToken,
enlistRequestChannelListenerInjectionToken,
RequestFromChannel,
requestFromChannelInjectionToken,
sendMessageToChannelInjectionToken,
} from "@k8slens/messaging";
import { pipeline } from "@ogre-tools/fp";
import { filter, map } from "lodash/fp";
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;
};
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, { frameId: 42, processId: 42 }));
});
messagePropagationBuffer.add(resolvableHandlePromise);
} else {
handlersForChannel.forEach((handler) => handler(message, { frameId: 42, processId: 42 }));
}
});
});
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<RequestChannelHandler<Channel>>>>;
}) => {
const requestHandlersByChannel = new Map<string, Set<RequestChannelHandler<Channel>>>();
requestListenersByDi.set(di, requestHandlersByChannel);
di.override(
requestFromChannelInjectionToken,
() =>
(async (channel, request) =>
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];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [handler] = listeners!;
return handler;
},
async (handler) => 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);
};
});
};
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;
},
};
};