Make base store non Singleton (#6690)

* Remove Singleton from BaseStore to remove global shared state

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove more usages of Singleton

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace use of legacy global execHelm with injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove last use of legacy global execHelm

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Extract BaseStore deps into constructor argument

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce method to make store migrations injectable
- Use it for ClusterStore

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch HotbarStore to injectable migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch UserStore to injectable migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move migration utils into common/utils/

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch WeblinkStore to injectable migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove dead code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error in base-store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove tests that reference lastSeenVersion
- That value is not used anywhere in code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove usage of legacy global .getInstance

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove usage of legacy global ClusterStore.getInstance

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add simple migrations dependency for stores without any preexisting migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix messed up import

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add typing to transient injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup formatting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix typing in tests to satisfy requirement to have cacheFile

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* More consistent use of BaseStore.displayName

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add catching of error while starting main application

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move initializing sentry to runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove unneeded appPathsInjectionToken
- Only had once impl, which was in common anyway

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add support for multiple "runAfter" runnables
- Needed so that several dependencies can be declared

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Use multiple runAfter support to fix crash on renderer

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove traces

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override to fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix base store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix runManyFor tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix hotbar store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix user store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override for getConfigurationFileModel to fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove overrides for configuration stores

- Now that there is an override for getConfiguration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Overhaul FS fakes with full in-memory filesystem
- This increases our confidence in fs related logic

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove use of global shared Electron.App

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add fake access support

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Handle copy as part of fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add ensureDir/Sync support to fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Use pathExistsSync instead of fsInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add createReadStream to fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add stat to fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove dead code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test failures due to incomplete overrides

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully injectable-ize BaseStore so that ApplicationBuilder tests work

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Consolidate more bootstrapping into startFrame

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move initializing CatalogCategories to runnable in bootstrap

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert contextMenuOpen initializers into runnables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert navigateForExtension init to runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make cluster state sync fully injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move init hotbar store into runnables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make LensTheme fully injectable and runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup old code from missed from previous commit

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Manually split out terminal color names and fully type LensTheme

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix old imports

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove unnecessart awaits

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove dead code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully cherry pick injectablizing custom monaco themes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix duplicate mock warning

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix incorrectly fully cherry picking new runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Complete cherry-pick of current cluster injcetablization

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix override file name

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix injecting before app paths are set up

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix injecting before app paths are set up

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix ordering of runnable and order of injection

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all renderer runnables to late-inject style
- To help fix issues around injection time

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix react-beautiful-dnd mocks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update and fix WriteJson(Sync) to fix error in tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix HotbarStore.load being called twice is being buggy

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update listing-active-helm-repositories-in-preferences snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix sidebar-and-tab-navigation-tests
- Move enabling extensions in tests to a proper location
- Fix flushing promises

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove props from dnd mock to make snapshot diffs smaller

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix import

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by overriding things that are no longer overriden by default
- NOTE: They are overridden when using ApplicationBuilder

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix hotbar store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix cluster store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension-loader tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension-discovery tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix cluster-role-dialog tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix user store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix kubeconfig sync tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix sidebar and tab tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove unused code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix pick paths import type error and simplify signature

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error in legacy ipc registration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove another use of legacy requestOpenPathPicker

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace use of legacy global PathPicker.Pick

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix usage in light of changed prop names

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix catalog tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix more type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test flakiness by removing side effects from userStore preferences

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix loading

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crash

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cherry pick updated startFrameInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add tests to verify runMany behaviour in new possible incorrect configuration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix init ordering during start frame

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix cluster state sync

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots after removing side-effects

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add override for technical test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Correctly mark currentlyInClusterFrame as causedSideEffects

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Better formatting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix behaviour regression

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add better logging

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix BaseStore sync

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update last snapshot

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override for randomBytes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make startMainApplication not an injection time side effect

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Choose better names for start-frame runnable tokens

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove duplication of code in RunManyFor

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add unit tests and fix handling empty runAfter array

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace use of mobx from runManyFor with custom barrier

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add missing test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-12-15 17:07:19 +02:00
1 parent 7c5faaaf1d
commit 25f37ac1d1
364 files changed
+10035 -5246

No files matched your search

+379 -1
View File
@@ -8,6 +8,8 @@ import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/i
import type { Runnable } from "./run-many-for";
import { runManyFor } from "./run-many-for";
import { getPromiseStatus } from "../test-utils/get-promise-status";
import { runInAction } from "mobx";
import { flushPromises } from "../test-utils/flush-promises";
describe("runManyFor", () => {
describe("given no hierarchy, when running many", () => {
@@ -223,7 +225,68 @@ describe("runManyFor", () => {
);
return expect(() => runMany()).rejects.toThrow(
/Tried to get a composite but encountered missing parent ids: "some-runnable-2".\n\nAvailable parent ids are:\n"[0-9a-z-]+",\n"some-runnable-1"/,
/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: (di) => ({
id: "some-runnable-1",
run: () => runMock("some-runnable-1"),
runAfter: [
di.inject(someOtherInjectable),
di.inject(someSecondInjectable),
],
}),
injectionToken: someInjectionToken,
});
const someSecondInjectable = getInjectable({
id: "some-runnable-2",
instantiate: () => ({
id: "some-runnable-2",
run: () => runMock("some-runnable-2"),
}),
injectionToken: someInjectionToken,
});
const someOtherInjectable = getInjectable({
id: "some-runnable-3",
instantiate: () => ({
id: "some-runnable-3",
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"/,
);
});
@@ -279,4 +342,319 @@ describe("runManyFor", () => {
]);
});
});
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: () => ({
id: "runnable-1",
run: () => runMock("runnable-1"),
}),
injectionToken: someInjectionToken,
});
const runnableTwoInjectable = getInjectable({
id: "runnable-2",
instantiate: () => ({
id: "runnable-2",
run: () => runMock("runnable-2"),
runAfter: [], // shouldn't block being called
}),
injectionToken: someInjectionToken,
});
const runnableThreeInjectable = getInjectable({
id: "runnable-3",
instantiate: (di) => ({
id: "runnable-3",
run: () => runMock("runnable-3"),
runAfter: di.inject(runnableOneInjectable),
}),
injectionToken: someInjectionToken,
});
const runnableFourInjectable = getInjectable({
id: "runnable-4",
instantiate: (di) => ({
id: "runnable-4",
run: () => runMock("runnable-4"),
runAfter: [di.inject(runnableThreeInjectable)], // should be the same as an single item
}),
injectionToken: someInjectionToken,
});
const runnableFiveInjectable = getInjectable({
id: "runnable-5",
instantiate: (di) => ({
id: "runnable-5",
run: () => runMock("runnable-5"),
runAfter: di.inject(runnableThreeInjectable),
}),
injectionToken: someInjectionToken,
});
const runnableSixInjectable = getInjectable({
id: "runnable-6",
instantiate: (di) => ({
id: "runnable-6",
run: () => runMock("runnable-6"),
runAfter: [
di.inject(runnableFourInjectable),
di.inject(runnableFiveInjectable),
],
}),
injectionToken: someInjectionToken,
});
const runnableSevenInjectable = getInjectable({
id: "runnable-7",
instantiate: (di) => ({
id: "runnable-7",
run: () => runMock("runnable-7"),
runAfter: [
di.inject(runnableFiveInjectable),
di.inject(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);
});
});
});
});
+117 -25
View File
@@ -3,46 +3,138 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable";
import type { Composite } from "../utils/composite/get-composite/get-composite";
import { getCompositeFor } from "../utils/composite/get-composite/get-composite";
import type { SingleOrMany } from "../utils";
import { getOrInsert, getOrInsertSetFor, isDefined } from "../utils";
import * as uuid from "uuid";
import assert from "assert";
import type { Asyncify } from "type-fest";
import type TypedEventEmitter from "typed-emitter";
import EventEmitter from "events";
export interface Runnable<TParameter = void> {
id: string;
run: Run<TParameter>;
runAfter?: Runnable<TParameter>;
runAfter?: SingleOrMany<Runnable<TParameter>>;
}
type Run<Param> = (parameter: Param) => Promise<void> | void;
export type RunMany = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => Run<Param>;
export type RunMany = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => Asyncify<Run<Param>>;
async function runCompositeRunnables<Param>(param: Param, composite: Composite<Runnable<Param>>) {
await composite.value.run(param);
await Promise.all(composite.children.map(composite => runCompositeRunnables(param, composite)));
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);
}
};
const verifyRunnablesAreDAG = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>, runnables: Runnable<Param>[]) => {
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 || (Array.isArray(runnable.runAfter) && runnable.runAfter.length === 0)) {
addRunnableId(rootId).add(runnable.id);
} else if (Array.isArray(runnable.runAfter)) {
for (const parentRunnable of runnable.runAfter) {
addRunnableId(parentRunnable.id).add(runnable.id);
}
} else {
addRunnableId(runnable.runAfter.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 "${injectionToken.id}"`);
}
const runAfters = [runnable.runAfter]
.flat()
.filter(isDefined)
.map(runnable => runnable.id)
.join('", "');
throw new Error(`Runnable "${id}" is unreachable for injection token "${injectionToken.id}": run afters "${runAfters}" are a part of different injection tokens.`);
}
}
};
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(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: Runnable<Param>): Promise<void> => {
const parentRunnables = [runnable.runAfter].flat().filter(isDefined);
for (const parentRunnable of parentRunnables) {
await barrier.blockOn(parentRunnable.id);
}
await runnable.run(param);
barrier.setFinished(runnable.id);
};
};
export function runManyFor(di: DiContainerForInjection): RunMany {
return <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => async (param: Param) => {
const executeRunnable = executeRunnableWith(param);
const allRunnables = di.injectMany(injectionToken);
const rootId = uuid.v4();
const getCompositeRunnables = getCompositeFor<Runnable<Param>>({
getId: (runnable) => runnable.id,
getParentId: (runnable) => (
runnable.id === rootId
? undefined
: runnable.runAfter?.id ?? rootId
),
});
const composite = getCompositeRunnables([
// This is a dummy runnable to conform to the requirements of `getCompositeFor` to only have one root
{
id: rootId,
run: () => {},
},
...allRunnables,
]);
await runCompositeRunnables(param, composite);
verifyRunnablesAreDAG(injectionToken, allRunnables);
await Promise.all(allRunnables.map(executeRunnable));
};
}