Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added closeable collection and shutdown mechanism #246

Merged
merged 8 commits into from
Jan 28, 2025
11 changes: 9 additions & 2 deletions packages/api/src/graphql/GraphqlSequencerModule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert";

import { SequencerModule } from "@proto-kit/sequencer";
import { Closeable, closeable, SequencerModule } from "@proto-kit/sequencer";
import {
ChildContainerProvider,
Configurable,
Expand Down Expand Up @@ -29,9 +29,10 @@ export interface GraphqlModulesDefintion<
config?: ModulesConfig<GraphQLModules>;
}

@closeable()
export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
extends ModuleContainer<GraphQLModules>
implements Configurable<unknown>, SequencerModule<unknown>
implements Configurable<unknown>, SequencerModule<unknown>, Closeable
{
public static from<GraphQLModules extends GraphqlModulesRecord>(
definition: GraphqlModulesDefintion<GraphQLModules>
Expand Down Expand Up @@ -93,4 +94,10 @@ export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
}
await this.graphqlServer.startServer();
}

public async close() {
if (this.graphqlServer !== undefined) {
await this.graphqlServer.close();
}
}
}
15 changes: 13 additions & 2 deletions packages/api/src/graphql/GraphqlServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,18 @@ export class GraphqlServer extends SequencerModule<GraphqlServerOptions> {
});
}

public close() {
this.server?.close();
public async close() {
if (this.server !== undefined) {
const { server } = this;

await new Promise<void>((res) => {
server.close((error) => {
if (error !== undefined) {
log.error(error);
}
res();
});
});
}
}
}
19 changes: 18 additions & 1 deletion packages/common/src/config/ModuleContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "./ConfigurableModule";
import { ChildContainerProvider } from "./ChildContainerProvider";
import { ChildContainerCreatable } from "./ChildContainerCreatable";
import { getInjectAliases } from "./injectAlias";

const errors = {
configNotSetInContainer: (moduleName: string) =>
Expand Down Expand Up @@ -228,6 +229,16 @@ export class ModuleContainer<
}
}

protected registerAliases(originalToken: string, clas: TypedClass<any>) {
const aliases = getInjectAliases(clas);

aliases.forEach((alias) =>
this.container.register(alias, {
useToken: originalToken,
})
);
}

/**
* Register modules into the current container, and registers
* a respective resolution hook in order to decorate the module
Expand All @@ -250,6 +261,8 @@ export class ModuleContainer<
{ lifecycle: Lifecycle.ContainerScoped }
);
this.onAfterModuleResolution(moduleName);

this.registerAliases(moduleName, useClass);
}
});
}
Expand Down Expand Up @@ -412,7 +425,11 @@ export class ModuleContainer<
this.container.register(key, declaration, {
lifecycle: Lifecycle.Singleton,
});
// eslint-disable-next-line sonarjs/no-duplicated-branches
this.registerAliases(
key,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
declaration.useClass as TypedClass<unknown>
);
} else if (isTokenProvider(declaration)) {
this.container.register(key, declaration, {
lifecycle: Lifecycle.Singleton,
Expand Down
70 changes: 70 additions & 0 deletions packages/common/src/config/injectAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { TypedClass } from "../types";

export const injectAliasMetadataKey = "protokit-inject-alias";

/**
* Attaches metadata to the class that the ModuleContainer can pick up
* and inject this class in the DI container under the specified aliases.
* This method supports inheritance, therefore also gets aliases defined
* on superclasses
*/
export function injectAlias(aliases: string[]) {
return (target: TypedClass<unknown>) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const superAliases = Reflect.getMetadata(
injectAliasMetadataKey,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
Object.getPrototypeOf(target)
) as string[] | undefined;

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const existingAliases = Reflect.getMetadata(
injectAliasMetadataKey,
target
) as string[] | undefined;

let allAliases = aliases;

if (superAliases !== undefined) {
allAliases = allAliases.concat(superAliases);
}
if (existingAliases !== undefined) {
allAliases = allAliases.concat(existingAliases);
}

Reflect.defineMetadata(
injectAliasMetadataKey,
allAliases.filter(
(value, index, array) => array.indexOf(value) === index
),
target
);
};
}

