mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Convert runMany and runManySync to use injectManyWithMeta + move to seperate package (#7244)
* Convert runMany and runManySync to use injectManyWithMeta Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fixup type errors due to new Runnable requirements Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add documentation for verifyRunnablesAreDAG Signed-off-by: Sebastian Malton <sebastian@malton.name> * Simplify convertToWithIdWith Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move all utility functions to separate package Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move testing utilities to separate package Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move run-many and run-many-sync to separate package Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace all internal uses of utilities with new packages Signed-off-by: Sebastian Malton <sebastian@malton.name> * Use new @k8slens/run-many package in core Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add dep to open-lens Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fixup type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fixup uses of @k8slens/test-utils Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fixup getGlobalOverride Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move tests to new package too Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fixup uses of AsyncResult and autoBind Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fixup remaining import issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * Finial fixups to fix build Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert moving "testUsingFakeTime" to separate package - This fixes tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix integration tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit test failing due to spelling fix Signed-off-by: Sebastian Malton <sebastian@malton.name> --------- Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
ad9bafe2a5
commit
2789bcebcb
754 files changed
+2715
-2752
No files matched your search
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainerForInjection, InjectionInstanceWithMeta } from "@ogre-tools/injectable";
|
||||
import { getOrInsertSetFor, isDefined } from "@k8slens/utilities";
|
||||
import * as uuid from "uuid";
|
||||
import assert from "assert";
|
||||
import type { Runnable, RunnableSync, RunnableSyncWithId, RunnableWithId } from "./types";
|
||||
|
||||
const computedNextEdge = (traversed: string[], graph: Map<string, Set<string>>, currentId: string, seenIds: Set<string>) => {
|
||||
seenIds.add(currentId);
|
||||
const currentNode = graph.get(currentId);
|
||||
|
||||
assert(currentNode, `Runnable graph does not contain node with id="${currentId}"`);
|
||||
|
||||
for (const nextId of currentNode.values()) {
|
||||
if (traversed.includes(nextId)) {
|
||||
throw new Error(`Cycle in runnable graph: "${traversed.join(`" -> "`)}" -> "${nextId}"`);
|
||||
}
|
||||
|
||||
computedNextEdge([...traversed, nextId], graph, nextId, seenIds);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies that the graph produces by `runnables`'s `runAfter`'s is acyclic. Namely that it
|
||||
* produces a Directed Acyclic Graph.
|
||||
* @param tokenId The ID of the injectionToken that was `injectManyWithMeta()`-ed. Used for error messages
|
||||
* @param runnables The list of runnables to check.
|
||||
*/
|
||||
export function verifyRunnablesAreDAG<Param>(tokenId: string, runnables: RunnableWithId<Param>[]): void;
|
||||
export function verifyRunnablesAreDAG<Param>(tokenId: string, runnables: RunnableSyncWithId<Param>[]): void;
|
||||
|
||||
export function verifyRunnablesAreDAG<Param>(tokenId: string, runnables: (RunnableWithId<Param>[]) | (RunnableSyncWithId<Param>[])): void {
|
||||
const rootId = uuid.v4();
|
||||
const runnableGraph = new Map<string, Set<string>>();
|
||||
const seenIds = new Set<string>();
|
||||
const addRunnableId = getOrInsertSetFor(runnableGraph);
|
||||
|
||||
// Build the Directed graph
|
||||
for (const runnable of runnables) {
|
||||
addRunnableId(runnable.id);
|
||||
|
||||
if (runnable.runAfter.length === 0) {
|
||||
addRunnableId(rootId).add(runnable.id);
|
||||
} else {
|
||||
for (const parentRunnable of runnable.runAfter) {
|
||||
addRunnableId(parentRunnable.id).add(runnable.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addRunnableId(rootId);
|
||||
|
||||
// Do a DFS to find any cycles
|
||||
computedNextEdge([], runnableGraph, rootId, seenIds);
|
||||
|
||||
for (const id of runnableGraph.keys()) {
|
||||
if (!seenIds.has(id)) {
|
||||
const runnable = runnables.find(runnable => runnable.id === id);
|
||||
|
||||
if (!runnable) {
|
||||
throw new Error(`Runnable "${id}" is not part of the injection token "${tokenId}"`);
|
||||
}
|
||||
|
||||
const runAfters = [runnable.runAfter]
|
||||
.flat()
|
||||
.filter(isDefined)
|
||||
.map(runnable => runnable.id)
|
||||
.join('", "');
|
||||
|
||||
throw new Error(`Runnable "${id}" is unreachable for injection token "${tokenId}": run afters "${runAfters}" are a part of different injection tokens.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConvertToWithId {
|
||||
<Param>(src: InjectionInstanceWithMeta<Runnable<Param>>): RunnableWithId<Param>;
|
||||
<Param>(src: InjectionInstanceWithMeta<RunnableSync<Param>>): RunnableSyncWithId<Param>;
|
||||
}
|
||||
|
||||
export const convertToWithIdWith = (di: DiContainerForInjection) => {
|
||||
const convert = <Param>(meta: { id: string }, instance: Runnable<Param>): RunnableWithId<Param> => ({
|
||||
id: meta.id,
|
||||
run: instance.run,
|
||||
runAfter: [instance.runAfter]
|
||||
.flat()
|
||||
.filter(isDefined)
|
||||
.map((injectable) => convert(injectable, di.inject(injectable))),
|
||||
});
|
||||
|
||||
return ((src) => convert(src.meta, src.instance)) as ConvertToWithId;
|
||||
};
|
||||
@@ -0,0 +1,622 @@
|
||||
/**
|
||||
* 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 { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { Runnable } from "./types";
|
||||
import { runManyFor } from "./run-many-for";
|
||||
import { getPromiseStatus, flushPromises } from "@k8slens/test-utils";
|
||||
import { runInAction } from "mobx";
|
||||
|
||||
describe("runManyFor", () => {
|
||||
describe("given no hierarchy, when running many", () => {
|
||||
let runMock: AsyncFnMock<(...args: unknown[]) => Promise<void>>;
|
||||
let actualPromise: Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
runMock = asyncFn();
|
||||
|
||||
const someInjectionTokenForRunnables = getInjectionToken<Runnable>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-injectable",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-call"),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-other-injectable",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-other-call"),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable);
|
||||
|
||||
const runMany = runManyFor(rootDi)(someInjectionTokenForRunnables);
|
||||
|
||||
actualPromise = runMany() as Promise<void>;
|
||||
});
|
||||
|
||||
it("runs all runnables at the same time", () => {
|
||||
expect(runMock.mock.calls).toEqual([
|
||||
["some-call"],
|
||||
["some-other-call"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
it("when all runnables resolve, resolves", async () => {
|
||||
await Promise.all([runMock.resolve(), runMock.resolve()]);
|
||||
|
||||
expect(await actualPromise).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given hierarchy that is three levels deep, when running many", () => {
|
||||
let runMock: AsyncFnMock<(...args: unknown[]) => Promise<void>>;
|
||||
let actualPromise: Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = createContainer("irrelevant");
|
||||
|
||||
runMock = asyncFn();
|
||||
|
||||
const someInjectionTokenForRunnables = getInjectionToken<Runnable>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable1 = getInjectable({
|
||||
id: "some-injectable-1",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("third-level-run"),
|
||||
runAfter: someInjectable2,
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
const someInjectable2 = getInjectable({
|
||||
id: "some-injectable-2",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("second-level-run"),
|
||||
runAfter: someInjectable3,
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
const someInjectable3 = getInjectable({
|
||||
id: "some-injectable-3",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("first-level-run"),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
di.register(someInjectable1, someInjectable2, someInjectable3);
|
||||
|
||||
const runMany = runManyFor(di)(someInjectionTokenForRunnables);
|
||||
|
||||
actualPromise = runMany() as Promise<void>;
|
||||
});
|
||||
|
||||
it("runs first level runnables", () => {
|
||||
expect(runMock.mock.calls).toEqual([["first-level-run"]]);
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when first level runnables resolve", () => {
|
||||
beforeEach(async () => {
|
||||
runMock.mockClear();
|
||||
|
||||
await runMock.resolveSpecific(["first-level-run"]);
|
||||
});
|
||||
|
||||
it("runs second level runnables", async () => {
|
||||
expect(runMock.mock.calls).toEqual([["second-level-run"]]);
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when second level runnables resolve", () => {
|
||||
beforeEach(async () => {
|
||||
runMock.mockClear();
|
||||
|
||||
await runMock.resolveSpecific(["second-level-run"]);
|
||||
});
|
||||
|
||||
it("runs final third level runnables", async () => {
|
||||
expect(runMock.mock.calls).toEqual([["third-level-run"]]);
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when final third level runnables resolve", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["third-level-run"]);
|
||||
});
|
||||
|
||||
it("resolves", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("given invalid hierarchy, when running runnables, throws", () => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
const runMock = asyncFn<(...args: unknown[]) => void>();
|
||||
|
||||
const someInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someOtherInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-other-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-runnable-1",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-runnable-1"),
|
||||
runAfter: someOtherInjectable,
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-runnable-2",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-runnable-2"),
|
||||
}),
|
||||
injectionToken: someOtherInjectionToken,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable);
|
||||
|
||||
const runMany = runManyFor(rootDi)(
|
||||
someInjectionToken,
|
||||
);
|
||||
|
||||
return expect(() => runMany()).rejects.toThrow(
|
||||
/Runnable "some-runnable-1" is unreachable for injection token "some-injection-token": run afters "some-runnable-2" are a part of different injection tokens./,
|
||||
);
|
||||
});
|
||||
|
||||
it("given partially incorrect hierarchy, when running runnables, throws", () => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
const runMock = asyncFn<(...args: unknown[]) => void>();
|
||||
|
||||
const someInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someOtherInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-other-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-runnable-1",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-runnable-1"),
|
||||
runAfter: [
|
||||
someOtherInjectable,
|
||||
someSecondInjectable,
|
||||
],
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const someSecondInjectable = getInjectable({
|
||||
id: "some-runnable-2",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-runnable-2"),
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-runnable-3",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-runnable-3"),
|
||||
}),
|
||||
injectionToken: someOtherInjectionToken,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable, someSecondInjectable);
|
||||
|
||||
const runMany = runManyFor(rootDi)(
|
||||
someInjectionToken,
|
||||
);
|
||||
|
||||
return expect(() => runMany()).rejects.toThrow(
|
||||
/Runnable "some-runnable-3" is not part of the injection token "some-injection-token"/,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when running many with parameter", () => {
|
||||
let runMock: AsyncFnMock<(...args: unknown[]) => Promise<void>>;
|
||||
|
||||
beforeEach(() => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
runMock = asyncFn();
|
||||
|
||||
const someInjectionTokenForRunnablesWithParameter = getInjectionToken<
|
||||
Runnable<string>
|
||||
>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-runnable-1",
|
||||
instantiate: () => ({
|
||||
run: (parameter) => runMock("run-of-some-runnable-1", parameter),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnablesWithParameter,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-runnable-2",
|
||||
instantiate: () => ({
|
||||
run: (parameter) => runMock("run-of-some-runnable-2", parameter),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnablesWithParameter,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable);
|
||||
|
||||
const runMany = runManyFor(rootDi)(
|
||||
someInjectionTokenForRunnablesWithParameter,
|
||||
);
|
||||
|
||||
runMany("some-parameter");
|
||||
});
|
||||
|
||||
it("runs all runnables using the parameter", () => {
|
||||
expect(runMock.mock.calls).toEqual([
|
||||
["run-of-some-runnable-1", "some-parameter"],
|
||||
["run-of-some-runnable-2", "some-parameter"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given multiple runAfters", () => {
|
||||
let runMock: AsyncFnMock<(...args: unknown[]) => void>;
|
||||
let finishingPromise: Promise<void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
runMock = asyncFn<(...args: unknown[]) => void>();
|
||||
|
||||
const someInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const runnableOneInjectable = getInjectable({
|
||||
id: "runnable-1",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("runnable-1"),
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableTwoInjectable = getInjectable({
|
||||
id: "runnable-2",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("runnable-2"),
|
||||
runAfter: [], // shouldn't block being called
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableThreeInjectable = getInjectable({
|
||||
id: "runnable-3",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("runnable-3"),
|
||||
runAfter: runnableOneInjectable,
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableFourInjectable = getInjectable({
|
||||
id: "runnable-4",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("runnable-4"),
|
||||
runAfter: [runnableThreeInjectable], // should be the same as an single item
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableFiveInjectable = getInjectable({
|
||||
id: "runnable-5",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("runnable-5"),
|
||||
runAfter: runnableThreeInjectable,
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableSixInjectable = getInjectable({
|
||||
id: "runnable-6",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("runnable-6"),
|
||||
runAfter: [
|
||||
runnableFourInjectable,
|
||||
runnableFiveInjectable,
|
||||
],
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableSevenInjectable = getInjectable({
|
||||
id: "runnable-7",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("runnable-7"),
|
||||
runAfter: [
|
||||
runnableFiveInjectable,
|
||||
runnableSixInjectable,
|
||||
],
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
rootDi.register(
|
||||
runnableOneInjectable,
|
||||
runnableTwoInjectable,
|
||||
runnableThreeInjectable,
|
||||
runnableFourInjectable,
|
||||
runnableFiveInjectable,
|
||||
runnableSixInjectable,
|
||||
runnableSevenInjectable,
|
||||
);
|
||||
});
|
||||
|
||||
const runMany = runManyFor(rootDi);
|
||||
const runSome = runMany(someInjectionToken);
|
||||
|
||||
finishingPromise = runSome();
|
||||
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("should run 'runnable-1'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-1");
|
||||
});
|
||||
|
||||
it("should run 'runnable-2'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-2");
|
||||
});
|
||||
|
||||
it("should not run 'runnable-3'", () => {
|
||||
expect(runMock).not.toBeCalledWith("runnable-3");
|
||||
});
|
||||
|
||||
describe("when 'runnable-1' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-1"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-3'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-3");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-3' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-3"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-4'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-4");
|
||||
});
|
||||
|
||||
it("should run 'runnable-5'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-5");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-4' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-4"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-5' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-5"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-6'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-6");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-6' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-6"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-7'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-7");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(7);
|
||||
});
|
||||
|
||||
describe("when 'runnable-7' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-7"]);
|
||||
});
|
||||
|
||||
it("should resolve the runMany promise call", async () => {
|
||||
await finishingPromise;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-5' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-5"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-4' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-4"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-6'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-6");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-6' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-6"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-7'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-7");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(7);
|
||||
});
|
||||
|
||||
describe("when 'runnable-7' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-7"]);
|
||||
});
|
||||
|
||||
it("should resolve the runMany promise call", async () => {
|
||||
await finishingPromise;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable";
|
||||
import { getOrInsert } from "@k8slens/utilities";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
import EventEmitter from "events";
|
||||
import { convertToWithIdWith, verifyRunnablesAreDAG } from "./helpers";
|
||||
import type { RunnableWithId, Runnable, Run } from "./types";
|
||||
import type { Asyncify } from "type-fest";
|
||||
|
||||
export type RunMany = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => Asyncify<Run<Param>>;
|
||||
|
||||
interface BarrierEvent {
|
||||
finish: (id: string) => void;
|
||||
}
|
||||
|
||||
class DynamicBarrier {
|
||||
private readonly finishedIds = new Map<string, Promise<void>>();
|
||||
private readonly events: TypedEventEmitter<BarrierEvent> = new EventEmitter();
|
||||
|
||||
private initFinishingPromise(id: string): Promise<void> {
|
||||
return getOrInsert(this.finishedIds, id, new Promise<void>(resolve => {
|
||||
const handler = (finishedId: string) => {
|
||||
if (finishedId === id) {
|
||||
resolve();
|
||||
this.events.removeListener("finish", handler);
|
||||
}
|
||||
};
|
||||
|
||||
this.events.addListener("finish", handler);
|
||||
}));
|
||||
}
|
||||
|
||||
setFinished(id: string): void {
|
||||
void this.initFinishingPromise(id);
|
||||
|
||||
this.events.emit("finish", id);
|
||||
}
|
||||
|
||||
async blockOn(id: string): Promise<void> {
|
||||
await this.initFinishingPromise(id);
|
||||
}
|
||||
}
|
||||
|
||||
const executeRunnableWith = <Param>(param: Param) => {
|
||||
const barrier = new DynamicBarrier();
|
||||
|
||||
return async (runnable: RunnableWithId<Param>): Promise<void> => {
|
||||
for (const parentRunnable of runnable.runAfter) {
|
||||
await barrier.blockOn(parentRunnable.id);
|
||||
}
|
||||
|
||||
await runnable.run(param);
|
||||
barrier.setFinished(runnable.id);
|
||||
};
|
||||
};
|
||||
|
||||
export function runManyFor(di: DiContainerForInjection): RunMany {
|
||||
const convertToWithId = convertToWithIdWith(di);
|
||||
|
||||
return <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => async (param: Param) => {
|
||||
const executeRunnable = executeRunnableWith(param);
|
||||
const allRunnables = di.injectManyWithMeta(injectionToken).map(x => convertToWithId(x));
|
||||
|
||||
verifyRunnablesAreDAG(injectionToken.id, allRunnables);
|
||||
|
||||
await Promise.all(allRunnables.map(executeRunnable));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import { runManySyncFor } from "./run-many-sync-for";
|
||||
import type { RunnableSync } from "./types";
|
||||
|
||||
describe("runManySyncFor", () => {
|
||||
describe("given hierarchy, when running many", () => {
|
||||
let runMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
runMock = jest.fn();
|
||||
|
||||
const someInjectionTokenForRunnables = getInjectionToken<RunnableSync>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-injectable",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-call"),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-other-injectable",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-other-call"),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable);
|
||||
|
||||
const runMany = runManySyncFor(rootDi)(someInjectionTokenForRunnables);
|
||||
|
||||
runMany();
|
||||
});
|
||||
|
||||
it("runs all runnables at the same time", () => {
|
||||
expect(runMock.mock.calls).toEqual([
|
||||
["some-call"],
|
||||
["some-other-call"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given hierarchy that is three levels deep, when running many", () => {
|
||||
let runMock: jest.Mock<(arg: string) => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = createContainer("irrelevant");
|
||||
|
||||
runMock = jest.fn();
|
||||
|
||||
const someInjectionTokenForRunnables = getInjectionToken<RunnableSync>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable1 = getInjectable({
|
||||
id: "some-injectable-1",
|
||||
instantiate: () => ({
|
||||
run: () => void runMock("third-level-run"),
|
||||
runAfter: someInjectable2,
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
const someInjectable2 = getInjectable({
|
||||
id: "some-injectable-2",
|
||||
instantiate: () => ({
|
||||
run: () => void runMock("second-level-run"),
|
||||
runAfter: someInjectable3,
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
const someInjectable3 = getInjectable({
|
||||
id: "some-injectable-3",
|
||||
instantiate: () => ({
|
||||
run: () => void runMock("first-level-run"),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnables,
|
||||
});
|
||||
|
||||
di.register(someInjectable1, someInjectable2, someInjectable3);
|
||||
|
||||
const runMany = runManySyncFor(di)(someInjectionTokenForRunnables);
|
||||
|
||||
runMany();
|
||||
});
|
||||
|
||||
it("runs runnables in order", () => {
|
||||
expect(runMock.mock.calls).toEqual([["first-level-run"], ["second-level-run"], ["third-level-run"]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("given invalid hierarchy, when running runnables, throws", () => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
const runMock = jest.fn();
|
||||
|
||||
const someInjectionToken = getInjectionToken<RunnableSync>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someOtherInjectionToken = getInjectionToken<RunnableSync>({
|
||||
id: "some-other-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-runnable-1",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-runnable-1"),
|
||||
runAfter: someOtherInjectable,
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-runnable-2",
|
||||
instantiate: () => ({
|
||||
run: () => runMock("some-runnable-2"),
|
||||
}),
|
||||
injectionToken: someOtherInjectionToken,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable);
|
||||
|
||||
const runMany = runManySyncFor(rootDi)(
|
||||
someInjectionToken,
|
||||
);
|
||||
|
||||
return expect(() => runMany()).toThrow(
|
||||
/Runnable "some-runnable-1" is unreachable for injection token "some-injection-token": run afters "some-runnable-2" are a part of different injection tokens./,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when running many with parameter", () => {
|
||||
let runMock: jest.Mock<(arg: string, arg2: string) => undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
runMock = jest.fn();
|
||||
|
||||
const someInjectionTokenForRunnablesWithParameter = getInjectionToken<
|
||||
RunnableSync<string>
|
||||
>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-runnable-1",
|
||||
instantiate: () => ({
|
||||
run: (parameter) => void runMock("run-of-some-runnable-1", parameter),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnablesWithParameter,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-runnable-2",
|
||||
instantiate: () => ({
|
||||
run: (parameter) => void runMock("run-of-some-runnable-2", parameter),
|
||||
}),
|
||||
injectionToken: someInjectionTokenForRunnablesWithParameter,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable);
|
||||
|
||||
const runMany = runManySyncFor(rootDi)(
|
||||
someInjectionTokenForRunnablesWithParameter,
|
||||
);
|
||||
|
||||
runMany("some-parameter");
|
||||
});
|
||||
|
||||
it("runs all runnables using the parameter", () => {
|
||||
expect(runMock.mock.calls).toEqual([
|
||||
["run-of-some-runnable-1", "some-parameter"],
|
||||
["run-of-some-runnable-2", "some-parameter"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable";
|
||||
import type { Disposer } from "@k8slens/utilities";
|
||||
import type { RunnableSync, RunSync, RunnableSyncWithId } from "./types";
|
||||
import { convertToWithIdWith, verifyRunnablesAreDAG } from "./helpers";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
import EventEmitter from "events";
|
||||
|
||||
export type RunManySync = <Param>(injectionToken: InjectionToken<RunnableSync<Param>, void>) => RunSync<Param>;
|
||||
|
||||
class SyncBarrier {
|
||||
private readonly finishedIds = new Set<string>();
|
||||
private readonly events: TypedEventEmitter<Record<string, () => void>> = new EventEmitter();
|
||||
|
||||
setFinished(id: string): void {
|
||||
this.finishedIds.add(id);
|
||||
this.events.emit(id);
|
||||
}
|
||||
|
||||
onceParentsAreFinished(id: string, parentIds: string[], action: () => void) {
|
||||
const finishers = new Map<string, Disposer>();
|
||||
|
||||
const checkAndRun = () => {
|
||||
if (finishers.size === 0) {
|
||||
action();
|
||||
this.setFinished(id);
|
||||
}
|
||||
};
|
||||
|
||||
for (const parentId of parentIds) {
|
||||
if (this.finishedIds.has(parentId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const onParentFinished = () => {
|
||||
this.events.removeListener(parentId, onParentFinished);
|
||||
finishers.delete(parentId);
|
||||
checkAndRun();
|
||||
};
|
||||
|
||||
finishers.set(parentId, onParentFinished);
|
||||
this.events.once(parentId, onParentFinished);
|
||||
}
|
||||
|
||||
checkAndRun();
|
||||
}
|
||||
}
|
||||
|
||||
const executeRunnableWith = <Param>(param: Param) => {
|
||||
const barrier = new SyncBarrier();
|
||||
|
||||
return (runnable: RunnableSyncWithId<Param>) => {
|
||||
barrier.onceParentsAreFinished(
|
||||
runnable.id,
|
||||
runnable.runAfter.map(r => r.id),
|
||||
() => runnable.run(param),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export function runManySyncFor(di: DiContainerForInjection): RunManySync {
|
||||
const convertToWithId = convertToWithIdWith(di);
|
||||
|
||||
return <Param>(injectionToken: InjectionToken<RunnableSync<Param>, void>) => (param: Param): undefined => {
|
||||
const executeRunnable = executeRunnableWith(param);
|
||||
const allRunnables = di.injectManyWithMeta(injectionToken).map(convertToWithId);
|
||||
|
||||
verifyRunnablesAreDAG(injectionToken.id, allRunnables);
|
||||
|
||||
allRunnables.forEach(executeRunnable);
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
import type { SingleOrMany } from "@k8slens/utilities";
|
||||
|
||||
export type Run<Param> = (parameter: Param) => Promise<void> | void;
|
||||
|
||||
export interface Runnable<T = void> {
|
||||
id?: never;
|
||||
run: Run<T>;
|
||||
readonly runAfter?: SingleOrMany<Injectable<Runnable<T>, Runnable<T>, void>>;
|
||||
}
|
||||
|
||||
export interface RunnableWithId<T> {
|
||||
run: Run<T>;
|
||||
readonly id: string;
|
||||
readonly runAfter: RunnableWithId<T>[];
|
||||
}
|
||||
|
||||
export interface RunnableSync<T = void> {
|
||||
id?: never;
|
||||
run: RunSync<T>;
|
||||
runAfter?: SingleOrMany<Injectable<RunnableSync<T>, RunnableSync<T>, void>>;
|
||||
}
|
||||
|
||||
export interface RunnableSyncWithId<T> {
|
||||
run: RunSync<T>;
|
||||
readonly id: string;
|
||||
readonly runAfter: RunnableSyncWithId<T>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: this is the worse of two evils. This makes sure that `RunnableSync` always is sync.
|
||||
* If the return type is `void` instead then async functions (those return `Promise<T>`) can
|
||||
* coerce to it.
|
||||
*/
|
||||
export type RunSync<Param> = (parameter: Param) => undefined;
|
||||
Reference in new issue
Block a user