1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2023-03-17 09:33:30 +02:00
parent c9f16beb54
commit 88023e1f70
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
31 changed files with 1070 additions and 1271 deletions

View File

@ -1,2 +1 @@
module.exports = module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;

View File

@ -20,19 +20,16 @@ import { filter, groupBy, map, nth, toPairs } from "lodash/fp";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import type { JsonPrimitive } from "type-fest"; import type { JsonPrimitive } from "type-fest";
export type JsonifiableObject = export type JsonifiableObject = { [Key in string]?: Jsonifiable } | { toJSON: () => Jsonifiable };
| { [Key in string]?: Jsonifiable }
| { toJSON: () => Jsonifiable };
export type JsonifiableArray = readonly Jsonifiable[]; export type JsonifiableArray = readonly Jsonifiable[];
export type Jsonifiable = JsonPrimitive | JsonifiableObject | JsonifiableArray; export type Jsonifiable = JsonPrimitive | JsonifiableObject | JsonifiableArray;
export type ComputedChannelFactory = <T>( export type ComputedChannelFactory = <T>(
channel: MessageChannel<T>, channel: MessageChannel<T>,
pendingValue: T pendingValue: T,
) => IComputedValue<T>; ) => IComputedValue<T>;
export const computedChannelInjectionToken = export const computedChannelInjectionToken = getInjectionToken<ComputedChannelFactory>({
getInjectionToken<ComputedChannelFactory>({
id: "computed-channel-injection-token", id: "computed-channel-injection-token",
}); });
@ -51,6 +48,10 @@ export const computedChannelObserverInjectionToken = getInjectionToken<
id: "computed-channel-observer", id: "computed-channel-observer",
}); });
export const computedChannelAdministrationChannel: MessageChannel<ComputedChannelAdminMessage> = {
id: "computed-channel-administration-channel",
};
const computedChannelInjectable = getInjectable({ const computedChannelInjectable = getInjectable({
id: "computed-channel", id: "computed-channel",
@ -67,7 +68,7 @@ const computedChannelInjectable = getInjectable({
if (!contextIsReactive) { if (!contextIsReactive) {
throw new Error( 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.` `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.`,
); );
} }
@ -134,17 +135,17 @@ export const duplicateChannelObserverGuardInjectable = getInjectable({
groupBy((observer) => observer.channel.id), groupBy((observer) => observer.channel.id),
toPairs, toPairs,
filter(([, channelObservers]) => channelObservers.length > 1), filter(([, channelObservers]) => channelObservers.length > 1),
map(nth(0)) map(nth(0)),
); );
if (duplicateObserverChannelIds.length) { if (duplicateObserverChannelIds.length) {
throw new Error( throw new Error(
`Tried to register duplicate channel observer for channels "${duplicateObserverChannelIds.join( `Tried to register duplicate channel observer for channels "${duplicateObserverChannelIds.join(
'", "' '", "',
)}"` )}"`,
); );
} }
} },
); );
}, },
}; };
@ -153,18 +154,10 @@ export const duplicateChannelObserverGuardInjectable = getInjectable({
injectionToken: onLoadOfApplicationInjectionToken, injectionToken: onLoadOfApplicationInjectionToken,
}); });
export const computedChannelAdministrationChannel: MessageChannel<ComputedChannelAdminMessage> = export const computedChannelAdministrationListenerInjectable = getMessageChannelListenerInjectable({
{
id: "computed-channel-administration-channel",
};
export const computedChannelAdministrationListenerInjectable =
getMessageChannelListenerInjectable({
id: "computed-channel-administration", id: "computed-channel-administration",
getHandler: (di) => { getHandler: (di) => {
const sendMessageToChannel = di.inject( const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
sendMessageToChannelInjectionToken
);
const disposersByChannelId = new Map<string, () => void>(); const disposersByChannelId = new Map<string, () => void>();
@ -172,10 +165,7 @@ export const computedChannelAdministrationListenerInjectable =
if (message.status === "became-observed") { if (message.status === "became-observed") {
const result = di const result = di
.injectMany(computedChannelObserverInjectionToken) .injectMany(computedChannelObserverInjectionToken)
.find( .find((channelObserver) => channelObserver.channel.id === message.channelId);
(channelObserver) =>
channelObserver.channel.id === message.channelId
);
if (result === undefined) { if (result === undefined) {
return; return;
@ -189,18 +179,18 @@ export const computedChannelAdministrationListenerInjectable =
id: message.channelId, id: message.channelId,
}, },
observed observed,
), ),
{ {
fireImmediately: true, fireImmediately: true,
} },
); );
disposersByChannelId.set(message.channelId, disposer); disposersByChannelId.set(message.channelId, disposer);
} else { } else {
const disposer = disposersByChannelId.get(message.channelId); const disposer = disposersByChannelId.get(message.channelId);
disposer!(); disposer?.();
} }
}; };
}, },

View File

@ -1,10 +1,6 @@
import React from "react"; import React from "react";
import { act } from "@testing-library/react"; import { act } from "@testing-library/react";
import { import { createContainer, DiContainer, getInjectable } from "@ogre-tools/injectable";
createContainer,
DiContainer,
getInjectable,
} from "@ogre-tools/injectable";
import { import {
getMessageBridgeFake, getMessageBridgeFake,
MessageBridgeFake, MessageBridgeFake,
@ -35,8 +31,11 @@ import { observer } from "mobx-react";
const testChannel: MessageChannel<string> = { id: "some-channel-id" }; const testChannel: MessageChannel<string> = { id: "some-channel-id" };
const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" }; const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach( const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue<string> }) => (
({ scenarioIsAsync }) => <div>{someComputed.get()}</div>
));
[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach(({ scenarioIsAsync }) =>
describe(`computed-channel, given running message bridge fake as ${ describe(`computed-channel, given running message bridge fake as ${
scenarioIsAsync ? "async" : "sync" scenarioIsAsync ? "async" : "sync"
}`, () => { }`, () => {
@ -56,8 +55,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
registerMobX(di1); registerMobX(di1);
registerMobX(di2); registerMobX(di2);
const administrationChannelTestListenerInjectable = const administrationChannelTestListenerInjectable = getMessageChannelListenerInjectable({
getMessageChannelListenerInjectable({
id: "administration-channel-test-listener", id: "administration-channel-test-listener",
channel: computedChannelAdministrationChannel, channel: computedChannelAdministrationChannel,
@ -66,8 +64,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
}, },
}); });
const channelValueTestListenerInjectable = const channelValueTestListenerInjectable = getMessageChannelListenerInjectable({
getMessageChannelListenerInjectable({
id: "test-channel-value-listener", id: "test-channel-value-listener",
channel: testChannel, channel: testChannel,
@ -118,10 +115,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
const computedChannel = di1.inject(computedChannelInjectionToken); const computedChannel = di1.inject(computedChannelInjectionToken);
computedTestChannel = computedChannel( computedTestChannel = computedChannel(testChannel, "some-pending-value");
testChannel,
"some-pending-value"
);
}); });
it("there is no admin message yet", () => { it("there is no admin message yet", () => {
@ -134,33 +128,25 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
beforeEach(() => { beforeEach(() => {
const render = renderFor(di2); const render = renderFor(di2);
rendered = render( rendered = render(<TestComponent someComputed={computedTestChannel} />);
<TestComponent someComputed={computedTestChannel} />
);
}); });
describe( const scenarioName = scenarioIsAsync ? "when all messages are propagated" : "immediately";
scenarioIsAsync
? "when all messages are propagated" // eslint-disable-next-line jest/valid-title
: "immediately", describe(scenarioName, () => {
() => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake messageBridgeFake.messagePropagationRecursive(act).then(done);
.messagePropagationRecursive(act)
.then(done);
} else { } else {
done(); done();
} }
}); });
it("renders", () => { it("renders", () => {
expect(rendered.container).toHaveTextContent( expect(rendered.container).toHaveTextContent("some-initial-value");
"some-initial-value" });
);
}); });
}
);
}); });
describe("when observing the computed channel in di-1", () => { describe("when observing the computed channel in di-1", () => {
@ -178,7 +164,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
{ {
fireImmediately: true, fireImmediately: true,
} },
); );
}); });
@ -187,14 +173,15 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
expect(observedValue).toBe("some-pending-value"); expect(observedValue).toBe("some-pending-value");
}); });
describe( const scenarioName = scenarioIsAsync
scenarioIsAsync
? "when admin messages are propagated" ? "when admin messages are propagated"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
} else { } else {
done(); done();
} }
@ -207,14 +194,15 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
}); });
}); });
describe( const scenarioName = scenarioIsAsync
scenarioIsAsync
? "when returning value-messages propagate" ? "when returning value-messages propagate"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
} else { } else {
done(); done();
} }
@ -237,14 +225,15 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
}); });
}); });
describe( const scenarioName = scenarioIsAsync
scenarioIsAsync
? "when value-messages propagate" ? "when value-messages propagate"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
} else { } else {
done(); done();
} }
@ -257,8 +246,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
it("the new value gets listened in di-1", () => { it("the new value gets listened in di-1", () => {
expect(latestValueMessage).toBe("some-new-value"); expect(latestValueMessage).toBe("some-new-value");
}); });
} });
);
}); });
describe("when stopping observation for the channel in di-1", () => { describe("when stopping observation for the channel in di-1", () => {
@ -268,14 +256,15 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
stopObserving(); stopObserving();
}); });
describe( const scenarioName = scenarioIsAsync
scenarioIsAsync
? "when admin-messages propagate" ? "when admin-messages propagate"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
} else { } else {
done(); done();
} }
@ -305,7 +294,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
expect(() => { expect(() => {
computedTestChannel.get(); computedTestChannel.get();
}).toThrow( }).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.' '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.',
); );
}); });
@ -325,29 +314,26 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
{ {
fireImmediately: true, fireImmediately: true,
} },
); );
}); });
scenarioIsAsync && scenarioIsAsync &&
it("computed test channel value is observed as the pending value again", () => { it("computed test channel value is observed as the pending value again", () => {
expect(observedValue).toBe( expect(observedValue).toBe("some-pending-value");
"some-pending-value"
);
}); });
describe( const scenarioName = scenarioIsAsync
scenarioIsAsync
? "when admin messages propagate" ? "when admin messages propagate"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
latestAdminMessage = undefined; latestAdminMessage = undefined;
messageBridgeFake void messageBridgeFake.messagePropagation().then(done);
.messagePropagation()
.then(done);
} else { } else {
done(); done();
} }
@ -362,53 +348,43 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
scenarioIsAsync && scenarioIsAsync &&
it("computed test channel value is still observed as the pending value", () => { it("computed test channel value is still observed as the pending value", () => {
expect(observedValue).toBe( expect(observedValue).toBe("some-pending-value");
"some-pending-value"
);
}); });
describe( const scenarioTitle = scenarioIsAsync
scenarioIsAsync
? "when value-messages propagate back" ? "when value-messages propagate back"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioTitle, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
latestValueMessage = undefined; latestValueMessage = undefined;
messageBridgeFake void messageBridgeFake.messagePropagation().then(done);
.messagePropagation()
.then(done);
} else { } else {
done(); done();
} }
}); });
it("the computed channel value changes", () => { it("the computed channel value changes", () => {
expect(observedValue).toBe( expect(observedValue).toBe("some-new-value-2");
"some-new-value-2"
);
}); });
it("the current value gets listened", () => { it("the current value gets listened", () => {
expect(latestValueMessage).toBe( expect(latestValueMessage).toBe("some-new-value-2");
"some-new-value-2" });
); });
}); });
} });
);
}
);
}); });
}); });
}
);
it("when accessing the computed value outside of reactive context, throws", () => { it("when accessing the computed value outside of reactive context, throws", () => {
expect(() => { expect(() => {
computedTestChannel.get(); computedTestChannel.get();
}).toThrow( }).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.' '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.',
); );
}); });
}); });
@ -431,14 +407,9 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
di2.register(channelObserver2Injectable); di2.register(channelObserver2Injectable);
}); });
const computedChannel = di1.inject( const computedChannel = di1.inject(computedChannelInjectionToken);
computedChannelInjectionToken
);
computedTestChannel = computedChannel( computedTestChannel = computedChannel(testChannel2, "some-pending-value");
testChannel2,
"some-pending-value"
);
reaction( reaction(
() => computedTestChannel.get(), () => computedTestChannel.get(),
@ -448,23 +419,20 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
{ {
fireImmediately: true, fireImmediately: true,
} },
); );
scenarioIsAsync && scenarioIsAsync && (await messageBridgeFake.messagePropagation());
(await messageBridgeFake.messagePropagation());
stopObserving(); stopObserving();
scenarioIsAsync && scenarioIsAsync && (await messageBridgeFake.messagePropagation());
(await messageBridgeFake.messagePropagation());
runInAction(() => { runInAction(() => {
someOtherObservable.set("some-value"); someOtherObservable.set("some-value");
}); });
scenarioIsAsync && scenarioIsAsync && (await messageBridgeFake.messagePropagation());
(await messageBridgeFake.messagePropagation());
expect(observedValue).toBe("some-value"); expect(observedValue).toBe("some-value");
}); });
@ -481,7 +449,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
{ {
fireImmediately: true, fireImmediately: true,
} },
); );
}); });
@ -512,14 +480,15 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
expect(nonReactiveValue).toBe("some-initial-value"); expect(nonReactiveValue).toBe("some-initial-value");
}); });
describe( const scenarioName = scenarioIsAsync
scenarioIsAsync
? "when messages would be propagated" ? "when messages would be propagated"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
} else { } else {
done(); done();
} }
@ -532,20 +501,17 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
it("does not send new admin message", () => { it("does not send new admin message", () => {
expect(latestAdminMessage).toBeUndefined(); expect(latestAdminMessage).toBeUndefined();
}); });
}
);
}); });
} });
); });
} });
);
}); });
it("when accessing the computed value outside of reactive context, throws", () => { it("when accessing the computed value outside of reactive context, throws", () => {
expect(() => { expect(() => {
computedTestChannel.get(); computedTestChannel.get();
}).toThrow( }).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.' '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.',
); );
}); });
@ -567,9 +533,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
di2.register(duplicateChannelObserverInjectable); di2.register(duplicateChannelObserverInjectable);
}); });
}); });
}).toThrow( }).toThrow('Tried to register duplicate channel observer for channels "some-channel-id"');
'Tried to register duplicate channel observer for channels "some-channel-id"'
);
}); });
}); });
@ -579,10 +543,7 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
beforeEach(() => { beforeEach(() => {
const computedChannel = di1.inject(computedChannelInjectionToken); const computedChannel = di1.inject(computedChannelInjectionToken);
computedTestChannel = computedChannel( computedTestChannel = computedChannel(testChannel, "some-pending-value");
testChannel,
"some-pending-value"
);
}); });
it("when the computed channel is observed, observes as undefined", () => { it("when the computed channel is observed, observes as undefined", () => {
@ -597,18 +558,12 @@ const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
{ {
fireImmediately: true, fireImmediately: true,
} },
); );
expect(observedValue).toBe("some-pending-value"); expect(observedValue).toBe("some-pending-value");
}); });
}); });
}); });
}) }),
);
const TestComponent = observer(
({ someComputed }: { someComputed: IComputedValue<string> }) => (
<div>{someComputed.get()}</div>
)
); );

View File

@ -12,9 +12,7 @@ export const messagingFeature = getFeature({
di, di,
targetModule: module, targetModule: module,
getRequireContexts: () => [ getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)],
require.context("./", true, /\.injectable\.(ts|tsx)$/),
],
}); });
}, },
}); });

View File

@ -1,10 +1,7 @@
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { enlistMessageChannelListenerInjectionToken } from "../message/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "../message/enlist-message-channel-listener-injection-token";
import { import { getStartableStoppable, StartableStoppable } from "@k8slens/startable-stoppable";
getStartableStoppable,
StartableStoppable,
} from "@k8slens/startable-stoppable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import { IComputedValue, reaction } from "mobx"; import { IComputedValue, reaction } from "mobx";
@ -15,62 +12,14 @@ import { enlistRequestChannelListenerInjectionToken } from "../request/enlist-re
import type { Channel } from "../channel.no-coverage"; import type { Channel } from "../channel.no-coverage";
export type ListeningOfChannels = StartableStoppable; export type ListeningOfChannels = StartableStoppable;
export const listeningOfChannelsInjectionToken = export const listeningOfChannelsInjectionToken = getInjectionToken<ListeningOfChannels>({
getInjectionToken<ListeningOfChannels>({
id: "listening-of-channels-injection-token", 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> }>( const listening = <T extends { id: string; channel: Channel<unknown> }>(
channelListeners: IComputedValue<T[]>, channelListeners: IComputedValue<T[]>,
enlistChannelListener: (listener: T) => () => void, enlistChannelListener: (listener: T) => () => void,
getId: (listener: T) => string getId: (listener: T) => string,
) => { ) => {
const listenerDisposers = new Map<string, () => void>(); const listenerDisposers = new Map<string, () => void>();
@ -78,11 +27,11 @@ const listening = <T extends { id: string; channel: Channel<unknown> }>(
() => channelListeners.get(), () => channelListeners.get(),
(newValues, oldValues = []) => { (newValues, oldValues = []) => {
const addedListeners = newValues.filter( const addedListeners = newValues.filter(
(newValue) => !oldValues.some((oldValue) => oldValue.id === newValue.id) (newValue) => !oldValues.some((oldValue) => oldValue.id === newValue.id),
); );
const removedListeners = oldValues.filter( const removedListeners = oldValues.filter(
(oldValue) => !newValues.some((newValue) => newValue.id === oldValue.id) (oldValue) => !newValues.some((newValue) => newValue.id === oldValue.id),
); );
addedListeners.forEach((listener) => { addedListeners.forEach((listener) => {
@ -90,7 +39,7 @@ const listening = <T extends { id: string; channel: Channel<unknown> }>(
if (listenerDisposers.has(id)) { if (listenerDisposers.has(id)) {
throw new Error( throw new Error(
`Tried to add listener for channel "${listener.channel.id}" but listener already exists.` `Tried to add listener for channel "${listener.channel.id}" but listener already exists.`,
); );
} }
@ -108,7 +57,7 @@ const listening = <T extends { id: string; channel: Channel<unknown> }>(
}); });
}, },
{ fireImmediately: true } { fireImmediately: true },
); );
return () => { return () => {
@ -116,3 +65,42 @@ const listening = <T extends { id: string; channel: Channel<unknown> }>(
listenerDisposers.forEach((dispose) => dispose()); listenerDisposers.forEach((dispose) => dispose());
}; };
}; };
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;

View File

@ -6,7 +6,7 @@ import type {
} from "./message-channel-listener-injection-token"; } from "./message-channel-listener-injection-token";
export type EnlistMessageChannelListener = ( export type EnlistMessageChannelListener = (
listener: MessageChannelListener<MessageChannel<unknown>> listener: MessageChannelListener<MessageChannel<unknown>>,
) => () => void; ) => () => void;
export const enlistMessageChannelListenerInjectionToken = export const enlistMessageChannelListenerInjectionToken =

View File

@ -1,7 +1,5 @@
import type { MessageChannel } from "./message-channel-listener-injection-token"; import type { MessageChannel } from "./message-channel-listener-injection-token";
export const getMessageChannel = <Request>( export const getMessageChannel = <Request>(id: string): MessageChannel<Request> => ({
id: string
): MessageChannel<Request> => ({
id, id,
}); });

View File

@ -6,9 +6,7 @@ export interface MessageChannel<Message> {
_messageSignature?: Message; _messageSignature?: Message;
} }
export type MessageChannelHandler<Channel> = Channel extends MessageChannel< export type MessageChannelHandler<Channel> = Channel extends MessageChannel<infer Message>
infer Message
>
? (message: Message) => void ? (message: Message) => void
: never; : never;
@ -24,10 +22,7 @@ export const messageChannelListenerInjectionToken = getInjectionToken<
id: "message-channel-listener", id: "message-channel-listener",
}); });
export interface GetMessageChannelListenerInfo< export interface GetMessageChannelListenerInfo<Channel extends MessageChannel<Message>, Message> {
Channel extends MessageChannel<Message>,
Message
> {
id: string; id: string;
channel: Channel; channel: Channel;
getHandler: (di: DiContainerForInjection) => MessageChannelHandler<Channel>; getHandler: (di: DiContainerForInjection) => MessageChannelHandler<Channel>;
@ -36,9 +31,9 @@ export interface GetMessageChannelListenerInfo<
export const getMessageChannelListenerInjectable = < export const getMessageChannelListenerInjectable = <
Channel extends MessageChannel<Message>, Channel extends MessageChannel<Message>,
Message Message,
>( >(
info: GetMessageChannelListenerInfo<Channel, Message> info: GetMessageChannelListenerInfo<Channel, Message>,
) => ) =>
getInjectable({ getInjectable({
id: `${info.channel.id}-message-listener-${info.id}`, id: `${info.channel.id}-message-listener-${info.id}`,

View File

@ -6,7 +6,6 @@ export interface SendMessageToChannel {
<Message>(channel: MessageChannel<Message>, message: Message): void; <Message>(channel: MessageChannel<Message>, message: Message): void;
} }
export const sendMessageToChannelInjectionToken = export const sendMessageToChannelInjectionToken = getInjectionToken<SendMessageToChannel>({
getInjectionToken<SendMessageToChannel>({
id: "send-message-to-message-channel", id: "send-message-to-message-channel",
}); });

View File

@ -6,7 +6,7 @@ import type {
} from "./request-channel-listener-injection-token"; } from "./request-channel-listener-injection-token";
export type EnlistRequestChannelListener = ( export type EnlistRequestChannelListener = (
listener: RequestChannelListener<RequestChannel<unknown, unknown>> listener: RequestChannelListener<RequestChannel<unknown, unknown>>,
) => () => void; ) => () => void;
export const enlistRequestChannelListenerInjectionToken = export const enlistRequestChannelListenerInjectionToken =

View File

@ -1,7 +1,7 @@
import type { RequestChannel } from "./request-channel-listener-injection-token"; import type { RequestChannel } from "./request-channel-listener-injection-token";
export const getRequestChannel = <Request, Response>( export const getRequestChannel = <Request, Response>(
id: string id: string,
): RequestChannel<Request, Response> => ({ ): RequestChannel<Request, Response> => ({
id, id,
}); });

View File

@ -29,7 +29,7 @@ export const requestChannelListenerInjectionToken = getInjectionToken<
export interface GetRequestChannelListenerInjectableInfo< export interface GetRequestChannelListenerInjectableInfo<
Channel extends RequestChannel<Request, Response>, Channel extends RequestChannel<Request, Response>,
Request, Request,
Response Response,
> { > {
id: string; id: string;
channel: Channel; channel: Channel;
@ -39,9 +39,9 @@ export interface GetRequestChannelListenerInjectableInfo<
export const getRequestChannelListenerInjectable = < export const getRequestChannelListenerInjectable = <
Channel extends RequestChannel<Request, Response>, Channel extends RequestChannel<Request, Response>,
Request, Request,
Response Response,
>( >(
info: GetRequestChannelListenerInjectableInfo<Channel, Request, Response> info: GetRequestChannelListenerInjectableInfo<Channel, Request, Response>,
) => ) =>
getInjectable({ getInjectable({
id: `${info.channel.id}-request-listener-${info.id}`, id: `${info.channel.id}-request-listener-${info.id}`,

View File

@ -4,7 +4,7 @@ import type { RequestChannel } from "./request-channel-listener-injection-token"
export interface RequestFromChannel { export interface RequestFromChannel {
<Request, Response>( <Request, Response>(
channel: RequestChannel<Request, Response>, channel: RequestChannel<Request, Response>,
request: Request request: Request,
): Promise<Response>; ): Promise<Response>;
<Response>(channel: RequestChannel<void, Response>): Promise<Response>; <Response>(channel: RequestChannel<void, Response>): Promise<Response>;
} }
@ -16,7 +16,6 @@ export type ChannelRequester<Channel> = Channel extends RequestChannel<
? (req: Request) => Promise<Awaited<Response>> ? (req: Request) => Promise<Awaited<Response>>
: never; : never;
export const requestFromChannelInjectionToken = export const requestFromChannelInjectionToken = getInjectionToken<RequestFromChannel>({
getInjectionToken<RequestFromChannel>({
id: "request-from-request-channel", id: "request-from-request-channel",
}); });

View File

@ -12,9 +12,7 @@ export const messagingFeatureForUnitTesting = getFeature({
di, di,
targetModule: module, targetModule: module,
getRequireContexts: () => [ getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)],
require.context("./", true, /\.injectable\.(ts|tsx)$/),
],
}); });
}, },
}); });

View File

@ -1,8 +1,4 @@
import { import { createContainer, DiContainer, Injectable } from "@ogre-tools/injectable";
createContainer,
DiContainer,
Injectable,
} from "@ogre-tools/injectable";
import asyncFn, { AsyncFnMock } from "@async-fn/jest"; import asyncFn, { AsyncFnMock } from "@async-fn/jest";
import { registerFeature } from "@k8slens/feature-core/src/register-feature"; import { registerFeature } from "@k8slens/feature-core/src/register-feature";
import { import {
@ -24,8 +20,19 @@ import { getRequestChannel } from "../../actual/request/get-request-channel";
import { startApplicationInjectionToken } from "@k8slens/application"; import { startApplicationInjectionToken } from "@k8slens/application";
import { messagingFeatureForUnitTesting } from "../feature"; import { messagingFeatureForUnitTesting } from "../feature";
[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach( type SomeMessageChannel = MessageChannel<string>;
({ scenarioIsAsync }) => 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 ${ describe(`get-message-bridge-fake, given running as ${
scenarioIsAsync ? "async" : "sync" scenarioIsAsync ? "async" : "sync"
}`, () => { }`, () => {
@ -53,10 +60,7 @@ import { messagingFeatureForUnitTesting } from "../feature";
runInAction(() => { runInAction(() => {
registerFeature(someDi1, messagingFeatureForUnitTesting); registerFeature(someDi1, messagingFeatureForUnitTesting);
registerFeature(someDi2, messagingFeatureForUnitTesting); registerFeature(someDi2, messagingFeatureForUnitTesting);
registerFeature( registerFeature(someDiWithoutListeners, messagingFeatureForUnitTesting);
someDiWithoutListeners,
messagingFeatureForUnitTesting
);
}); });
messageBridgeFake.involve(someDi1, someDi2, someDiWithoutListeners); messageBridgeFake.involve(someDi1, someDi2, someDiWithoutListeners);
@ -115,15 +119,10 @@ import { messagingFeatureForUnitTesting } from "../feature";
channel: someMessageChannel, channel: someMessageChannel,
getHandler: (di) => { getHandler: (di) => {
const sendMessage = di.inject( const sendMessage = di.inject(sendMessageToChannelInjectionToken);
sendMessageToChannelInjectionToken
);
return (message) => { return (message) => {
sendMessage( sendMessage(someMessageChannel, `some-response-to: ${message}`);
someMessageChannel,
`some-response-to: ${message}`
);
}; };
}, },
}); });
@ -136,26 +135,25 @@ import { messagingFeatureForUnitTesting } from "../feature";
describe("given a message is sent in di-1", () => { describe("given a message is sent in di-1", () => {
beforeEach(() => { beforeEach(() => {
const sendMessageToChannelFromDi1 = someDi1.inject( const sendMessageToChannelFromDi1 = someDi1.inject(
sendMessageToChannelInjectionToken sendMessageToChannelInjectionToken,
); );
sendMessageToChannelFromDi1(someMessageChannel, "some-message"); sendMessageToChannelFromDi1(someMessageChannel, "some-message");
}); });
describe( const scenarioTitle = scenarioIsAsync
scenarioIsAsync
? "when all message steps are propagated using a wrapper" ? "when all message steps are propagated using a wrapper"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioTitle, () => {
let someWrapper: jest.Mock; let someWrapper: jest.Mock;
beforeEach((done) => { beforeEach((done) => {
someWrapper = jest.fn((propagation) => propagation()); someWrapper = jest.fn((propagation) => propagation());
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake messageBridgeFake.messagePropagationRecursive(someWrapper).then(done);
.messagePropagationRecursive(someWrapper)
.then(done);
} else { } else {
done(); done();
} }
@ -163,7 +161,7 @@ import { messagingFeatureForUnitTesting } from "../feature";
it("the response gets handled in di-1", () => { it("the response gets handled in di-1", () => {
expect(someHandler1MockInDi1).toHaveBeenCalledWith( expect(someHandler1MockInDi1).toHaveBeenCalledWith(
"some-response-to: some-message" "some-response-to: some-message",
); );
}); });
@ -171,19 +169,17 @@ import { messagingFeatureForUnitTesting } from "../feature";
it("the wrapper gets called with the both propagations", () => { it("the wrapper gets called with the both propagations", () => {
expect(someWrapper).toHaveBeenCalledTimes(2); expect(someWrapper).toHaveBeenCalledTimes(2);
}); });
} });
);
describe( const scenarioName: string = scenarioIsAsync
scenarioIsAsync
? "when all message steps are propagated not using a wrapper" ? "when all message steps are propagated not using a wrapper"
: "immediately", : "immediately";
() => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake messageBridgeFake.messagePropagationRecursive().then(done);
.messagePropagationRecursive()
.then(done);
} else { } else {
done(); done();
} }
@ -191,19 +187,16 @@ import { messagingFeatureForUnitTesting } from "../feature";
it("the response gets handled in di-1", () => { it("the response gets handled in di-1", () => {
expect(someHandler1MockInDi1).toHaveBeenCalledWith( expect(someHandler1MockInDi1).toHaveBeenCalledWith(
"some-response-to: some-message" "some-response-to: some-message",
); );
}); });
} });
);
}); });
}); });
describe("when sending message in a DI", () => { describe("when sending message in a DI", () => {
beforeEach(() => { beforeEach(() => {
const sendMessageToChannelFromDi1 = someDi1.inject( const sendMessageToChannelFromDi1 = someDi1.inject(sendMessageToChannelInjectionToken);
sendMessageToChannelInjectionToken
);
sendMessageToChannelFromDi1(someMessageChannel, "some-message"); sendMessageToChannelFromDi1(someMessageChannel, "some-message");
}); });
@ -218,9 +211,10 @@ import { messagingFeatureForUnitTesting } from "../feature";
expect(someHandler2MockInDi2).not.toHaveBeenCalled(); expect(someHandler2MockInDi2).not.toHaveBeenCalled();
}); });
describe( const scenarioName = scenarioIsAsync ? "when messages are propagated" : "immediately";
scenarioIsAsync ? "when messages are propagated" : "immediately",
() => { // eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagation().then(done); messageBridgeFake.messagePropagation().then(done);
@ -230,16 +224,11 @@ import { messagingFeatureForUnitTesting } from "../feature";
}); });
it("listeners in other than sending DIs handle the message", () => { it("listeners in other than sending DIs handle the message", () => {
expect(someHandler1MockInDi2).toHaveBeenCalledWith( expect(someHandler1MockInDi2).toHaveBeenCalledWith("some-message");
"some-message"
);
expect(someHandler2MockInDi2).toHaveBeenCalledWith( expect(someHandler2MockInDi2).toHaveBeenCalledWith("some-message");
"some-message" });
);
}); });
}
);
scenarioIsAsync && scenarioIsAsync &&
describe("when messages are propagated using a wrapper, such as act() in react testing lib", () => { describe("when messages are propagated using a wrapper, such as act() in react testing lib", () => {
@ -256,13 +245,9 @@ import { messagingFeatureForUnitTesting } from "../feature";
}); });
it("listeners still handle the message", () => { it("listeners still handle the message", () => {
expect(someHandler1MockInDi2).toHaveBeenCalledWith( expect(someHandler1MockInDi2).toHaveBeenCalledWith("some-message");
"some-message"
);
expect(someHandler2MockInDi2).toHaveBeenCalledWith( expect(someHandler2MockInDi2).toHaveBeenCalledWith("some-message");
"some-message"
);
}); });
}); });
}); });
@ -272,9 +257,7 @@ import { messagingFeatureForUnitTesting } from "../feature";
someDi2.deregister(someListener1InDi2); someDi2.deregister(someListener1InDi2);
}); });
const sendMessageToChannelFromDi1 = someDi1.inject( const sendMessageToChannelFromDi1 = someDi1.inject(sendMessageToChannelInjectionToken);
sendMessageToChannelInjectionToken
);
someHandler1MockInDi2.mockClear(); someHandler1MockInDi2.mockClear();
@ -285,13 +268,9 @@ import { messagingFeatureForUnitTesting } from "../feature";
}); });
describe("given there are request listeners", () => { describe("given there are request listeners", () => {
let someHandler1MockInDi1: AsyncFnMock< let someHandler1MockInDi1: AsyncFnMock<(message: string) => Promise<number>>;
(message: string) => Promise<number>
>;
let someHandler1MockInDi2: AsyncFnMock< let someHandler1MockInDi2: AsyncFnMock<(message: string) => Promise<number>>;
(message: string) => Promise<number>
>;
let someListener1InDi2: Injectable<unknown, unknown>; let someListener1InDi2: Injectable<unknown, unknown>;
let actualPromise: Promise<number>; let actualPromise: Promise<number>;
@ -320,14 +299,9 @@ import { messagingFeatureForUnitTesting } from "../feature";
describe("when requesting from a channel in a DI", () => { describe("when requesting from a channel in a DI", () => {
beforeEach(() => { beforeEach(() => {
const requestFromChannelFromDi1 = someDi1.inject( const requestFromChannelFromDi1 = someDi1.inject(requestFromChannelInjectionToken);
requestFromChannelInjectionToken
);
actualPromise = requestFromChannelFromDi1( actualPromise = requestFromChannelFromDi1(someRequestChannel, "some-request");
someRequestChannel,
"some-request"
);
}); });
it("listener in requesting DI does not handle the request", () => { it("listener in requesting DI does not handle the request", () => {
@ -335,9 +309,7 @@ import { messagingFeatureForUnitTesting } from "../feature";
}); });
it("the listener in other than requesting DIs handle the request", () => { it("the listener in other than requesting DIs handle the request", () => {
expect(someHandler1MockInDi2).toHaveBeenCalledWith( expect(someHandler1MockInDi2).toHaveBeenCalledWith("some-request");
"some-request"
);
}); });
it("does not resolve yet", async () => { it("does not resolve yet", async () => {
@ -360,9 +332,7 @@ import { messagingFeatureForUnitTesting } from "../feature";
someDi2.deregister(someListener1InDi2); someDi2.deregister(someListener1InDi2);
}); });
const sendMessageToChannelFromDi1 = someDi1.inject( const sendMessageToChannelFromDi1 = someDi1.inject(sendMessageToChannelInjectionToken);
sendMessageToChannelInjectionToken
);
someHandler1MockInDi2.mockClear(); someHandler1MockInDi2.mockClear();
@ -372,8 +342,7 @@ import { messagingFeatureForUnitTesting } from "../feature";
}); });
it("given there are multiple listeners between different DIs for same channel, when requesting, throws", () => { it("given there are multiple listeners between different DIs for same channel, when requesting, throws", () => {
const someConflictingListenerInjectable = const someConflictingListenerInjectable = getRequestChannelListenerInjectable({
getRequestChannelListenerInjectable({
id: "conflicting-listener", id: "conflicting-listener",
channel: someRequestChannel, channel: someRequestChannel,
getHandler: () => () => 84, getHandler: () => () => 84,
@ -383,48 +352,25 @@ import { messagingFeatureForUnitTesting } from "../feature";
someDi1.register(someConflictingListenerInjectable); someDi1.register(someConflictingListenerInjectable);
}); });
const requestFromChannelFromDi2 = someDi2.inject( const requestFromChannelFromDi2 = someDi2.inject(requestFromChannelInjectionToken);
requestFromChannelInjectionToken
);
return expect(() => return expect(() =>
requestFromChannelFromDi2(someRequestChannel, "irrelevant") requestFromChannelFromDi2(someRequestChannel, "irrelevant"),
).rejects.toThrow( ).rejects.toThrow(
'Tried to make a request but multiple listeners were discovered for channel "some-request-channel" in multiple DIs.' '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", () => { it("when requesting from channel without listener, throws", () => {
const requestFromChannel = someDi1.inject( const requestFromChannel = someDi1.inject(requestFromChannelInjectionToken);
requestFromChannelInjectionToken
);
return expect(() => return expect(() =>
requestFromChannel( requestFromChannel(someRequestChannelWithoutListeners, "irrelevant"),
someRequestChannelWithoutListeners,
"irrelevant"
)
).rejects.toThrow( ).rejects.toThrow(
'Tried to make a request but no listeners for channel "some-request-channel-without-listeners" was discovered in any DIs' '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",
};

View File

@ -20,6 +20,126 @@ export type MessageBridgeFake = {
setAsync: (value: boolean) => void; 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));
});
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) =>
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 => { export const getMessageBridgeFake = (): MessageBridgeFake => {
const messageListenersByDi = new Map< const messageListenersByDi = new Map<
DiContainer, DiContainer,
@ -33,16 +153,15 @@ export const getMessageBridgeFake = (): MessageBridgeFake => {
const messagePropagationBuffer = new Set<AsyncFnMock<() => void>>(); const messagePropagationBuffer = new Set<AsyncFnMock<() => void>>();
const messagePropagation = async ( const messagePropagation = async (wrapper: (callback: any) => any = (callback) => callback()) => {
wrapper: (callback: any) => any = (callback) => callback()
) => {
const oldMessages = [...messagePropagationBuffer.values()]; const oldMessages = [...messagePropagationBuffer.values()];
messagePropagationBuffer.clear(); messagePropagationBuffer.clear();
await Promise.all(oldMessages.map((x) => wrapper(x.resolve))); await Promise.all(oldMessages.map((x) => wrapper(x.resolve)));
}; };
const messagePropagationRecursive = async ( const messagePropagationRecursive = async (
wrapper: (callback: any) => any = (callback) => callback() wrapper: (callback: any) => any = (callback) => callback(),
) => { ) => {
while (messagePropagationBuffer.size) { while (messagePropagationBuffer.size) {
await messagePropagation(wrapper); await messagePropagation(wrapper);
@ -75,136 +194,3 @@ export const getMessageBridgeFake = (): MessageBridgeFake => {
}, },
}; };
}; };
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);
};
});
};

View File

@ -1,8 +1,4 @@
import { import { createContainer, DiContainer, Injectable } from "@ogre-tools/injectable";
createContainer,
DiContainer,
Injectable,
} from "@ogre-tools/injectable";
import { registerFeature } from "@k8slens/feature-core"; import { registerFeature } from "@k8slens/feature-core";
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
@ -86,7 +82,7 @@ describe("listening-of-messages", () => {
await startApplication(); await startApplication();
}); });
it("it enlists a listener for the channel", () => { it("enlists a listener for the channel", () => {
expect(enlistMessageChannelListenerMock).toHaveBeenCalledWith({ expect(enlistMessageChannelListenerMock).toHaveBeenCalledWith({
id: "some-channel-id-message-listener-some-listener", id: "some-channel-id-message-listener-some-listener",
channel: someChannel, channel: someChannel,

View File

@ -90,7 +90,7 @@ describe("listening-of-requests", () => {
await startApplication(); await startApplication();
}); });
it("it enlists a listener for the channel", () => { it("enlists a listener for the channel", () => {
expect(enlistRequestChannelListenerMock).toHaveBeenCalledWith({ expect(enlistRequestChannelListenerMock).toHaveBeenCalledWith({
id: "some-channel-id-request-listener-some-listener", id: "some-channel-id-request-listener-some-listener",
channel: someChannel, channel: someChannel,

View File

@ -1,4 +1,4 @@
{ {
"extends": "@k8slens/typescript/config/base.json", "extends": "@k8slens/typescript/config/base.json",
"include": ["**/*.ts"] "include": ["**/*.ts", "**/*.tsx"]
} }

View File

@ -1,2 +1 @@
module.exports = module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;

View File

@ -29,9 +29,7 @@ describe("enlist message channel listener in main", () => {
di.override(ipcMainInjectable, () => ipcMainStub); di.override(ipcMainInjectable, () => ipcMainStub);
enlistMessageChannelListener = di.inject( enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
enlistMessageChannelListenerInjectionToken
);
}); });
describe("when called", () => { describe("when called", () => {
@ -53,10 +51,7 @@ describe("enlist message channel listener in main", () => {
}); });
it("registers the listener", () => { it("registers the listener", () => {
expect(onMock).toHaveBeenCalledWith( expect(onMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
"some-channel-id",
expect.any(Function)
);
}); });
it("does not de-register the listener yet", () => { it("does not de-register the listener yet", () => {
@ -75,10 +70,7 @@ describe("enlist message channel listener in main", () => {
it("when disposing the listener, de-registers the listener", () => { it("when disposing the listener, de-registers the listener", () => {
disposer(); disposer();
expect(offMock).toHaveBeenCalledWith( expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
"some-channel-id",
expect.any(Function)
);
}); });
}); });

View File

@ -1,16 +1,11 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { IpcMainInvokeEvent } from "electron"; import type { IpcMainInvokeEvent } from "electron";
import ipcMainInjectable from "../ipc-main/ipc-main.injectable"; import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
import type { import type { RequestChannel, RequestChannelListener } from "@k8slens/messaging";
RequestChannel,
RequestChannelListener,
} from "@k8slens/messaging";
import { enlistRequestChannelListenerInjectionToken } from "@k8slens/messaging"; import { enlistRequestChannelListenerInjectionToken } from "@k8slens/messaging";
export type EnlistRequestChannelListener = < export type EnlistRequestChannelListener = <TChannel extends RequestChannel<unknown, unknown>>(
TChannel extends RequestChannel<unknown, unknown> listener: RequestChannelListener<TChannel>,
>(
listener: RequestChannelListener<TChannel>
) => () => void; ) => () => void;
const enlistRequestChannelListenerInjectable = getInjectable({ const enlistRequestChannelListenerInjectable = getInjectable({
@ -20,8 +15,7 @@ const enlistRequestChannelListenerInjectable = getInjectable({
const ipcMain = di.inject(ipcMainInjectable); const ipcMain = di.inject(ipcMainInjectable);
return ({ channel, handler }) => { return ({ channel, handler }) => {
const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) => const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) => handler(request);
handler(request);
ipcMain.handle(channel.id, nativeHandleCallback); ipcMain.handle(channel.id, nativeHandleCallback);

View File

@ -37,9 +37,7 @@ describe("enlist request channel listener in main", () => {
di.override(ipcMainInjectable, () => ipcMainStub); di.override(ipcMainInjectable, () => ipcMainStub);
enlistRequestChannelListener = di.inject( enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectable);
enlistRequestChannelListenerInjectable
);
}); });
describe("when called", () => { describe("when called", () => {
@ -61,10 +59,7 @@ describe("enlist request channel listener in main", () => {
}); });
it("registers the listener", () => { it("registers the listener", () => {
expect(handleMock).toHaveBeenCalledWith( expect(handleMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
"some-channel-id",
expect.any(Function)
);
}); });
it("does not de-register the listener yet", () => { it("does not de-register the listener yet", () => {
@ -75,10 +70,7 @@ describe("enlist request channel listener in main", () => {
let actualPromise: Promise<any>; let actualPromise: Promise<any>;
beforeEach(() => { beforeEach(() => {
actualPromise = handleMock.mock.calls[0][1]( actualPromise = handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, "some-request");
{} as IpcMainInvokeEvent,
"some-request"
);
}); });
it("calls the handler with the request", () => { it("calls the handler with the request", () => {
@ -105,10 +97,7 @@ describe("enlist request channel listener in main", () => {
it("when disposing the listener, de-registers the listener", () => { it("when disposing the listener, de-registers the listener", () => {
disposer(); disposer();
expect(offMock).toHaveBeenCalledWith( expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
"some-channel-id",
expect.any(Function)
);
}); });
}); });

View File

@ -9,9 +9,7 @@ export const messagingFeatureForMain = getFeature({
di, di,
targetModule: module, targetModule: module,
getRequireContexts: () => [ getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)],
require.context("./", true, /\.injectable\.(ts|tsx)$/),
],
}); });
}, },
}); });

View File

@ -1,2 +1 @@
module.exports = module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;

View File

@ -9,9 +9,7 @@ export const messagingFeatureForRenderer = getFeature({
di, di,
targetModule: module, targetModule: module,
getRequireContexts: () => [ getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)],
require.context("./", true, /\.injectable\.(ts|tsx)$/),
],
}); });
}, },
}); });

View File

@ -29,9 +29,7 @@ describe("enlist message channel listener in renderer", () => {
di.override(ipcRendererInjectable, () => ipcRendererStub); di.override(ipcRendererInjectable, () => ipcRendererStub);
enlistMessageChannelListener = di.inject( enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
enlistMessageChannelListenerInjectionToken
);
}); });
describe("when called", () => { describe("when called", () => {
@ -53,10 +51,7 @@ describe("enlist message channel listener in renderer", () => {
}); });
it("registers the listener", () => { it("registers the listener", () => {
expect(onMock).toHaveBeenCalledWith( expect(onMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
"some-channel-id",
expect.any(Function)
);
}); });
it("does not de-register the listener yet", () => { it("does not de-register the listener yet", () => {
@ -75,10 +70,7 @@ describe("enlist message channel listener in renderer", () => {
it("when disposing the listener, de-registers the listener", () => { it("when disposing the listener, de-registers the listener", () => {
disposer(); disposer();
expect(offMock).toHaveBeenCalledWith( expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
"some-channel-id",
expect.any(Function)
);
}); });
}); });

View File

@ -9,8 +9,7 @@ const requestFromChannelInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const invokeIpc = di.inject(invokeIpcInjectable); const invokeIpc = di.inject(invokeIpcInjectable);
return ((channel, request) => return ((channel, request) => invokeIpc(channel.id, request)) as RequestFromChannel;
invokeIpc(channel.id, request)) as RequestFromChannel;
}, },
injectionToken: requestFromChannelInjectionToken, injectionToken: requestFromChannelInjectionToken,

View File

@ -34,10 +34,7 @@ describe("request-from-channel", () => {
}); });
it("invokes ipcRenderer of Electron", () => { it("invokes ipcRenderer of Electron", () => {
expect(invokeIpcMock).toHaveBeenCalledWith( expect(invokeIpcMock).toHaveBeenCalledWith("some-channel-id", "some-request-payload");
"some-channel-id",
"some-request-payload"
);
}); });
it("when invoke resolves with response, resolves with said response", async () => { it("when invoke resolves with response, resolves with said response", async () => {

View File

@ -1,9 +1,6 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import sendToIpcInjectable from "./send-to-ipc.injectable"; import sendToIpcInjectable from "./send-to-ipc.injectable";
import { import { SendMessageToChannel, sendMessageToChannelInjectionToken } from "@k8slens/messaging";
SendMessageToChannel,
sendMessageToChannelInjectionToken,
} from "@k8slens/messaging";
const messageToChannelInjectable = getInjectable({ const messageToChannelInjectable = getInjectable({
id: "message-to-channel", id: "message-to-channel",

View File

@ -22,9 +22,7 @@ describe("message-from-channel", () => {
describe("when called", () => { describe("when called", () => {
beforeEach(() => { beforeEach(() => {
const sendMessageToChannel = di.inject( const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
sendMessageToChannelInjectionToken
);
const someChannel: MessageChannel<number> = { const someChannel: MessageChannel<number> = {
id: "some-channel-id", id: "some-channel-id",