/**
* Marks the class to implement a certain interface T, while also attaching
* a DI-injection alias as metadata, that will be picked up by the ModuleContainer
* to allow resolving by that interface name
* @param name The name of the injection alias, convention is to use the same as the name of T
*/
export function implement<T>(name: string) {
return (
/**
* Check if the target class extends RuntimeModule, while
* also providing static config presets
*/
target: TypedClass<T>
) => {
injectAlias([name])(target);
};
}

export function getInjectAliases(target: TypedClass<unknown>): string[] {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const aliases = Reflect.getMetadata(
injectAliasMetadataKey,
target
) as string[];
return aliases ?? [];
}
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from "./compiling/AtomicCompileHelper";
export * from "./compiling/CompileRegistry";
export * from "./compiling/CompilableModule";
export * from "./compiling/services/ChildVerificationKeyService";
export * from "./config/injectAlias";
47 changes: 45 additions & 2 deletions packages/common/test/config/ModuleContainer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
ModulesRecord,
} from "../../src/config/ModuleContainer";
import { TypedClass } from "../../src/types";
import { DependencyFactory } from "../../src";
import { DependencyFactory, expectDefined } from "../../src";
import { injectAlias } from "../../src/config/injectAlias";

// module container will accept modules that extend this type
class BaseTestModule<Config> extends ConfigurableModule<Config> {}
Expand All @@ -24,6 +25,7 @@ interface TestModuleConfig {
}

