From f7a9643c11d83c4ab1c106f3672cb370c59b6ed1 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 2 Nov 2022 16:10:16 -0400 Subject: [PATCH] Add getSyncStartableStoppable and tests Signed-off-by: Sebastian Malton --- .../utils/get-startable-stoppable.test.ts | 69 +++++++++++-- src/common/utils/get-startable-stoppable.ts | 97 ++++++++++++------- 2 files changed, 125 insertions(+), 41 deletions(-) diff --git a/src/common/utils/get-startable-stoppable.test.ts b/src/common/utils/get-startable-stoppable.test.ts index 24b9e1862d..6d49c71cab 100644 --- a/src/common/utils/get-startable-stoppable.test.ts +++ b/src/common/utils/get-startable-stoppable.test.ts @@ -4,14 +4,15 @@ */ import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; -import { getStartableStoppable } from "./get-startable-stoppable"; +import type { StartableStoppable, SyncStartableStoppable } from "./get-startable-stoppable"; +import { getSyncStartableStoppable, getStartableStoppable } from "./get-startable-stoppable"; import { getPromiseStatus } from "../test-utils/get-promise-status"; import { flushPromises } from "../test-utils/flush-promises"; describe("getStartableStoppable", () => { let stopMock: AsyncFnMock<() => Promise>; let startMock: AsyncFnMock<() => Promise<() => Promise>>; - let actual: { stop: () => Promise; start: () => Promise; started: boolean }; + let actual: StartableStoppable; beforeEach(() => { stopMock = asyncFn(); @@ -29,7 +30,7 @@ describe("getStartableStoppable", () => { }); it("when stopping before ever starting, throws", () => { - expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has not started yet."); + expect(async () => actual.stop()).rejects.toThrow("Tried to stop \"some-id\", but it is already stopped."); }); it("is not started", () => { @@ -71,7 +72,7 @@ describe("getStartableStoppable", () => { }); it("throws", () => { - expect(error.message).toBe("Tried to start \"some-id\", but it is already being started."); + expect(error.message).toBe("Tried to start \"some-id\", but it is already starting."); }); }); @@ -91,7 +92,7 @@ describe("getStartableStoppable", () => { }); it("when started again, throws", () => { - expect(actual.start).rejects.toThrow("Tried to start \"some-id\", but it has already started."); + expect(actual.start).rejects.toThrow("Tried to start \"some-id\", but it is already started."); }); it("does not stop yet", () => { @@ -135,7 +136,7 @@ describe("getStartableStoppable", () => { }); it("when stopped again, throws", () => { - expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has already stopped."); + expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it is already stopped."); }); describe("when started again", () => { @@ -233,3 +234,59 @@ describe("getStartableStoppable", () => { }); }); }); + +describe("getSyncStartableStoppable", () => { + let stopMock: jest.MockedFunction<() => void>; + let startMock: jest.MockedFunction<() => () => void>; + let actual: SyncStartableStoppable; + + beforeEach(() => { + stopMock = jest.fn(); + startMock = jest.fn().mockImplementation(() => stopMock); + actual = getSyncStartableStoppable("some-id", startMock); + }); + + it("does not start yet", () => { + expect(startMock).not.toHaveBeenCalled(); + }); + + it("does not stop yet", () => { + expect(stopMock).not.toHaveBeenCalled(); + }); + + it("when stopping before ever starting, throws", () => { + expect(() => actual.stop()).toThrow("Tried to stop \"some-id\", but it is already stopped."); + }); + + it("is not started", () => { + expect(actual.started).toBe(false); + }); + + describe("when started", () => { + beforeEach(() => { + actual.start(); + }); + + it("calls start function", () => { + expect(startMock).toHaveBeenCalled(); + }); + + it("is started", () => { + expect(actual.started).toBe(true); + }); + + describe("when stopped", () => { + beforeEach(() => { + actual.stop(); + }); + + it("calls stop function", () => { + expect(stopMock).toBeCalled(); + }); + + it("is stopped", () => { + expect(actual.started).toBe(false); + }); + }); + }); +}); diff --git a/src/common/utils/get-startable-stoppable.ts b/src/common/utils/get-startable-stoppable.ts index 5590157ab8..3c7194ad78 100644 --- a/src/common/utils/get-startable-stoppable.ts +++ b/src/common/utils/get-startable-stoppable.ts @@ -3,58 +3,85 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -type Stopper = () => Promise | void; -type Starter = () => Promise | Stopper; +export type Stopper = () => Promise | void; +export type Starter = () => Promise | Stopper; -export const getStartableStoppable = ( - id: string, - startAndGetStopCallback: Starter, -) => { +export type SyncStopper = () => void; +export type SyncStarter = () => SyncStopper; + +export interface SyncStartableStoppable { + readonly started: boolean; + start: () => void; + stop: () => void; +} + +export interface StartableStoppable { + readonly started: boolean; + start: () => Promise; + stop: () => Promise; +} + +type StartableStoppableState = "stopped" | "started" | "starting"; + +export function getSyncStartableStoppable(id: string, syncStartAndGetSyncStopper: SyncStarter): SyncStartableStoppable { let stop: Stopper; - let stopped = false; - let started = false; - let starting = false; + let state: StartableStoppableState = "stopped"; + + return { + get started() { + return state === "started"; + }, + + start: (): void | Promise => { + if (state !== "stopped") { + throw new Error(`Tried to start "${id}", but it is already ${state}.`); + } + + state = "starting"; + stop = syncStartAndGetSyncStopper(); + state = "started"; + }, + + stop: (): void | Promise => { + if (state !== "started") { + throw new Error(`Tried to stop "${id}", but it is already ${state}.`); + } + + stop(); + state = "stopped"; + }, + }; +} + +export function getStartableStoppable(id: string, startAndGetStopCallback: Starter): StartableStoppable { + let stop: Stopper; + let state: StartableStoppableState = "stopped"; let startingPromise: Promise | Stopper; return { get started() { - return started; + return state === "started"; }, start: async () => { - if (starting) { - throw new Error(`Tried to start "${id}", but it is already being started.`); - } - - starting = true; - - if (started) { - throw new Error(`Tried to start "${id}", but it has already started.`); + if (state !== "stopped") { + throw new Error(`Tried to start "${id}", but it is already ${state}.`); } + state = "starting"; startingPromise = startAndGetStopCallback(); stop = await startingPromise; - - stopped = false; - started = true; - starting = false; + state = "started"; }, stop: async () => { + if (state === "stopped") { + throw new Error(`Tried to stop "${id}", but it is already ${state}.`); + } + await startingPromise; - - if (stopped) { - throw new Error(`Tried to stop "${id}", but it has already stopped.`); - } - - if (!started) { - throw new Error(`Tried to stop "${id}", but it has not started yet.`); - } - await stop(); - - started = false; - stopped = true; + state = "stopped"; }, }; -}; +}