@nivinjoseph/n-eda
Version:
Event Driven Architecture framework
509 lines (387 loc) • 19.8 kB
text/typescript
import { given } from "@nivinjoseph/n-defensive";
import { Container, Registry, ServiceLocator, ComponentInstaller } from "@nivinjoseph/n-ject";
import { ApplicationException, ObjectDisposedException } from "@nivinjoseph/n-exception";
import { EventBus } from "./event-bus.js";
import { EventSubMgr } from "./event-sub-mgr.js";
import { ClassDefinition, Disposable } from "@nivinjoseph/n-util";
import { EventRegistration } from "./event-registration.js";
import { Topic } from "./topic.js";
import { EdaEvent } from "./eda-event.js";
import MurmurHash from "murmurhash3js";
import { EdaEventHandler } from "./eda-event-handler.js";
import { AwsLambdaEventHandler } from "./redis-implementation/aws-lambda-event-handler.js";
import { LambdaDetails } from "./lambda-details.js";
import { RpcDetails } from "./rpc-details.js";
import { RpcEventHandler } from "./redis-implementation/rpc-event-handler.js";
import { GrpcEventHandler } from "./redis-implementation/grpc-event-handler.js";
import { GrpcDetails } from "./grpc-details.js";
import { ObserverEdaEventHandler } from "./observer-eda-event-handler.js";
import { DefaultEdaContext } from "./eda-context.js";
// import { ConsumerTracer } from "./event-handler-tracer";
// public
export class EdaManager implements Disposable
{
private readonly _container: Container;
private readonly _ownsContainer: boolean;
private readonly _topics: Array<Topic>;
private readonly _topicMap: Map<string, Topic>;
private readonly _eventMap: Map<string, EventRegistration>;
private readonly _observerEventMap: Map<string, EventRegistration>;
// private readonly _wildKeys: Array<string>;
// private _metricsEnabled = false;
private _partitionKeyMapper: (event: EdaEvent) => string = null as any;
private _eventBusRegistered = false;
private _eventSubMgrRegistered = false;
private _evtSubMgr: EventSubMgr | null = null;
private _consumerName = "UNNAMED";
private _consumerGroupId: string | null = null;
private _cleanKeys = false;
private _distributedObserverTopic: Topic | null = null;
// private _consumerTracer: ConsumerTracer | null = null;
private _awsLambdaDetails: LambdaDetails | null = null;
private _isAwsLambdaConsumer = false;
private _awsLambdaEventHandler: AwsLambdaEventHandler | null = null;
private _rpcDetails: RpcDetails | null = null;
private _isRpcConsumer = false;
private _rpcEventHandler: RpcEventHandler | null = null;
private _grpcDetails: GrpcDetails | null = null;
private _isGrpcConsumer = false;
private _grpcEventHandler: GrpcEventHandler | null = null;
private _isDisposed = false;
private _disposePromise: Promise<void> | null = null;
private _isBootstrapped = false;
public static get eventBusKey(): string { return "EventBus"; }
public static get eventSubMgrKey(): string { return "EventSubMgr"; }
public get containerRegistry(): Registry { return this._container; }
public get serviceLocator(): ServiceLocator { return this._container; }
public get topics(): ReadonlyArray<Topic> { return this._topics; }
public get distributedObserverTopic(): Topic | null { return this._distributedObserverTopic; }
public get eventMap(): ReadonlyMap<string, EventRegistration> { return this._eventMap; }
public get observerEventMap(): ReadonlyMap<string, EventRegistration> { return this._observerEventMap; }
public get consumerName(): string { return this._consumerName; }
public get consumerGroupId(): string | null { return this._consumerGroupId; }
public get cleanKeys(): boolean { return this._cleanKeys; }
// public get consumerTracer(): ConsumerTracer | null { return this._consumerTracer; }
public get awsLambdaDetails(): LambdaDetails | null { return this._awsLambdaDetails; }
public get awsLambdaProxyEnabled(): boolean { return this._awsLambdaDetails != null; }
public get isAwsLambdaConsumer(): boolean { return this._isAwsLambdaConsumer; }
public get rpcDetails(): RpcDetails | null { return this._rpcDetails; }
public get rpcProxyEnabled(): boolean { return this._rpcDetails != null; }
public get isRpcConsumer(): boolean { return this._isRpcConsumer; }
public get grpcDetails(): GrpcDetails | null { return this._grpcDetails; }
public get grpcProxyEnabled(): boolean { return this._grpcDetails != null; }
public get isGrpcConsumer(): boolean { return this._isGrpcConsumer; }
public get partitionKeyMapper(): (event: EdaEvent) => string { return this._partitionKeyMapper; }
// public get metricsEnabled(): boolean { return this._metricsEnabled; }
public constructor(container?: Container)
{
given(container as Container, "container").ensureIsObject().ensureIsType(Container);
if (container == null)
{
this._container = new Container();
this._ownsContainer = true;
}
else
{
this._container = container;
this._ownsContainer = false;
}
this._topics = new Array<Topic>();
this._topicMap = new Map<string, Topic>();
this._eventMap = new Map<string, EventRegistration>();
this._observerEventMap = new Map<string, EventRegistration>();
// this._wildKeys = new Array<string>();
this._container.registerScoped("EdaContext", DefaultEdaContext);
}
public useInstaller(installer: ComponentInstaller): this
{
given(installer, "installer").ensureHasValue().ensureIsObject();
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._container.install(installer);
return this;
}
public useConsumerName(name: string): this
{
given(name, "name").ensureHasValue().ensureIsString();
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._consumerName = name;
return this;
}
public registerTopics(...topics: Array<Topic>): this
{
given(topics, "topics").ensureHasValue().ensureIsArray();
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
for (const topic of topics)
{
const name = topic.name.toLowerCase();
if (this._topics.some(t => t.name.toLowerCase() === name))
throw new ApplicationException(`Multiple topics with the name '${name}' detected.`);
this._topics.push(topic);
}
return this;
}
// public enableMetrics(): this
// {
// given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
// this._metricsEnabled = true;
// return this;
// }
public usePartitionKeyMapper(func: (event: EdaEvent) => string): this
{
given(func, "func").ensureHasValue().ensureIsFunction();
given(this, "this")
.ensure(t => !t._partitionKeyMapper, "partition key mapper already set")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._partitionKeyMapper = func;
return this;
}
public registerEventHandlers(...eventHandlerClasses: Array<ClassDefinition<EdaEventHandler<EdaEvent>> | ClassDefinition<ObserverEdaEventHandler<EdaEvent>>>): this
{
given(eventHandlerClasses, "eventHandlerClasses").ensureHasValue().ensureIsArray();
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
for (const eventHandler of eventHandlerClasses)
{
const eventRegistration = new EventRegistration(eventHandler);
if (eventRegistration.isObservedEvent)
{
// observable, observedEvent, observer, observedEventHandler
// Need to enforce: that for one observable.observedEvent.observer combination, there is on only one handler
if (this._observerEventMap.has(eventRegistration.observationKey))
throw new ApplicationException(`Multiple observer event handlers detected for observer key '${eventRegistration.observationKey}'.`);
this._observerEventMap.set(eventRegistration.observationKey, eventRegistration);
}
else
{
// this enforces that you cannot have 2 handler classes for the same event
if (this._eventMap.has(eventRegistration.eventTypeName))
throw new ApplicationException(`Multiple event handlers detected for event '${eventRegistration.eventTypeName}'.`);
this._eventMap.set(eventRegistration.eventTypeName, eventRegistration);
}
// this enforces that you cannot have 2 handler classes with the same name
this._container.registerScoped(eventRegistration.eventHandlerTypeName, eventRegistration.eventHandlerType);
}
return this;
}
// public registerConsumerTracer(tracer: ConsumerTracer): this
// {
// given(tracer, "tracer").ensureHasValue().ensureIsFunction();
// given(this, "this")
// .ensure(t => !t._isBootstrapped, "invoking method after bootstrap")
// .ensure(t => !t._consumerTracer, "consumer tracer already set");
// this._consumerTracer = tracer;
// return this;
// }
public registerEventBus(eventBus: EventBus | ClassDefinition<EventBus>): this
{
given(eventBus, "eventBus").ensureHasValue();
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap")
.ensure(t => !t._eventBusRegistered, "event bus already registered");
if (typeof eventBus === "function")
this._container.registerSingleton(EdaManager.eventBusKey, eventBus);
else
this._container.registerInstance(EdaManager.eventBusKey, eventBus);
this._eventBusRegistered = true;
return this;
}
public registerEventSubscriptionManager(eventSubMgr: EventSubMgr | ClassDefinition<EventSubMgr>, consumerGroupId: string): this
{
given(eventSubMgr, "eventSubMgr").ensureHasValue();
given(consumerGroupId, "consumerGroupId").ensureHasValue().ensureIsString();
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap")
.ensure(t => !t._eventSubMgrRegistered, "event subscription manager already registered");
if (typeof eventSubMgr === "function")
this._container.registerSingleton(EdaManager.eventSubMgrKey, eventSubMgr);
else
this._container.registerInstance(EdaManager.eventSubMgrKey, eventSubMgr);
this._consumerGroupId = consumerGroupId.trim();
this._eventSubMgrRegistered = true;
return this;
}
public cleanUpKeys(): this
{
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._cleanKeys = true;
return this;
}
public proxyToAwsLambda(lambdaDetails: LambdaDetails): this
{
given(lambdaDetails, "lambdaDetails").ensureHasValue().ensureIsObject();
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._awsLambdaDetails = lambdaDetails;
return this;
}
public actAsAwsLambdaConsumer(handler: AwsLambdaEventHandler): this
{
given(handler, "handler").ensureHasValue().ensureIsObject().ensureIsInstanceOf(AwsLambdaEventHandler);
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._awsLambdaEventHandler = handler;
this._isAwsLambdaConsumer = true;
return this;
}
public proxyToRpc(rpcDetails: RpcDetails): this
{
given(rpcDetails, "rpcDetails").ensureHasValue().ensureIsObject();
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._rpcDetails = rpcDetails;
return this;
}
public actAsRpcConsumer(handler: RpcEventHandler): this
{
given(handler, "handler").ensureHasValue().ensureIsObject().ensureIsInstanceOf(RpcEventHandler);
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._rpcEventHandler = handler;
this._isRpcConsumer = true;
return this;
}
public proxyToGrpc(grpcDetails: GrpcDetails): this
{
given(grpcDetails, "grpcDetails").ensureHasValue().ensureIsObject();
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._grpcDetails = grpcDetails;
return this;
}
public actAsGrpcConsumer(handler: GrpcEventHandler): this
{
given(handler, "handler").ensureHasValue().ensureIsObject().ensureIsInstanceOf(GrpcEventHandler);
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._grpcEventHandler = handler;
this._isGrpcConsumer = true;
return this;
}
public enableDistributedObserver(topic: Topic): this
{
given(topic, "topic").ensureHasValue().ensureIsType(Topic);
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
const name = topic.name.toLowerCase();
if (this._topics.some(t => t.name.toLowerCase() === name))
throw new ApplicationException(`Multiple topics with the name '${name}' detected when registering distributed observer topic.`);
this._topics.push(topic);
this._distributedObserverTopic = topic;
return this;
}
public bootstrap(): void
{
if (this._isDisposed)
throw new ObjectDisposedException(this);
given(this, "this")
.ensure(t => !t._isBootstrapped, "bootstrapping more than once")
.ensure(t => t._topics.length > 0, "no topics registered")
// .ensure(t => !!t._partitionKeyMapper, "no partition key mapper set")
.ensure(t => t._eventBusRegistered, "no event bus registered")
.ensure(t => !(t._eventSubMgrRegistered && t._isAwsLambdaConsumer),
"cannot be both event subscriber and lambda consumer")
.ensure(t => !(t._eventSubMgrRegistered && t._isRpcConsumer),
"cannot be both event subscriber and rpc consumer")
.ensure(t => !(t._isAwsLambdaConsumer && t._isRpcConsumer),
"cannot be both lambda consumer and rpc consumer");
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this._partitionKeyMapper == null)
this._partitionKeyMapper = (edaEvent): string => edaEvent.partitionKey;
this._topics.map(t => this._topicMap.set(t.name, t));
if (this._ownsContainer)
this._container.bootstrap();
this._container.resolve<EventBus>(EdaManager.eventBusKey).initialize(this);
if (this._eventSubMgrRegistered)
this._container.resolve<EventSubMgr>(EdaManager.eventSubMgrKey).initialize(this);
if (this._isAwsLambdaConsumer)
this._awsLambdaEventHandler!.initialize(this);
if (this._isRpcConsumer)
this._rpcEventHandler!.initialize(this);
if (this._isGrpcConsumer)
this._grpcEventHandler!.initialize(this);
this._isBootstrapped = true;
}
public async beginConsumption(): Promise<void>
{
if (this._isDisposed)
throw new ObjectDisposedException(this);
given(this, "this")
.ensure(t => t._isBootstrapped, "not bootstrapped")
.ensure(t => t._eventSubMgrRegistered, "no EventSubMgr registered");
this._evtSubMgr = this.serviceLocator.resolve<EventSubMgr>(EdaManager.eventSubMgrKey);
await this._evtSubMgr.consume();
}
public mapToPartition(topic: string, event: EdaEvent): number
{
given(topic, "topic").ensureHasValue().ensureIsString()
.ensure(t => this._topicMap.has(t));
given(event, "event").ensureHasValue().ensureIsObject();
if (this._isDisposed)
throw new ObjectDisposedException(this);
given(this, "this")
.ensure(t => t._isBootstrapped, "not bootstrapped");
const partitionKey = this._partitionKeyMapper(event).trim();
return MurmurHash.x86.hash32(partitionKey) % this._topicMap.get(topic)!.numPartitions;
}
// public getEventRegistration(event: EdaEvent): EventRegistration | false
// {
// let eventRegistration: EventRegistration | null = null;
// if (this._eventMap.has(event.name))
// eventRegistration = this._eventMap.get(event.name) as EventRegistration;
// else
// {
// const wildKey = this._wildKeys.find(t => event.name.startsWith(t));
// if (wildKey)
// eventRegistration = this._eventMap.get(wildKey) as EventRegistration;
// }
// return eventRegistration || false;
// }
public dispose(): Promise<void>
{
if (!this._isDisposed)
{
this._isDisposed = true;
if (this._evtSubMgr != null)
this._disposePromise = this._evtSubMgr.dispose().then(() => this._container.dispose());
else
this._disposePromise = this._container.dispose();
}
return this._disposePromise!;
}
// private createEventMap(eventHandlerClasses: ReadonlyArray<Function>): Map<string, EventRegistration>
// {
// given(eventHandlerClasses, "eventHandlerClasses").ensureHasValue().ensureIsArray();
// const eventRegistrations = eventHandlerClasses.map(t => new EventRegistration(t));
// const eventMap = new Map<string, EventRegistration>();
// eventRegistrations.forEach(t =>
// {
// if (eventMap.has(t.eventTypeName))
// throw new ApplicationException(`Multiple handlers detected for event '${t.eventTypeName}'.`);
// eventMap.set(t.eventTypeName, t);
// });
// const keys = [...eventMap.keys()];
// eventMap.forEach(t =>
// {
// if (t.isWild)
// {
// const conflicts = keys.where(u => u !== t.eventTypeName && u.startsWith(t.eventTypeName));
// if (conflicts.length > 0)
// throw new ApplicationException(`Handler conflict detected between wildcard '${t.eventTypeName}' and events '${conflicts.join(",")}'.`);
// }
// this._container.registerScoped(t.eventHandlerTypeName, t.eventHandlerType);
// });
// return eventMap;
// }
// private registerBusAndMgr(eventBus: EventBus | Function, eventSubMgr: EventSubMgr | Function): void
// {
// given(eventBus, "eventBus").ensureHasValue().ensure(t => typeof t === "function" || typeof t === "object");
// given(eventSubMgr, "eventSubMgr").ensureHasValue().ensure(t => typeof t === "function" || typeof t === "object");
// if (typeof eventBus === "function")
// this._container.registerSingleton(EdaManager.eventBusKey, eventBus);
// else
// this._container.registerInstance(EdaManager.eventBusKey, eventBus);
// if (typeof eventSubMgr === "function")
// this._container.registerSingleton(EdaManager.eventSubMgrKey, eventSubMgr);
// else
// this._container.registerInstance(EdaManager.eventSubMgrKey, eventSubMgr);
// }
}