@injectable()
@injectAlias(["child-alias", "multi-alias"])
class ChildModule extends BaseTestModule<NoConfig> {
public constructor(@inject("TestModule") public readonly testModule: any) {
super();
Expand All @@ -34,6 +36,7 @@ class ChildModule extends BaseTestModule<NoConfig> {
}
}

@injectAlias(["base-alias", "multi-alias"])
class TestModule
extends BaseTestModule<TestModuleConfig>
implements DependencyFactory
Expand Down Expand Up @@ -66,7 +69,11 @@ class WrongTestModule {}

class TestModuleContainer<
Modules extends TestModulesRecord,
> extends ModuleContainer<Modules> {}
> extends ModuleContainer<Modules> {
public get dependencyContainer() {
return this.container;
}
}

describe("moduleContainer", () => {
let container: TestModuleContainer<{
Expand Down Expand Up @@ -169,4 +176,40 @@ describe("moduleContainer", () => {
expect(config.testConfigProperty2).toBe(2);
expect(config.testConfigProperty3).toBe(undefined);
});

it("should resolve dependencies correctly via alias", () => {
container.configure({
TestModule: {
testConfigProperty,
},

OtherTestModule: {
otherTestConfigProperty: testConfigProperty,
},
});

container.create(() => tsyringeContainer.createChildContainer());

// Unfortunately we still need this so that the dependencies are registered
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const m1 = container.resolve("base-alias" as any);
const m2 = container.resolve("TestModule");

expectDefined(m1);
// Check if its the same reference
expect(m1).toBe(m2);

const dm1 = container.resolve("child-alias" as any) as ChildModule;
const dm2 = container.resolve("DependencyModule1");

expect(dm1.x()).toBe("dependency factory works");
expect(dm1.testModule).toBeDefined();
expect(dm1).toBe(dm2);

const multi =
container.dependencyContainer.resolveAll<BaseTestModule<unknown>>(
"multi-alias"
);
expect(multi).toHaveLength(2);
});
});
28 changes: 28 additions & 0 deletions packages/common/test/config/injectAlias.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import "reflect-metadata";
import { getInjectAliases, injectAlias } from "../../src/config/injectAlias";

@injectAlias(["foo", "bar"])
class TestClass {}

@injectAlias(["ayy"])
class TestClass2 extends TestClass {}

describe("injectAlias metadata", () => {
it("set and retrieve", () => {
expect.assertions(2);

const aliases = getInjectAliases(TestClass);

expect(aliases).toHaveLength(2);
expect(aliases).toStrictEqual(["foo", "bar"]);
});

it("recursive", () => {
expect.assertions(2);

const aliases = getInjectAliases(TestClass2);

expect(aliases).toHaveLength(3);
expect(aliases).toStrictEqual(["ayy", "foo", "bar"]);
});
});
10 changes: 9 additions & 1 deletion packages/deployment/src/queue/BullQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
InstantiatedQueue,
TaskQueue,
AbstractTaskQueue,
closeable,
} from "@proto-kit/sequencer";

import { InstantiatedBullQueue } from "./InstantiatedBullQueue";
Expand All @@ -24,9 +25,10 @@ export interface BullQueueConfig {
/**
* TaskQueue implementation for BullMQ
*/
@closeable()
export class BullQueue
extends AbstractTaskQueue<BullQueueConfig>
implements TaskQueue
implements TaskQueue, Closeable
{
private activePromise?: Promise<void>;

Expand Down Expand Up @@ -101,4 +103,10 @@ export class BullQueue
public async start() {
noop();
}

public async close() {
await this.closeQueues();

// Closing of active workers is handled by the LocalTaskWorkerModule
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe("GeneratedResolverFactoryGraphqlModule", () => {
}
});

afterAll(() => {
indexer.resolve("GraphqlServer").close();
afterAll(async () => {
await indexer.resolve("GraphqlServer").close();
});
});
2 changes: 2 additions & 0 deletions packages/persistance/src/PrismaRedisDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
SequencerModule,
StorageDependencyMinimumDependencies,
Database,
closeable,
} from "@proto-kit/sequencer";
import { ChildContainerProvider } from "@proto-kit/common";
import { PrismaClient } from "@prisma/client";
Expand All @@ -26,6 +27,7 @@ export interface PrismaRedisCombinedConfig {
}

@sequencerModule()
@closeable()
export class PrismaRedisDatabase
extends SequencerModule<PrismaRedisCombinedConfig>
implements PrismaConnection, RedisConnection, Database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("prisma integration", () => {
};

const teardown = async () => {
await appChain.sequencer.resolve("Database").close();
await appChain.sequencer.close();
};

describe("produce fuzzed block", () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/appChain/AppChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,5 +348,9 @@ export class AppChain<
.resolve(WorkerReadyModule)
.waitForReady();
}

public async close() {
await this.sequencer.close();
}
}
/* eslint-enable @typescript-eslint/consistent-type-assertions */
1 change: 1 addition & 0 deletions packages/sequencer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./mempool/private/PrivateMempool";
export * from "./sequencer/executor/Sequencer";
export * from "./sequencer/executor/Sequenceable";
export * from "./sequencer/builder/SequencerModule";
export * from "./sequencer/builder/Closeable";
export * from "./worker/flow/Flow";
export * from "./worker/flow/Task";
export * from "./worker/flow/JSONTaskSerializer";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { inject, injectable } from "tsyringe";
import { injectOptional, log } from "@proto-kit/common";
import gcd from "compute-gcd";

import { Closeable } from "../../../worker/queue/TaskQueue";
import { closeable, Closeable } from "../../../sequencer/builder/Closeable";
import { BatchProducerModule } from "../BatchProducerModule";
import { Mempool } from "../../../mempool/Mempool";
import { BlockQueue } from "../../../storage/repositories/BlockStorage";
Expand Down Expand Up @@ -32,6 +32,7 @@ export interface TimedBlockTriggerEvent extends BlockEvents {
}

@injectable()
@closeable()
export class TimedBlockTrigger
extends BlockTriggerBase<TimedBlockTriggerConfig, TimedBlockTriggerEvent>
implements BlockTrigger, Closeable
Expand Down
Loading
Loading