1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Make startable stoppable support async starting

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-05-04 12:52:15 +03:00
parent 952ff1deb9
commit c7dba52cd0
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
10 changed files with 183 additions and 94 deletions

View File

@ -2,16 +2,20 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getStartableStoppable } from "./get-startable-stoppable";
import { getPromiseStatus } from "../test-utils/get-promise-status";
import { flushPromises } from "../test-utils/flush-promises";
describe("getStartableStoppable", () => {
let stopMock: jest.Mock<() => void>;
let startMock: jest.Mock<() => () => void>;
let actual: { stop: () => void; start: () => void; started: boolean };
let stopMock: AsyncFnMock<() => Promise<void>>;
let startMock: AsyncFnMock<() => Promise<() => Promise<void>>>;
let actual: { stop: () => Promise<void>; start: () => Promise<void>; started: boolean };
beforeEach(() => {
stopMock = jest.fn();
startMock = jest.fn(() => stopMock);
stopMock = asyncFn();
startMock = asyncFn();
actual = getStartableStoppable("some-id", startMock);
});
@ -25,9 +29,7 @@ describe("getStartableStoppable", () => {
});
it("when stopping before ever starting, throws", () => {
expect(() => {
actual.stop();
}).toThrow("Tried to stop \"some-id\", but it has not started yet.");
expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has not started yet.");
});
it("is not started", () => {
@ -35,69 +37,180 @@ describe("getStartableStoppable", () => {
});
describe("when started", () => {
let startPromise: Promise<void>;
beforeEach(() => {
actual.start();
startPromise = actual.start();
});
it("starts", () => {
it("starts starting", () => {
expect(startMock).toHaveBeenCalled();
});
it("is started", () => {
expect(actual.started).toBe(true);
it("starting does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(startPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when started again, throws", () => {
expect(() => {
actual.start();
}).toThrow("Tried to start \"some-id\", but it has already started.");
it("is not started yet", () => {
expect(actual.started).toBe(false);
});
it("does not stop yet", () => {
expect(stopMock).not.toHaveBeenCalled();
});
describe("when stopped", () => {
beforeEach(() => {
actual.stop();
describe("when starting finishes", () => {
beforeEach(async () => {
await startMock.resolve(stopMock);
});
it("stops", () => {
expect(stopMock).toHaveBeenCalled();
it("is started", () => {
expect(actual.started).toBe(true);
});
it("is not started", () => {
expect(actual.started).toBe(false);
it("starting resolves", async () => {
const promiseStatus = await getPromiseStatus(startPromise);
expect(promiseStatus.fulfilled).toBe(true);
});
it("when stopped again, throws", () => {
expect(() => {
actual.stop();
}).toThrow("Tried to stop \"some-id\", but it has already stopped.");
it("when started again, throws", () => {
expect(actual.start).rejects.toThrow("Tried to start \"some-id\", but it has already started.");
});
describe("when started again", () => {
it("does not stop yet", () => {
expect(stopMock).not.toHaveBeenCalled();
});
describe("when stopped", () => {
let stopPromise: Promise<void>;
beforeEach(() => {
startMock.mockClear();
actual.start();
stopPromise = actual.stop();
});
it("starts", () => {
expect(startMock).toHaveBeenCalled();
it("starts stopping", () => {
expect(stopMock).toHaveBeenCalled();
});
it("is started", () => {
it("stopping does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(stopPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("is not stopped yet", () => {
expect(actual.started).toBe(true);
});
it("when stopped, stops", () => {
stopMock.mockClear();
describe("when stopping finishes", () => {
beforeEach(async () => {
await stopMock.resolve();
});
actual.stop();
it("is not started", () => {
expect(actual.started).toBe(false);
});
it("stopping resolves", async () => {
const promiseStatus = await getPromiseStatus(stopPromise);
expect(promiseStatus.fulfilled).toBe(true);
});
it("when stopped again, throws", () => {
expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has already stopped.");
});
describe("when started again", () => {
beforeEach(
() => {
startMock.mockClear();
actual.start();
});
it("starts", () => {
expect(startMock).toHaveBeenCalled();
});
it("is not started yet", () => {
expect(actual.started).toBe(false);
});
describe("when starting finishes", () => {
beforeEach(async () => {
await startMock.resolve(stopMock);
});
it("is started", () => {
expect(actual.started).toBe(true);
});
it("when stopped again, starts stopping again", async () => {
stopMock.mockClear();
actual.stop();
await flushPromises();
expect(stopMock).toHaveBeenCalled();
});
});
});
});
});
});
describe("when stopped before starting finishes", () => {
let stopPromise: Promise<void>;
beforeEach(() => {
stopPromise = actual.stop();
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(stopPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("is not started yet", () => {
expect(actual.started).toBe(false);
});
describe("when starting finishes", () => {
beforeEach(async () => {
await startMock.resolve(stopMock);
});
it("starts stopping", () => {
expect(stopMock).toHaveBeenCalled();
});
it("is not stopped yet", () => {
expect(actual.started).toBe(true);
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(stopPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
describe("when stopping finishes", () => {
beforeEach(async () => {
await stopMock.resolve();
});
it("is stopped", () => {
expect(actual.started).toBe(false);
});
it("resolves", async () => {
const promiseStatus = await getPromiseStatus(stopPromise);
expect(promiseStatus.fulfilled).toBe(true);
});
});
});
});
});

View File

@ -2,32 +2,39 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
type Stopper = () => Promise<void> | void;
type Starter = () => Promise<Stopper> | Stopper;
export const getStartableStoppable = (
id: string,
startAndGetStopCallback: () => () => void,
startAndGetStopCallback: Starter,
) => {
let dispose: () => void;
let stop: Stopper;
let stopped = false;
let started = false;
let startingPromise: Promise<Stopper> | Stopper;
return {
get started() {
return started;
},
start: () => {
start: async () => {
if (started) {
throw new Error(`Tried to start "${id}", but it has already started.`);
}
startingPromise = startAndGetStopCallback();
stop = await startingPromise;
stopped = false;
dispose = startAndGetStopCallback();
started = true;
},
stop: () => {
stop: async () => {
await startingPromise;
if (stopped) {
throw new Error(`Tried to stop "${id}", but it has already stopped.`);
}
@ -36,10 +43,9 @@ export const getStartableStoppable = (
throw new Error(`Tried to stop "${id}", but it has not started yet.`);
}
await stop();
started = false;
dispose();
stopped = true;
},
};

View File

@ -13,8 +13,8 @@ const startCatalogSyncInjectable = getInjectable({
const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable);
return {
run: () => {
catalogSyncToRenderer.start();
run: async () => {
await catalogSyncToRenderer.start();
},
};
},

View File

@ -13,9 +13,9 @@ const stopCatalogSyncInjectable = getInjectable({
const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable);
return {
run: () => {
run: async () => {
if (catalogSyncToRenderer.started) {
catalogSyncToRenderer.stop();
await catalogSyncToRenderer.stop();
}
},
};

View File

@ -15,8 +15,8 @@ const startApplicationMenuInjectable = getInjectable({
);
return {
run: () => {
applicationMenu.start();
run: async () => {
await applicationMenu.start();
},
};
},

View File

@ -15,8 +15,8 @@ const stopApplicationMenuInjectable = getInjectable({
);
return {
run: () => {
applicationMenu.stop();
run: async () => {
await applicationMenu.stop();
},
};
},

View File

@ -13,8 +13,8 @@ const startTrayInjectable = getInjectable({
const trayInitializer = di.inject(trayInjectable);
return {
run: () => {
trayInitializer.start();
run: async () => {
await trayInitializer.start();
},
};
},

View File

@ -13,8 +13,8 @@ const stopTrayInjectable = getInjectable({
const trayInitializer = di.inject(trayInjectable);
return {
run: () => {
trayInitializer.stop();
run: async () => {
await trayInitializer.stop();
},
};
},

View File

@ -1,26 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable";
const trayIconPathInjectable = getInjectable({
id: "tray-icon-path",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const isDevelopment = di.inject(isDevelopmentInjectable);
const staticDir = di.inject(staticFilesDirectoryInjectable);
return getAbsolutePath(
staticDir,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
"trayIconTemplate.png",
);
},
});
export default trayIconPathInjectable;

View File

@ -9,10 +9,8 @@ import navigateToPreferencesInjectable from "../../common/front-end-routing/rout
import stopServicesAndExitAppInjectable from "../stop-services-and-exit-app.injectable";
import { getStartableStoppable } from "../../common/utils/get-startable-stoppable";
import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
import trayIconPathInjectable from "./tray-icon-path.injectable";
import showAboutInjectable from "../menu/show-about.injectable";
import showApplicationWindowInjectable
from "../start-main-application/lens-window/show-application-window.injectable";
import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
const trayInjectable = getInjectable({
id: "tray",
@ -22,7 +20,6 @@ const trayInjectable = getInjectable({
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
const trayIconPath = di.inject(trayIconPathInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const showAboutPopup = di.inject(showAboutInjectable);
@ -32,7 +29,6 @@ const trayInjectable = getInjectable({
navigateToPreferences,
stopServicesAndExitApp,
isAutoUpdateEnabled,
trayIconPath,
showApplicationWindow,
showAboutPopup,
),