mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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:
1 parent
7c5faaaf1d
commit
25f37ac1d1
364 files changed
+10035
-5246
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user