@lad-tech/nsc-toolkit 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +105 -0
- package/.github/workflows/publish-package-to-npmjs.yml +18 -0
- package/.prettierrc.json +9 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/Client.js +198 -0
- package/dist/Client.js.map +1 -0
- package/dist/Method.js +7 -0
- package/dist/Method.js.map +1 -0
- package/dist/Root.js +66 -0
- package/dist/Root.js.map +1 -0
- package/dist/Service.js +387 -0
- package/dist/Service.js.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/injector.js +39 -0
- package/dist/injector.js.map +1 -0
- package/dist/interfaces.js +4 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/types/Client.d.ts +25 -0
- package/dist/types/Method.d.ts +6 -0
- package/dist/types/Root.d.ts +26 -0
- package/dist/types/Service.d.ts +78 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/injector.d.ts +11 -0
- package/dist/types/interfaces.d.ts +72 -0
- package/examples/HttpGate/index.ts +61 -0
- package/examples/HttpGate/package-lock.json +835 -0
- package/examples/HttpGate/package.json +15 -0
- package/examples/LogicService/index.ts +16 -0
- package/examples/LogicService/interfaces.ts +10 -0
- package/examples/LogicService/methods/WeirdSum.ts +20 -0
- package/examples/LogicService/service.json +31 -0
- package/examples/LogicService/service.ts +15 -0
- package/examples/MathService/Untitled-1.json +62 -0
- package/examples/MathService/index.ts +22 -0
- package/examples/MathService/interfaces.ts +26 -0
- package/examples/MathService/methods/Fibonacci.ts +29 -0
- package/examples/MathService/methods/Sum.ts +16 -0
- package/examples/MathService/methods/SumStream.ts +18 -0
- package/examples/MathService/service.json +64 -0
- package/examples/MathService/service.ts +18 -0
- package/examples/SimpleCache.ts +21 -0
- package/examples/misc/trace_1.png +0 -0
- package/examples/misc/trace_2.png +0 -0
- package/package.json +41 -0
- package/src/Client.ts +237 -0
- package/src/Method.ts +7 -0
- package/src/Root.ts +69 -0
- package/src/Service.ts +419 -0
- package/src/index.ts +5 -0
- package/src/injector.ts +43 -0
- package/src/interfaces.ts +89 -0
- package/tsconfig.json +19 -0
package/src/Method.ts
ADDED
package/src/Root.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Baggage, CacheService } from './interfaces';
|
|
2
|
+
import * as opentelemetry from '@opentelemetry/api';
|
|
3
|
+
import { NatsConnection } from 'nats';
|
|
4
|
+
import { Logs } from '@lad-tech/toolbelt';
|
|
5
|
+
|
|
6
|
+
export class Root {
|
|
7
|
+
protected SERVICE_SUBJECT_FOR_GET_HTTP_SETTINGS = 'get_http_settings';
|
|
8
|
+
protected CACHE_SERVICE_KEY = 'CACHE';
|
|
9
|
+
protected
|
|
10
|
+
protected logger: Logs.Logger;
|
|
11
|
+
|
|
12
|
+
constructor(protected brocker: NatsConnection, cache?: CacheService) {
|
|
13
|
+
this.logger = new Logs.Logger();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected castToNumber(value?: string) {
|
|
17
|
+
const result = +value!;
|
|
18
|
+
if (isNaN(result)) {
|
|
19
|
+
throw new Error(`Невозможно привести значение ${value} к числу`);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected getSettingFromEnv(name: string, required = true) {
|
|
25
|
+
const value = process.env[name];
|
|
26
|
+
if (!value && required) {
|
|
27
|
+
throw new Error(`Не установлена обязательная настройка: ${name}`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Make opentelemetry context from baggagge
|
|
34
|
+
*/
|
|
35
|
+
protected getContext(baggage?: Baggage) {
|
|
36
|
+
if (baggage) {
|
|
37
|
+
return opentelemetry.trace.setSpanContext(opentelemetry.context.active(), baggage);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected getExpired(expired?: number, ownTimeout?: number) {
|
|
42
|
+
try {
|
|
43
|
+
if (!expired) {
|
|
44
|
+
const timeout = ownTimeout || this.castToNumber(this.getSettingFromEnv('DEFAULT_REPONSE_TIMEOUT'));
|
|
45
|
+
return Date.now() + timeout * 1000;
|
|
46
|
+
}
|
|
47
|
+
return expired;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(error);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected buildErrorMessage(error: string | Error, code?: number) {
|
|
55
|
+
let message = '';
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
message = error.message;
|
|
58
|
+
} else {
|
|
59
|
+
message = error;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
payload: null,
|
|
63
|
+
error: {
|
|
64
|
+
message,
|
|
65
|
+
code,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/Service.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { Root } from './Root';
|
|
2
|
+
import { JSONCodec, Subscription } from 'nats';
|
|
3
|
+
import { Message, Emitter, Method, ServiceOptions, Baggage, ExternalBaggage, ClientService } from './interfaces';
|
|
4
|
+
import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
5
|
+
import { Resource } from '@opentelemetry/resources';
|
|
6
|
+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
|
7
|
+
import { Tracer, Context, Span, trace } from '@opentelemetry/api';
|
|
8
|
+
import { InstanceContainer, ServiceContainer } from './injector';
|
|
9
|
+
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
|
|
10
|
+
import { IncomingHttpHeaders, ServerResponse } from 'http';
|
|
11
|
+
import { Readable, Transform, pipeline } from 'stream';
|
|
12
|
+
import { Logs } from '@lad-tech/toolbelt';
|
|
13
|
+
import * as http from 'http';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
|
|
16
|
+
export class Service<E extends Emitter = {}> extends Root {
|
|
17
|
+
public emitter = {} as E;
|
|
18
|
+
private serviceName: string;
|
|
19
|
+
private httpServer?: http.Server;
|
|
20
|
+
protected httpPort?: number;
|
|
21
|
+
protected ipAddress?: string;
|
|
22
|
+
private subscriptions: Subscription[] = [];
|
|
23
|
+
private httpMethods = new Map<string, Method>();
|
|
24
|
+
private rootSpans = new Map<string, Span>();
|
|
25
|
+
|
|
26
|
+
constructor(private options: ServiceOptions<E>) {
|
|
27
|
+
super(options.brokerConnection, options.cache?.service);
|
|
28
|
+
|
|
29
|
+
this.serviceName = options.name;
|
|
30
|
+
this.logger.setLocation(this.serviceName);
|
|
31
|
+
if (options.events.length) {
|
|
32
|
+
this.emitter = options.events.reduce((result, action) => {
|
|
33
|
+
result[action] = ((params: unknown) => {
|
|
34
|
+
this.brocker.publish(`${options.name}.${String(action)}`, this.buildMessage(params));
|
|
35
|
+
}) as E[keyof E];
|
|
36
|
+
return result;
|
|
37
|
+
}, this.emitter);
|
|
38
|
+
}
|
|
39
|
+
this.createTracer();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create global Tracer
|
|
44
|
+
*/
|
|
45
|
+
private createTracer() {
|
|
46
|
+
const provider = new BasicTracerProvider({
|
|
47
|
+
resource: new Resource({
|
|
48
|
+
[SemanticResourceAttributes.SERVICE_NAME]: this.options.name,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
const exporter = new JaegerExporter({
|
|
52
|
+
endpoint: this.getSettingFromEnv('OTEL_AGENT', false),
|
|
53
|
+
});
|
|
54
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
|
|
55
|
+
provider.register();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrapper for async methods. Create span
|
|
60
|
+
*/
|
|
61
|
+
private async perform(
|
|
62
|
+
func: (...args: unknown[]) => Promise<unknown>,
|
|
63
|
+
funcContext: unknown,
|
|
64
|
+
arg: unknown[],
|
|
65
|
+
tracer: Tracer,
|
|
66
|
+
context?: Context,
|
|
67
|
+
) {
|
|
68
|
+
const span = tracer.startSpan(func.name, undefined, context);
|
|
69
|
+
const query = func.apply(funcContext, arg);
|
|
70
|
+
query
|
|
71
|
+
.then(() => span.end())
|
|
72
|
+
.catch(error => {
|
|
73
|
+
span.setAttribute('error', true);
|
|
74
|
+
span.setAttribute('error.kind', error);
|
|
75
|
+
span.end();
|
|
76
|
+
throw error;
|
|
77
|
+
});
|
|
78
|
+
return query;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creating an object to inject into Method (business logic)
|
|
83
|
+
*/
|
|
84
|
+
private createObjectWithDependencies(action: string, tracer: Tracer, baggage?: Baggage) {
|
|
85
|
+
const services = ServiceContainer.get(action);
|
|
86
|
+
const dependences: Record<string, unknown> = {};
|
|
87
|
+
if (services?.size) {
|
|
88
|
+
services.forEach((Dependence, key) => {
|
|
89
|
+
dependences[key] = new Dependence(this.brocker, baggage, this.options.cache);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const perform = this.perform;
|
|
93
|
+
const context = this.getContext(baggage);
|
|
94
|
+
const instances = InstanceContainer.get(action);
|
|
95
|
+
if (instances?.size) {
|
|
96
|
+
instances.forEach((instance, key) => {
|
|
97
|
+
const trap = {
|
|
98
|
+
get(target: any, propKey: string, receiver: any) {
|
|
99
|
+
const method = Reflect.get(target, propKey, receiver);
|
|
100
|
+
if (typeof method === 'function') {
|
|
101
|
+
return function (...args: unknown[]) {
|
|
102
|
+
return perform(method, instance, args, tracer, context);
|
|
103
|
+
};
|
|
104
|
+
} else {
|
|
105
|
+
return method;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
dependences[key] = new Proxy(instance, trap);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dependences['logger'] = new Logs.Logger({ location: `${this.serviceName}.${action}`, metadata: baggage });
|
|
114
|
+
return dependences;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create Method (business logic) context
|
|
119
|
+
*/
|
|
120
|
+
private createMethodContext(Method: Method, dependencies: Record<string, unknown>) {
|
|
121
|
+
const context = new Method();
|
|
122
|
+
for (const key in dependencies) {
|
|
123
|
+
context[key] = dependencies[key];
|
|
124
|
+
}
|
|
125
|
+
context['emitter'] = this.emitter;
|
|
126
|
+
return context;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create Baggage from span. Expired one-on-one business logic call
|
|
131
|
+
*/
|
|
132
|
+
private getNextBaggage(span: Span, baggage?: Baggage) {
|
|
133
|
+
const { traceId, spanId, traceFlags } = span.spanContext();
|
|
134
|
+
return { traceId, spanId, traceFlags, expired: baggage?.expired };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* If there is no baggage. For example, in HTTP Gateway
|
|
139
|
+
*/
|
|
140
|
+
public getRootBaggage(subject: string, headers?: ExternalBaggage, ownTimeout?: number) {
|
|
141
|
+
const baggage = headers ? this.getBaggageFromHTTPHeader(headers) : undefined;
|
|
142
|
+
const tracer = trace.getTracer('');
|
|
143
|
+
const context = this.getContext(baggage);
|
|
144
|
+
const span = tracer.startSpan(subject, undefined, context);
|
|
145
|
+
const newBaggage = this.getNextBaggage(span, baggage);
|
|
146
|
+
this.rootSpans.set(newBaggage.traceId, span);
|
|
147
|
+
return {
|
|
148
|
+
...newBaggage,
|
|
149
|
+
expired: this.getExpired(undefined, ownTimeout),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* End root baggage
|
|
155
|
+
*/
|
|
156
|
+
public endRootSpan(traceId: string, error?: Error) {
|
|
157
|
+
const span = this.rootSpans.get(traceId);
|
|
158
|
+
if (span) {
|
|
159
|
+
if (error) {
|
|
160
|
+
span.setAttribute('error', true);
|
|
161
|
+
span.setAttribute('error.kind', error.message);
|
|
162
|
+
}
|
|
163
|
+
span.end();
|
|
164
|
+
this.rootSpans.delete(traceId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public buildService<C extends ClientService>(Client: C, baggage?: Baggage) {
|
|
169
|
+
return new Client(this.brocker, baggage, this.options.cache) as InstanceType<C>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create service Method for send HTTP settings
|
|
174
|
+
*/
|
|
175
|
+
private async runServiceMethodForHttp() {
|
|
176
|
+
const subject = `${this.serviceName}.${this.SERVICE_SUBJECT_FOR_GET_HTTP_SETTINGS}`;
|
|
177
|
+
const subscription = this.brocker.subscribe(subject, { queue: this.serviceName });
|
|
178
|
+
this.subscriptions.push(subscription);
|
|
179
|
+
for await (const message of subscription) {
|
|
180
|
+
message.respond(this.buildMessage(this.getHttpSettings()));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private makeHttpSingleResponse(response: ServerResponse, data: Message) {
|
|
185
|
+
const responseData = JSON.stringify(data);
|
|
186
|
+
response
|
|
187
|
+
.writeHead(200, {
|
|
188
|
+
'Content-Length': Buffer.byteLength(responseData),
|
|
189
|
+
'Content-Type': 'application/json',
|
|
190
|
+
})
|
|
191
|
+
.write(responseData);
|
|
192
|
+
response.end();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create transform stream for convert object to string in stream pipeline
|
|
197
|
+
*/
|
|
198
|
+
private getStringifyTransform() {
|
|
199
|
+
return new Transform({
|
|
200
|
+
objectMode: true,
|
|
201
|
+
transform(chunk, encoding, push) {
|
|
202
|
+
try {
|
|
203
|
+
if (chunk instanceof Buffer) {
|
|
204
|
+
push(null, chunk);
|
|
205
|
+
}
|
|
206
|
+
const result = JSON.stringify(chunk);
|
|
207
|
+
push(null, result);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
push(error);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private makeHttpStreamReponse(response: ServerResponse, data: Message<Readable>) {
|
|
216
|
+
data.payload.on('error', error => {
|
|
217
|
+
this.logger.error(error);
|
|
218
|
+
});
|
|
219
|
+
response.writeHead(200, {
|
|
220
|
+
'Content-Type': 'application/octet-stream',
|
|
221
|
+
});
|
|
222
|
+
pipeline(data.payload, this.getStringifyTransform(), response, () => {});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Up HTTP server and start listen http routes
|
|
227
|
+
*/
|
|
228
|
+
private async buildHTTPHandlers() {
|
|
229
|
+
await this.upHTTPServer();
|
|
230
|
+
this.runServiceMethodForHttp();
|
|
231
|
+
this.httpServer!.on('request', async (request, response) => {
|
|
232
|
+
try {
|
|
233
|
+
if (request.method !== 'POST' || !request.url) {
|
|
234
|
+
throw new Error('Wrong http request');
|
|
235
|
+
}
|
|
236
|
+
const parsedUrl = request.url.split('/');
|
|
237
|
+
parsedUrl.shift();
|
|
238
|
+
const [serviceName, action, ...other] = parsedUrl;
|
|
239
|
+
const wrongServiceName = this.serviceName !== serviceName;
|
|
240
|
+
const Method = this.httpMethods.get(action);
|
|
241
|
+
if (other.length || wrongServiceName || !Method) {
|
|
242
|
+
throw new Error('Wrong url or service name or action');
|
|
243
|
+
}
|
|
244
|
+
const baggage = this.getBaggageFromHTTPHeader(request.headers as IncomingHttpHeaders & ExternalBaggage);
|
|
245
|
+
|
|
246
|
+
if (Method.settings.options?.useStream?.request) {
|
|
247
|
+
const result = await this.handled(request, Method, baggage);
|
|
248
|
+
if (Method.settings.options.useStream.response && result.payload instanceof Readable) {
|
|
249
|
+
this.makeHttpStreamReponse(response, result as Message<Readable>);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.makeHttpSingleResponse(response, result);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const requestDataRaw: Buffer[] = [];
|
|
256
|
+
for await (const data of request) {
|
|
257
|
+
requestDataRaw.push(data);
|
|
258
|
+
}
|
|
259
|
+
const requestData = Buffer.concat(requestDataRaw).toString();
|
|
260
|
+
const result = await this.handled(JSON.parse(requestData), Method, baggage);
|
|
261
|
+
if (Method.settings.options?.useStream?.response && result.payload instanceof Readable) {
|
|
262
|
+
this.makeHttpStreamReponse(response, result as Message<Readable>);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
this.makeHttpSingleResponse(response, result);
|
|
266
|
+
} catch (error: unknown) {
|
|
267
|
+
this.logger.error(error);
|
|
268
|
+
if (error instanceof Error) {
|
|
269
|
+
this.makeHttpSingleResponse(response, this.buildErrorMessage(error));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.makeHttpSingleResponse(response, this.buildErrorMessage('System unknown error'));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Run business logic for request
|
|
279
|
+
*/
|
|
280
|
+
private async handled(payload: unknown, Method: Method, baggage?: Baggage): Promise<Message> {
|
|
281
|
+
const subject = `${this.serviceName}.${Method.settings.action}`;
|
|
282
|
+
const tracer = trace.getTracer('');
|
|
283
|
+
|
|
284
|
+
const context = this.getContext(baggage);
|
|
285
|
+
const span = tracer.startSpan(subject, undefined, context);
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const requestedDependencies = this.createObjectWithDependencies(
|
|
289
|
+
Method.settings.action,
|
|
290
|
+
tracer,
|
|
291
|
+
this.getNextBaggage(span, baggage),
|
|
292
|
+
);
|
|
293
|
+
const context = this.createMethodContext(Method, requestedDependencies);
|
|
294
|
+
const result = {
|
|
295
|
+
payload: await context.handler.call(context, payload),
|
|
296
|
+
};
|
|
297
|
+
span.end();
|
|
298
|
+
return result;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.logger.error(error);
|
|
301
|
+
span.setAttribute('error', true);
|
|
302
|
+
span.setAttribute('error.kind', error);
|
|
303
|
+
span.end();
|
|
304
|
+
return this.buildErrorMessage(error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Start service. Subscribe for subject and up http server
|
|
310
|
+
*/
|
|
311
|
+
public async start() {
|
|
312
|
+
const { methods } = this.options;
|
|
313
|
+
try {
|
|
314
|
+
methods.forEach(async Method => {
|
|
315
|
+
if (Method.settings.options?.useStream) {
|
|
316
|
+
this.httpMethods.set(Method.settings.action, Method);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const subject = `${this.serviceName}.${Method.settings.action}`;
|
|
321
|
+
|
|
322
|
+
const subscription = this.brocker.subscribe(subject, { queue: this.serviceName });
|
|
323
|
+
this.subscriptions.push(subscription);
|
|
324
|
+
for await (const message of subscription) {
|
|
325
|
+
const { payload, baggage } = JSONCodec<Message<unknown>>().decode(message.data);
|
|
326
|
+
try {
|
|
327
|
+
const result = await this.handled(payload, Method, baggage);
|
|
328
|
+
message.respond(this.buildMessage(result));
|
|
329
|
+
} catch (error) {
|
|
330
|
+
message.respond(this.buildMessage({ error: error.message }));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
if (this.httpMethods.size > 0) {
|
|
335
|
+
await this.buildHTTPHandlers();
|
|
336
|
+
}
|
|
337
|
+
this.logger.info('Service successfully started!');
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error instanceof Error) {
|
|
340
|
+
this.logger.error(error.name, error.message);
|
|
341
|
+
} else {
|
|
342
|
+
this.logger.error('An error occurred while starting the service', error);
|
|
343
|
+
}
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Build message for broker
|
|
350
|
+
*/
|
|
351
|
+
private buildMessage(message: unknown) {
|
|
352
|
+
return Buffer.from(JSON.stringify(message));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private async upHTTPServer() {
|
|
356
|
+
this.httpServer = http.createServer();
|
|
357
|
+
this.ipAddress = this.getMyIpV4();
|
|
358
|
+
this.httpPort = await new Promise((resolve, reject) => {
|
|
359
|
+
this.httpServer = this.httpServer!.listen(0, () => {
|
|
360
|
+
const address = this.httpServer!.address();
|
|
361
|
+
|
|
362
|
+
if (!address) {
|
|
363
|
+
reject(new Error('Failed to get the port number: server is not listening'));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (typeof address === 'string') {
|
|
368
|
+
reject(new Error('Listening on a unix socket is not supported'));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
resolve(address.port);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
return this.httpServer!;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private getMyIpV4() {
|
|
379
|
+
const networkInterfaces = os.networkInterfaces();
|
|
380
|
+
const myIpV4Address = Object.keys(networkInterfaces).reduce((ip, key) => {
|
|
381
|
+
if (ip) {
|
|
382
|
+
return ip;
|
|
383
|
+
}
|
|
384
|
+
const networkInterface = networkInterfaces[key];
|
|
385
|
+
const externalIpV4Interface = networkInterface?.find(item => !item.internal && item.family === 'IPv4');
|
|
386
|
+
if (externalIpV4Interface) {
|
|
387
|
+
return externalIpV4Interface.address;
|
|
388
|
+
}
|
|
389
|
+
return ip;
|
|
390
|
+
}, '');
|
|
391
|
+
if (!myIpV4Address) {
|
|
392
|
+
throw new Error('Failed to get service ip address');
|
|
393
|
+
}
|
|
394
|
+
return myIpV4Address;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private getHttpSettings() {
|
|
398
|
+
return {
|
|
399
|
+
ip: this.ipAddress,
|
|
400
|
+
port: this.httpPort,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private getBaggageFromHTTPHeader(headers: ExternalBaggage) {
|
|
405
|
+
const expired = headers['nsc-expired'] ? +headers['nsc-expired'] : undefined;
|
|
406
|
+
const traceId = headers['nsc-trace-id'];
|
|
407
|
+
const spanId = headers['nsc-span-id'];
|
|
408
|
+
const traceFlags = headers['nsc-trace-flags'] ? +headers['nsc-trace-flags'] : undefined;
|
|
409
|
+
if (traceId && spanId && traceFlags) {
|
|
410
|
+
return {
|
|
411
|
+
traceId,
|
|
412
|
+
spanId,
|
|
413
|
+
traceFlags,
|
|
414
|
+
expired,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
}
|
package/src/index.ts
ADDED
package/src/injector.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { Method, ClientService } from './interfaces';
|
|
3
|
+
|
|
4
|
+
export type Instance = Record<string, (props: unknown) => Promise<unknown>>;
|
|
5
|
+
export type Dependence = ClientService<unknown>;
|
|
6
|
+
export type DependenceStorage = Map<string, Dependence>;
|
|
7
|
+
export type InstanceStorage = Map<string, Instance>;
|
|
8
|
+
|
|
9
|
+
const serviceMetaKey = Symbol('services');
|
|
10
|
+
const instanceMetaKey = Symbol('instance');
|
|
11
|
+
|
|
12
|
+
export const ServiceContainer: Map<string, DependenceStorage> = new Map();
|
|
13
|
+
export const InstanceContainer: Map<string, InstanceStorage> = new Map();
|
|
14
|
+
|
|
15
|
+
export function related<T extends Method>(target: T) {
|
|
16
|
+
const dependencies: DependenceStorage = Reflect.getMetadata(serviceMetaKey, target.prototype);
|
|
17
|
+
const instances: InstanceStorage = Reflect.getMetadata(instanceMetaKey, target.prototype);
|
|
18
|
+
ServiceContainer.set(target.settings.action, dependencies);
|
|
19
|
+
InstanceContainer.set(target.settings.action, instances);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setMetaData(item: Dependence | Instance, itemName: string, metaKey: symbol, target: any) {
|
|
23
|
+
let storage: Map<string, unknown>;
|
|
24
|
+
if (Reflect.hasMetadata(metaKey, target)) {
|
|
25
|
+
storage = Reflect.getMetadata(metaKey, target);
|
|
26
|
+
} else {
|
|
27
|
+
storage = new Map();
|
|
28
|
+
Reflect.defineMetadata(metaKey, storage, target);
|
|
29
|
+
}
|
|
30
|
+
storage.set(itemName, item);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function service(dependence: Dependence) {
|
|
34
|
+
return function (target: any, dependenceName: string): void {
|
|
35
|
+
setMetaData(dependence, dependenceName, serviceMetaKey, target);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function instance(instance: Instance) {
|
|
40
|
+
return function (target: any, instanceName: string): void {
|
|
41
|
+
setMetaData(instance, instanceName, instanceMetaKey, target);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// COMMON
|
|
2
|
+
|
|
3
|
+
import type { NatsConnection } from 'nats';
|
|
4
|
+
import type { Client } from './Client';
|
|
5
|
+
|
|
6
|
+
export interface MethodOptions {
|
|
7
|
+
useStream?: {
|
|
8
|
+
request?: boolean;
|
|
9
|
+
response?: boolean;
|
|
10
|
+
};
|
|
11
|
+
cache?: number; // in minuts
|
|
12
|
+
timeout?: number; // in seconds
|
|
13
|
+
runTimeValidation?: {
|
|
14
|
+
request?: boolean;
|
|
15
|
+
response?: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MethodSettings {
|
|
20
|
+
action: string;
|
|
21
|
+
options?: MethodOptions;
|
|
22
|
+
request?: Record<string, unknown>;
|
|
23
|
+
response?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Method {
|
|
27
|
+
settings: MethodSettings;
|
|
28
|
+
new (): { handler: (params: unknown) => Promise<unknown> };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ClientService<C = Client> = new (
|
|
32
|
+
natsConnection: NatsConnection,
|
|
33
|
+
baggage?: Baggage,
|
|
34
|
+
cache?: CacheSettings,
|
|
35
|
+
) => C;
|
|
36
|
+
|
|
37
|
+
export type Baggage = {
|
|
38
|
+
traceId: string;
|
|
39
|
+
spanId: string;
|
|
40
|
+
traceFlags: number;
|
|
41
|
+
expired?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ExternalBaggage = {
|
|
45
|
+
'nsc-expired'?: number;
|
|
46
|
+
'nsc-trace-id'?: string;
|
|
47
|
+
'nsc-span-id'?: string;
|
|
48
|
+
'nsc-trace-flags'?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface Message<M = any> {
|
|
52
|
+
payload: M;
|
|
53
|
+
baggage?: Baggage;
|
|
54
|
+
error?: {
|
|
55
|
+
message: string;
|
|
56
|
+
code?: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type Emitter = Record<string, (params: unknown) => void>;
|
|
61
|
+
|
|
62
|
+
export interface CacheSettings {
|
|
63
|
+
service: CacheService;
|
|
64
|
+
timeout: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ServiceOptions<E extends Emitter> {
|
|
68
|
+
name: string;
|
|
69
|
+
brokerConnection: NatsConnection;
|
|
70
|
+
methods: Method[];
|
|
71
|
+
events: keyof E extends [] ? [] : [keyof E];
|
|
72
|
+
cache?: CacheSettings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Listener<E extends Emitter> {
|
|
76
|
+
on<A extends keyof E>(action: A, handler: E[A]): void;
|
|
77
|
+
off<A extends keyof E>(action: A, handler: E[A]): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface HttpSettings {
|
|
81
|
+
ip?: string;
|
|
82
|
+
port?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface CacheService {
|
|
86
|
+
set: (key: string, value: string, expired?: number) => Promise<void>;
|
|
87
|
+
get: (key: string) => Promise<string | undefined>;
|
|
88
|
+
delete: (key: string) => Promise<void>;
|
|
89
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"sourceMap": true,
|
|
5
|
+
"strictNullChecks": true,
|
|
6
|
+
"module": "commonjs",
|
|
7
|
+
"target": "es2019",
|
|
8
|
+
"allowJs": false,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"experimentalDecorators": true,
|
|
12
|
+
"emitDecoratorMetadata": true,
|
|
13
|
+
"lib": ["es2019"],
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"declarationDir": "./dist/types/"
|
|
16
|
+
},
|
|
17
|
+
"include": ["."],
|
|
18
|
+
"exclude": ["node_modules","./examples"]
|
|
19
|
+
}
|