nats-micro
Version:
NATS micro compatible extra-lightweight microservice library
289 lines (233 loc) • 7.58 kB
text/typescript
import { threadContext } from 'debug-threads-ns';
import EventEmitter from 'events';
import { Discovery } from './discovery.js';
import { Broker } from '../broker.js';
import { debug } from '../debug.js';
import { storage } from '../decorators/storage.js';
import {
Handler, MessageHandler, MicroserviceConfig, MicroserviceMethodConfig,
Request, Response,
} from '../types/index.js';
import {
errorToString, attachThreadContext, wrapMethodSafe,
} from '../utils/index.js';
import { deprecate } from 'util';
export type MicroserviceOptions = {
noStopMethod?: boolean;
};
type StartedMethod<R, T> = {
handler: MessageHandler<T>;
config: MicroserviceMethodConfig<R, T>;
};
export class Microservice {
private readonly ee = new EventEmitter();
public readonly discovery: Discovery;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly startedMethods: Record<string, StartedMethod<any, any>> = {};
constructor(
public readonly broker: Broker,
config: MicroserviceConfig | (() => MicroserviceConfig),
private readonly options?: MicroserviceOptions,
) {
this.discovery = new Discovery(
broker,
config,
{
transformConfig: this.options?.noStopMethod
? undefined
: this.addMicroserviceStopToConfig.bind(this),
},
);
}
public static async create(
broker: Broker,
config: MicroserviceConfig | (() => MicroserviceConfig),
options?: MicroserviceOptions,
): Promise<Microservice> {
const ms = new Microservice(broker, config, options);
await ms.start();
return ms;
}
public static async createFromClass<T extends object>(
broker: Broker,
target: T,
options?: MicroserviceOptions,
): Promise<Microservice> {
const config = storage.getConfig(target);
if (!config)
throw new Error('Class not found');
for (const method of Object.values(config.methods))
method.handler = method.handler.bind(target);
const service = await Microservice.create(broker, config, options);
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
(target as any)['__microservice'] = service;
return service;
}
public get id(): Readonly<string> {
return Object.freeze(this.discovery.id);
}
public get config(): Readonly<MicroserviceConfig> {
return Object.freeze(this.discovery.config);
}
public on(event: 'stop', listener: () => void): void;
public on(event: 'close', listener: () => void): void; // close is deprecated
public on(event: string, listener: () => void): void {
if (event === 'close')
deprecate(
() => this.on('stop', listener),
'close is deprecated. Use stop instead',
);
else
this.ee.on(event, listener);
}
public off(event: 'stop', listener: () => void): void;
public off(event: 'close', listener: () => void): void; // close is deprecated
public off(event: string, listener: () => void): void {
if (event === 'close')
deprecate(
() => this.off('stop', listener),
'close is deprecated. Use stop instead',
);
else
this.ee.off(event, listener);
}
private emit(event: 'stop'): void;
private emit(event: 'close'): void; // close is deprecated
private emit(event: string): void {
this.ee.emit(event);
}
private addMicroserviceStopToConfig(config: MicroserviceConfig): MicroserviceConfig {
return {
...config,
methods: {
...config.methods,
microservice_stop: {
handler: this.handleStop.bind(this),
metadata: {
'nats.micro.ext.v1.feature': 'microservice_stop',
'nats.micro.ext.v1.feature.params': `{"name":"${config.name}","id":"${this.id}"}`,
},
unbalanced: true,
local: true,
},
},
};
}
private async startMethod<R, T>(
name: string,
method: MicroserviceMethodConfig<R, T>,
): Promise<void> {
const methodWrap = wrapMethodSafe(
this.broker,
attachThreadContext(
this.discovery.id,
this.getProfiledMethodHandler(
name,
method,
),
),
{
microservice: this.config.name,
method: name,
methodConfig: method,
},
);
this.startedMethods[name] = {
handler: methodWrap,
config: method,
};
this.broker.on<R>(
this.discovery.getMethodSubject(name, method),
methodWrap,
method.unbalanced || method.local ? undefined : 'q',
);
}
private async stopMethod<R, T>(
name: string,
method: MicroserviceMethodConfig<R, T>,
): Promise<void> {
this.broker.off<R>(
this.discovery.getMethodSubject(name, method),
this.startedMethods[name].handler,
);
delete (this.startedMethods[name]);
}
public async start(): Promise<this> {
threadContext.init(this.discovery.id);
const cfg = this.discovery.config;
debug.ms.thread.info(`Starting microservice ${cfg.name}(${Object.keys(cfg.methods).join(',')})`);
for (const [name, method] of Object.entries(cfg.methods))
await this.startMethod(name, method);
await this.discovery.start();
return this;
}
public async restart(): Promise<this> {
if (!this.discovery.isStarted)
return this.start();
threadContext.init(this.discovery.id);
const cfg = this.discovery.config;
debug.ms.thread.info(`Restarting microservice ${cfg.name}(${Object.keys(cfg.methods).join(',')})`);
for (const [name, method] of Object.entries(this.startedMethods))
await this.stopMethod(name, method.config);
for (const [name, method] of Object.entries(cfg.methods))
await this.startMethod(name, method);
await this.discovery.publish();
return this;
}
private async handleStop(_req: Request<void>, res: Response<void>): Promise<void> {
await this.stop();
res.send(undefined);
}
public async stop(): Promise<this> {
threadContext.init(this.discovery.id);
const cfg = this.discovery.config;
debug.ms.thread.info(`Stopping microservice ${cfg.name}(${Object.keys(cfg.methods).join(',')})`);
for (const [name, method] of Object.entries(cfg.methods))
await this.stopMethod(name, method);
await this.discovery.stop();
this.emit('stop');
this.emit('close'); // close is deprecated
return this;
}
private getProfiledMethodHandler<T, R>(
name: string,
method: MicroserviceMethodConfig<T, R>,
): Handler<T, R> {
return async (req, res): Promise<void> => {
const start = process.hrtime.bigint();
try {
if (method.middlewares)
for (const middleware of method.middlewares) {
await middleware(req, res);
}
if (!res.isClosed) {
await method.handler(req, res);
if (method.postMiddlewares)
for (const middleware of method.postMiddlewares) {
await middleware(req, res);
}
}
res.closeWaiter.catch((err) => {
throw err;
});
res.closeWaiter.then(() => {
const elapsed = process.hrtime.bigint() - start;
this.discovery.profileMethod(
name,
undefined,
Number(elapsed),
);
});
}
catch (err) {
const elapsed = process.hrtime.bigint() - start;
this.discovery.profileMethod(
name,
errorToString(err),
Number(elapsed),
);
throw err;
}
};
}
}