imean-service-engine 1.0.0 → 1.2.0
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/README.md +10 -13
- package/dist/mod.cjs +104 -85
- package/dist/mod.d.cts +2 -0
- package/dist/mod.d.ts +2 -0
- package/dist/mod.js +103 -85
- package/package.json +84 -74
package/README.md
CHANGED
@@ -1,9 +1,7 @@
|
|
1
|
-
# Microservice Framework
|
1
|
+
# Microservice Framework
|
2
2
|
|
3
3
|
一个轻量级的 TypeScript 微服务框架。提供了类型安全、自动客户端生成、请求重试等特性。
|
4
4
|
|
5
|
-
[](https://jsr.io/@imean/service-engine)
|
6
|
-
|
7
5
|
## 特性
|
8
6
|
|
9
7
|
- 📝 完全的 TypeScript 支持
|
@@ -14,7 +12,6 @@
|
|
14
12
|
- 🌟 优雅的装饰器 API
|
15
13
|
- 🚦 优雅停机支持
|
16
14
|
- 📡 生成基于 fetch 的客户端代码,可以在 Deno 、Node.js、Bun 以及浏览器中使用
|
17
|
-
- 🌟 服务间调用可以利用 Deno 的分布式模块引入快速集成生成的客户端
|
18
15
|
- 🌟 支持 Stream 流传输,客户端使用 AsyncIterator 迭代
|
19
16
|
- 🌟 服务引擎支持通过 WebSocket 进行实时通信,相比 HTTP 请求具有以下优势:
|
20
17
|
- 保持长连接,减少连接建立的开销
|
@@ -30,7 +27,7 @@
|
|
30
27
|
## 安装
|
31
28
|
|
32
29
|
```typescript
|
33
|
-
import { Action, Microservice, Module } from "
|
30
|
+
import { Action, Microservice, Module } from "imean-service-engine";
|
34
31
|
```
|
35
32
|
|
36
33
|
## 快速开始
|
@@ -126,7 +123,7 @@ TypeScript 客户端代码。
|
|
126
123
|
|
127
124
|
```typescript
|
128
125
|
const client = new MicroserviceClient({
|
129
|
-
baseUrl: "http://localhost:3000
|
126
|
+
baseUrl: "http://localhost:3000",
|
130
127
|
});
|
131
128
|
// 创建用户
|
132
129
|
const user = await client.users.createUser("张三", 25);
|
@@ -230,7 +227,7 @@ interface ClientOptions {
|
|
230
227
|
|
231
228
|
```typescript
|
232
229
|
// main.ts
|
233
|
-
import { startCheck } from "
|
230
|
+
import { startCheck } from "imean-service-engine";
|
234
231
|
|
235
232
|
// 数据库连接检查
|
236
233
|
async function checkDatabase() {
|
@@ -326,17 +323,17 @@ your-service/
|
|
326
323
|
// config/index.ts
|
327
324
|
export const config = {
|
328
325
|
database: {
|
329
|
-
host:
|
330
|
-
port: parseInt(
|
326
|
+
host: process.env.DB_HOST || "localhost",
|
327
|
+
port: parseInt(process.env.DB_PORT || "5432"),
|
331
328
|
// ...
|
332
329
|
},
|
333
330
|
redis: {
|
334
|
-
url:
|
331
|
+
url: process.env.REDIS_URL || "redis://localhost:6379",
|
335
332
|
// ...
|
336
333
|
},
|
337
334
|
service: {
|
338
|
-
port: parseInt(
|
339
|
-
prefix:
|
335
|
+
port: parseInt(process.env.PORT || "3000"),
|
336
|
+
prefix: process.env.API_PREFIX || "/api",
|
340
337
|
},
|
341
338
|
};
|
342
339
|
|
@@ -550,7 +547,7 @@ const client = new MicroserviceClient({
|
|
550
547
|
|
551
548
|
### Node.js 环境使用 WebSocket
|
552
549
|
|
553
|
-
|
550
|
+
最新Node.js已经提供了 WebSocket 实现,可以直接使用。如果在较低 Node.js 环境下,可以使用 `isomorphic-ws` 包来提供 WebSocket 实现:
|
554
551
|
|
555
552
|
```typescript
|
556
553
|
import WebSocket from "isomorphic-ws";
|
package/dist/mod.cjs
CHANGED
@@ -7,9 +7,9 @@ var etcd3 = require('etcd3');
|
|
7
7
|
var fs = require('fs-extra');
|
8
8
|
var hono = require('hono');
|
9
9
|
var lruCache = require('lru-cache');
|
10
|
-
var winston = require('winston');
|
11
10
|
var api = require('@opentelemetry/api');
|
12
|
-
var
|
11
|
+
var winston = require('winston');
|
12
|
+
var prettier = require('prettier');
|
13
13
|
var crypto2 = require('crypto');
|
14
14
|
var zlib = require('zlib');
|
15
15
|
var nodeWs = require('@hono/node-ws');
|
@@ -20,6 +20,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
20
20
|
var ejson3__default = /*#__PURE__*/_interopDefault(ejson3);
|
21
21
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
22
22
|
var winston__default = /*#__PURE__*/_interopDefault(winston);
|
23
|
+
var prettier__default = /*#__PURE__*/_interopDefault(prettier);
|
23
24
|
var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
|
24
25
|
var dayjs__default = /*#__PURE__*/_interopDefault(dayjs);
|
25
26
|
|
@@ -108,6 +109,7 @@ var logger = winston__default.default.createLogger({
|
|
108
109
|
var logger_default = logger;
|
109
110
|
|
110
111
|
// decorators/schedule.ts
|
112
|
+
var tracer = api.trace.getTracer("scheduler");
|
111
113
|
var SCHEDULE_METADATA = Symbol("schedule:metadata");
|
112
114
|
function Schedule(options) {
|
113
115
|
return function(_originalMethod, context) {
|
@@ -160,20 +162,44 @@ var Scheduler = class {
|
|
160
162
|
});
|
161
163
|
campaign.on("elected", () => {
|
162
164
|
this.isLeader.set(serviceId, true);
|
163
|
-
this.startTimer(serviceId, metadata, method);
|
165
|
+
this.startTimer(serviceId, metadata, moduleName, method);
|
164
166
|
logger_default.info(`become leader for ${moduleName}.${methodName}`);
|
165
167
|
});
|
166
168
|
}
|
167
169
|
/**
|
168
170
|
* 启动定时器
|
169
171
|
*/
|
170
|
-
startTimer(serviceId, metadata, method) {
|
172
|
+
startTimer(serviceId, metadata, moduleName, method) {
|
171
173
|
this.stopTimer(serviceId);
|
174
|
+
const wrappedMethod = async () => {
|
175
|
+
tracer.startActiveSpan(
|
176
|
+
`ScheduleTask ${moduleName}.${metadata.name}`,
|
177
|
+
{ root: true },
|
178
|
+
async (span) => {
|
179
|
+
span.setAttribute("serviceId", serviceId);
|
180
|
+
span.setAttribute("methodName", metadata.name);
|
181
|
+
span.setAttribute("moduleName", moduleName);
|
182
|
+
span.setAttribute("interval", metadata.interval);
|
183
|
+
span.setAttribute("mode", metadata.mode);
|
184
|
+
try {
|
185
|
+
await method();
|
186
|
+
} catch (error) {
|
187
|
+
span.setStatus({
|
188
|
+
code: api.SpanStatusCode.ERROR,
|
189
|
+
message: error.message
|
190
|
+
});
|
191
|
+
} finally {
|
192
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
193
|
+
span.end();
|
194
|
+
}
|
195
|
+
}
|
196
|
+
);
|
197
|
+
};
|
172
198
|
if (metadata.mode === "FIXED_DELAY" /* FIXED_DELAY */) {
|
173
199
|
const runTask = async () => {
|
174
200
|
if (!this.isLeader.get(serviceId)) return;
|
175
201
|
try {
|
176
|
-
await
|
202
|
+
await wrappedMethod();
|
177
203
|
} finally {
|
178
204
|
this.timers.set(serviceId, setTimeout(runTask, metadata.interval));
|
179
205
|
}
|
@@ -184,7 +210,7 @@ var Scheduler = class {
|
|
184
210
|
serviceId,
|
185
211
|
setInterval(async () => {
|
186
212
|
if (!this.isLeader.get(serviceId)) return;
|
187
|
-
await
|
213
|
+
await wrappedMethod();
|
188
214
|
}, metadata.interval)
|
189
215
|
);
|
190
216
|
}
|
@@ -220,11 +246,9 @@ var Scheduler = class {
|
|
220
246
|
}
|
221
247
|
}
|
222
248
|
};
|
223
|
-
|
224
|
-
// utils/format.ts
|
225
249
|
async function formatCode(code) {
|
226
250
|
try {
|
227
|
-
return code;
|
251
|
+
return prettier__default.default.format(code, { parser: "typescript" });
|
228
252
|
} catch {
|
229
253
|
return code;
|
230
254
|
}
|
@@ -389,8 +413,7 @@ var brotli = {
|
|
389
413
|
};
|
390
414
|
|
391
415
|
// core/handler.ts
|
392
|
-
var
|
393
|
-
var logger2 = apiLogs.logs.getLogger("action-handler");
|
416
|
+
var tracer2 = api.trace.getTracer("action-handler");
|
394
417
|
var ActionHandler = class {
|
395
418
|
constructor(moduleInstance, actionName, metadata, microservice, moduleName) {
|
396
419
|
this.moduleInstance = moduleInstance;
|
@@ -400,47 +423,60 @@ var ActionHandler = class {
|
|
400
423
|
this.moduleName = moduleName;
|
401
424
|
}
|
402
425
|
async handle(req) {
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
} catch (error) {
|
414
|
-
throw new Error(`Invalid request body: ${error.message}`);
|
426
|
+
return await tracer2.startActiveSpan(
|
427
|
+
`handle ${this.moduleName}.${this.actionName}`,
|
428
|
+
async (span) => {
|
429
|
+
span.setAttribute("module", this.moduleName);
|
430
|
+
span.setAttribute("action", this.actionName);
|
431
|
+
try {
|
432
|
+
return await this._handle(req);
|
433
|
+
} finally {
|
434
|
+
span.end();
|
435
|
+
}
|
415
436
|
}
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
437
|
+
);
|
438
|
+
}
|
439
|
+
async _validate(req) {
|
440
|
+
const span = tracer2.startSpan("validate");
|
441
|
+
try {
|
442
|
+
let args;
|
443
|
+
if (typeof req === "string") {
|
421
444
|
try {
|
422
|
-
|
445
|
+
args = Object.values(ejson3__default.default.parse(req));
|
423
446
|
} catch (error) {
|
424
|
-
throw new Error(
|
425
|
-
`Invalid argument ${index}: ${error.message}`
|
426
|
-
);
|
447
|
+
throw new Error(`Invalid request body: ${error.message}`);
|
427
448
|
}
|
428
|
-
}
|
449
|
+
} else {
|
450
|
+
args = req;
|
451
|
+
}
|
452
|
+
if (this.metadata.params) {
|
453
|
+
args = args.map((arg, index) => {
|
454
|
+
try {
|
455
|
+
return this.metadata.params[index].parse(arg);
|
456
|
+
} catch (error) {
|
457
|
+
throw new Error(
|
458
|
+
`Invalid argument ${index}: ${error.message}`
|
459
|
+
);
|
460
|
+
}
|
461
|
+
});
|
462
|
+
}
|
463
|
+
const requestHash = hashText(ejson3__default.default.stringify(args));
|
464
|
+
span.setAttribute("requestHash", requestHash);
|
465
|
+
return { args, requestHash };
|
466
|
+
} finally {
|
467
|
+
span.end();
|
429
468
|
}
|
430
|
-
|
469
|
+
}
|
470
|
+
async _handle(req) {
|
471
|
+
const span = api.trace.getActiveSpan();
|
472
|
+
const { args, requestHash } = await this._validate(req);
|
431
473
|
if (!this.metadata.stream && this.metadata.cache && !this.microservice.options.disableCache) {
|
432
474
|
const cacheKey = `${this.moduleName}.${this.actionName}.${requestHash}`;
|
433
475
|
const cached = await this.microservice.cache.get(cacheKey);
|
434
476
|
const now = Date.now();
|
435
|
-
if (cached
|
436
|
-
|
437
|
-
|
438
|
-
this.actionName,
|
439
|
-
0,
|
440
|
-
true,
|
441
|
-
true
|
442
|
-
);
|
443
|
-
return cached.value.data;
|
477
|
+
if (cached !== null && (!this.metadata.ttl || cached?.expireAt > now)) {
|
478
|
+
span?.setAttribute("cacheHit", true);
|
479
|
+
return cached.data;
|
444
480
|
}
|
445
481
|
}
|
446
482
|
try {
|
@@ -449,11 +485,11 @@ var ActionHandler = class {
|
|
449
485
|
args
|
450
486
|
);
|
451
487
|
if (this.metadata.stream) {
|
488
|
+
span?.setAttribute("stream", true);
|
452
489
|
if (!isAsyncIterable(result)) {
|
453
490
|
throw new Error("Stream action must return AsyncIterator");
|
454
491
|
}
|
455
492
|
let count = 0;
|
456
|
-
const self = this;
|
457
493
|
return {
|
458
494
|
[Symbol.asyncIterator]() {
|
459
495
|
const iterator = result[Symbol.asyncIterator]();
|
@@ -461,24 +497,22 @@ var ActionHandler = class {
|
|
461
497
|
async next() {
|
462
498
|
try {
|
463
499
|
const { value, done } = await iterator.next();
|
500
|
+
span?.addEvent("stream.next");
|
464
501
|
if (!done) count++;
|
465
502
|
if (done) {
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
self.actionName,
|
470
|
-
responseTime / count,
|
471
|
-
true
|
472
|
-
);
|
503
|
+
span?.setAttribute("streamCount", count);
|
504
|
+
span?.addEvent("stream.end");
|
505
|
+
span?.end();
|
473
506
|
}
|
474
507
|
return { value, done };
|
475
508
|
} catch (error) {
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
);
|
509
|
+
span?.addEvent("stream.error", { error: error.message });
|
510
|
+
span?.recordException(error);
|
511
|
+
span?.setStatus({
|
512
|
+
code: api.SpanStatusCode.ERROR,
|
513
|
+
message: error.message
|
514
|
+
});
|
515
|
+
span?.end();
|
482
516
|
throw error;
|
483
517
|
}
|
484
518
|
}
|
@@ -497,41 +531,26 @@ var ActionHandler = class {
|
|
497
531
|
if (this.metadata.cache && !this.microservice.options.disableCache) {
|
498
532
|
const cacheKey = `${this.moduleName}.${this.actionName}.${requestHash}`;
|
499
533
|
const now = Date.now();
|
500
|
-
this.microservice.cache.set(
|
501
|
-
|
502
|
-
|
503
|
-
|
534
|
+
this.microservice.cache.set(
|
535
|
+
cacheKey,
|
536
|
+
{
|
537
|
+
data: parsedResult,
|
538
|
+
expireAt: this.metadata.ttl ? now + this.metadata.ttl : void 0
|
539
|
+
},
|
540
|
+
{ ttl: this.metadata.ttl }
|
541
|
+
);
|
504
542
|
}
|
505
|
-
|
506
|
-
|
507
|
-
this.actionName,
|
508
|
-
0,
|
509
|
-
true
|
510
|
-
);
|
543
|
+
span?.setStatus({ code: api.SpanStatusCode.OK, message: "success" });
|
544
|
+
span?.end();
|
511
545
|
return parsedResult;
|
512
546
|
} catch (error) {
|
513
547
|
if (this.metadata.printError !== false && this.microservice.options.printError !== false) {
|
514
548
|
console.error(`Error in ${this.moduleName}.${this.actionName}:`, error);
|
515
549
|
}
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
service: {
|
520
|
-
id: this.microservice.serviceId,
|
521
|
-
name: this.microservice.options.name,
|
522
|
-
version: this.microservice.options.version,
|
523
|
-
env: this.microservice.options.env
|
524
|
-
},
|
525
|
-
module: this.moduleName,
|
526
|
-
action: this.actionName,
|
527
|
-
params: args,
|
528
|
-
time: Date.now(),
|
529
|
-
error: error.message
|
530
|
-
});
|
531
|
-
}
|
550
|
+
span?.recordException(error);
|
551
|
+
span?.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
|
552
|
+
span?.end();
|
532
553
|
throw error;
|
533
|
-
} finally {
|
534
|
-
span.end();
|
535
554
|
}
|
536
555
|
}
|
537
556
|
};
|
package/dist/mod.d.cts
CHANGED
@@ -149,6 +149,8 @@ declare class ActionHandler {
|
|
149
149
|
private moduleName;
|
150
150
|
constructor(moduleInstance: any, actionName: string, metadata: ActionMetadata, microservice: Microservice, moduleName: string);
|
151
151
|
handle(req: string | any[]): Promise<any>;
|
152
|
+
private _validate;
|
153
|
+
private _handle;
|
152
154
|
}
|
153
155
|
|
154
156
|
declare const ServiceContext: {
|
package/dist/mod.d.ts
CHANGED
@@ -149,6 +149,8 @@ declare class ActionHandler {
|
|
149
149
|
private moduleName;
|
150
150
|
constructor(moduleInstance: any, actionName: string, metadata: ActionMetadata, microservice: Microservice, moduleName: string);
|
151
151
|
handle(req: string | any[]): Promise<any>;
|
152
|
+
private _validate;
|
153
|
+
private _handle;
|
152
154
|
}
|
153
155
|
|
154
156
|
declare const ServiceContext: {
|
package/dist/mod.js
CHANGED
@@ -6,9 +6,9 @@ import { Etcd3 } from 'etcd3';
|
|
6
6
|
import fs from 'fs-extra';
|
7
7
|
import { Hono } from 'hono';
|
8
8
|
import { LRUCache } from 'lru-cache';
|
9
|
+
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
9
10
|
import winston, { format } from 'winston';
|
10
|
-
import
|
11
|
-
import { logs } from '@opentelemetry/api-logs';
|
11
|
+
import prettier from 'prettier';
|
12
12
|
import crypto2 from 'node:crypto';
|
13
13
|
import { brotliDecompress } from 'node:zlib';
|
14
14
|
import { brotliCompress, constants } from 'zlib';
|
@@ -100,6 +100,7 @@ var logger = winston.createLogger({
|
|
100
100
|
var logger_default = logger;
|
101
101
|
|
102
102
|
// decorators/schedule.ts
|
103
|
+
var tracer = trace.getTracer("scheduler");
|
103
104
|
var SCHEDULE_METADATA = Symbol("schedule:metadata");
|
104
105
|
function Schedule(options) {
|
105
106
|
return function(_originalMethod, context) {
|
@@ -152,20 +153,44 @@ var Scheduler = class {
|
|
152
153
|
});
|
153
154
|
campaign.on("elected", () => {
|
154
155
|
this.isLeader.set(serviceId, true);
|
155
|
-
this.startTimer(serviceId, metadata, method);
|
156
|
+
this.startTimer(serviceId, metadata, moduleName, method);
|
156
157
|
logger_default.info(`become leader for ${moduleName}.${methodName}`);
|
157
158
|
});
|
158
159
|
}
|
159
160
|
/**
|
160
161
|
* 启动定时器
|
161
162
|
*/
|
162
|
-
startTimer(serviceId, metadata, method) {
|
163
|
+
startTimer(serviceId, metadata, moduleName, method) {
|
163
164
|
this.stopTimer(serviceId);
|
165
|
+
const wrappedMethod = async () => {
|
166
|
+
tracer.startActiveSpan(
|
167
|
+
`ScheduleTask ${moduleName}.${metadata.name}`,
|
168
|
+
{ root: true },
|
169
|
+
async (span) => {
|
170
|
+
span.setAttribute("serviceId", serviceId);
|
171
|
+
span.setAttribute("methodName", metadata.name);
|
172
|
+
span.setAttribute("moduleName", moduleName);
|
173
|
+
span.setAttribute("interval", metadata.interval);
|
174
|
+
span.setAttribute("mode", metadata.mode);
|
175
|
+
try {
|
176
|
+
await method();
|
177
|
+
} catch (error) {
|
178
|
+
span.setStatus({
|
179
|
+
code: SpanStatusCode.ERROR,
|
180
|
+
message: error.message
|
181
|
+
});
|
182
|
+
} finally {
|
183
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
184
|
+
span.end();
|
185
|
+
}
|
186
|
+
}
|
187
|
+
);
|
188
|
+
};
|
164
189
|
if (metadata.mode === "FIXED_DELAY" /* FIXED_DELAY */) {
|
165
190
|
const runTask = async () => {
|
166
191
|
if (!this.isLeader.get(serviceId)) return;
|
167
192
|
try {
|
168
|
-
await
|
193
|
+
await wrappedMethod();
|
169
194
|
} finally {
|
170
195
|
this.timers.set(serviceId, setTimeout(runTask, metadata.interval));
|
171
196
|
}
|
@@ -176,7 +201,7 @@ var Scheduler = class {
|
|
176
201
|
serviceId,
|
177
202
|
setInterval(async () => {
|
178
203
|
if (!this.isLeader.get(serviceId)) return;
|
179
|
-
await
|
204
|
+
await wrappedMethod();
|
180
205
|
}, metadata.interval)
|
181
206
|
);
|
182
207
|
}
|
@@ -212,11 +237,9 @@ var Scheduler = class {
|
|
212
237
|
}
|
213
238
|
}
|
214
239
|
};
|
215
|
-
|
216
|
-
// utils/format.ts
|
217
240
|
async function formatCode(code) {
|
218
241
|
try {
|
219
|
-
return code;
|
242
|
+
return prettier.format(code, { parser: "typescript" });
|
220
243
|
} catch {
|
221
244
|
return code;
|
222
245
|
}
|
@@ -381,8 +404,7 @@ var brotli = {
|
|
381
404
|
};
|
382
405
|
|
383
406
|
// core/handler.ts
|
384
|
-
var
|
385
|
-
var logger2 = logs.getLogger("action-handler");
|
407
|
+
var tracer2 = trace.getTracer("action-handler");
|
386
408
|
var ActionHandler = class {
|
387
409
|
constructor(moduleInstance, actionName, metadata, microservice, moduleName) {
|
388
410
|
this.moduleInstance = moduleInstance;
|
@@ -392,47 +414,60 @@ var ActionHandler = class {
|
|
392
414
|
this.moduleName = moduleName;
|
393
415
|
}
|
394
416
|
async handle(req) {
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
} catch (error) {
|
406
|
-
throw new Error(`Invalid request body: ${error.message}`);
|
417
|
+
return await tracer2.startActiveSpan(
|
418
|
+
`handle ${this.moduleName}.${this.actionName}`,
|
419
|
+
async (span) => {
|
420
|
+
span.setAttribute("module", this.moduleName);
|
421
|
+
span.setAttribute("action", this.actionName);
|
422
|
+
try {
|
423
|
+
return await this._handle(req);
|
424
|
+
} finally {
|
425
|
+
span.end();
|
426
|
+
}
|
407
427
|
}
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
428
|
+
);
|
429
|
+
}
|
430
|
+
async _validate(req) {
|
431
|
+
const span = tracer2.startSpan("validate");
|
432
|
+
try {
|
433
|
+
let args;
|
434
|
+
if (typeof req === "string") {
|
413
435
|
try {
|
414
|
-
|
436
|
+
args = Object.values(ejson3.parse(req));
|
415
437
|
} catch (error) {
|
416
|
-
throw new Error(
|
417
|
-
`Invalid argument ${index}: ${error.message}`
|
418
|
-
);
|
438
|
+
throw new Error(`Invalid request body: ${error.message}`);
|
419
439
|
}
|
420
|
-
}
|
440
|
+
} else {
|
441
|
+
args = req;
|
442
|
+
}
|
443
|
+
if (this.metadata.params) {
|
444
|
+
args = args.map((arg, index) => {
|
445
|
+
try {
|
446
|
+
return this.metadata.params[index].parse(arg);
|
447
|
+
} catch (error) {
|
448
|
+
throw new Error(
|
449
|
+
`Invalid argument ${index}: ${error.message}`
|
450
|
+
);
|
451
|
+
}
|
452
|
+
});
|
453
|
+
}
|
454
|
+
const requestHash = hashText(ejson3.stringify(args));
|
455
|
+
span.setAttribute("requestHash", requestHash);
|
456
|
+
return { args, requestHash };
|
457
|
+
} finally {
|
458
|
+
span.end();
|
421
459
|
}
|
422
|
-
|
460
|
+
}
|
461
|
+
async _handle(req) {
|
462
|
+
const span = trace.getActiveSpan();
|
463
|
+
const { args, requestHash } = await this._validate(req);
|
423
464
|
if (!this.metadata.stream && this.metadata.cache && !this.microservice.options.disableCache) {
|
424
465
|
const cacheKey = `${this.moduleName}.${this.actionName}.${requestHash}`;
|
425
466
|
const cached = await this.microservice.cache.get(cacheKey);
|
426
467
|
const now = Date.now();
|
427
|
-
if (cached
|
428
|
-
|
429
|
-
|
430
|
-
this.actionName,
|
431
|
-
0,
|
432
|
-
true,
|
433
|
-
true
|
434
|
-
);
|
435
|
-
return cached.value.data;
|
468
|
+
if (cached !== null && (!this.metadata.ttl || cached?.expireAt > now)) {
|
469
|
+
span?.setAttribute("cacheHit", true);
|
470
|
+
return cached.data;
|
436
471
|
}
|
437
472
|
}
|
438
473
|
try {
|
@@ -441,11 +476,11 @@ var ActionHandler = class {
|
|
441
476
|
args
|
442
477
|
);
|
443
478
|
if (this.metadata.stream) {
|
479
|
+
span?.setAttribute("stream", true);
|
444
480
|
if (!isAsyncIterable(result)) {
|
445
481
|
throw new Error("Stream action must return AsyncIterator");
|
446
482
|
}
|
447
483
|
let count = 0;
|
448
|
-
const self = this;
|
449
484
|
return {
|
450
485
|
[Symbol.asyncIterator]() {
|
451
486
|
const iterator = result[Symbol.asyncIterator]();
|
@@ -453,24 +488,22 @@ var ActionHandler = class {
|
|
453
488
|
async next() {
|
454
489
|
try {
|
455
490
|
const { value, done } = await iterator.next();
|
491
|
+
span?.addEvent("stream.next");
|
456
492
|
if (!done) count++;
|
457
493
|
if (done) {
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
self.actionName,
|
462
|
-
responseTime / count,
|
463
|
-
true
|
464
|
-
);
|
494
|
+
span?.setAttribute("streamCount", count);
|
495
|
+
span?.addEvent("stream.end");
|
496
|
+
span?.end();
|
465
497
|
}
|
466
498
|
return { value, done };
|
467
499
|
} catch (error) {
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
);
|
500
|
+
span?.addEvent("stream.error", { error: error.message });
|
501
|
+
span?.recordException(error);
|
502
|
+
span?.setStatus({
|
503
|
+
code: SpanStatusCode.ERROR,
|
504
|
+
message: error.message
|
505
|
+
});
|
506
|
+
span?.end();
|
474
507
|
throw error;
|
475
508
|
}
|
476
509
|
}
|
@@ -489,41 +522,26 @@ var ActionHandler = class {
|
|
489
522
|
if (this.metadata.cache && !this.microservice.options.disableCache) {
|
490
523
|
const cacheKey = `${this.moduleName}.${this.actionName}.${requestHash}`;
|
491
524
|
const now = Date.now();
|
492
|
-
this.microservice.cache.set(
|
493
|
-
|
494
|
-
|
495
|
-
|
525
|
+
this.microservice.cache.set(
|
526
|
+
cacheKey,
|
527
|
+
{
|
528
|
+
data: parsedResult,
|
529
|
+
expireAt: this.metadata.ttl ? now + this.metadata.ttl : void 0
|
530
|
+
},
|
531
|
+
{ ttl: this.metadata.ttl }
|
532
|
+
);
|
496
533
|
}
|
497
|
-
|
498
|
-
|
499
|
-
this.actionName,
|
500
|
-
0,
|
501
|
-
true
|
502
|
-
);
|
534
|
+
span?.setStatus({ code: SpanStatusCode.OK, message: "success" });
|
535
|
+
span?.end();
|
503
536
|
return parsedResult;
|
504
537
|
} catch (error) {
|
505
538
|
if (this.metadata.printError !== false && this.microservice.options.printError !== false) {
|
506
539
|
console.error(`Error in ${this.moduleName}.${this.actionName}:`, error);
|
507
540
|
}
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
service: {
|
512
|
-
id: this.microservice.serviceId,
|
513
|
-
name: this.microservice.options.name,
|
514
|
-
version: this.microservice.options.version,
|
515
|
-
env: this.microservice.options.env
|
516
|
-
},
|
517
|
-
module: this.moduleName,
|
518
|
-
action: this.actionName,
|
519
|
-
params: args,
|
520
|
-
time: Date.now(),
|
521
|
-
error: error.message
|
522
|
-
});
|
523
|
-
}
|
541
|
+
span?.recordException(error);
|
542
|
+
span?.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
543
|
+
span?.end();
|
524
544
|
throw error;
|
525
|
-
} finally {
|
526
|
-
span.end();
|
527
545
|
}
|
528
546
|
}
|
529
547
|
};
|
package/package.json
CHANGED
@@ -1,74 +1,84 @@
|
|
1
|
-
{
|
2
|
-
"name": "imean-service-engine",
|
3
|
-
"version": "1.
|
4
|
-
"description": "microservice engine",
|
5
|
-
"keywords": [
|
6
|
-
"microservice",
|
7
|
-
"websocket",
|
8
|
-
"http",
|
9
|
-
"node"
|
10
|
-
],
|
11
|
-
"author": "imean",
|
12
|
-
"type": "module",
|
13
|
-
"license": "MIT",
|
14
|
-
"repository": {
|
15
|
-
"type": "git",
|
16
|
-
"url": "git+https://git.imean.tech/imean/imean-microservice-framework.git"
|
17
|
-
},
|
18
|
-
"main": "dist/mod.js",
|
19
|
-
"module": "dist/mod.js",
|
20
|
-
"types": "dist/mod.d.ts",
|
21
|
-
"exports": {
|
22
|
-
".": {
|
23
|
-
"types": "./dist/mod.d.ts",
|
24
|
-
"import": "./dist/mod.js",
|
25
|
-
"require": "./dist/mod.cjs"
|
26
|
-
}
|
27
|
-
},
|
28
|
-
"files": [
|
29
|
-
"dist",
|
30
|
-
"README.md",
|
31
|
-
"LICENSE"
|
32
|
-
],
|
33
|
-
"scripts": {
|
34
|
-
"dev": "tsx dev/index.ts",
|
35
|
-
"build": "tsup",
|
36
|
-
"lint": "deno lint",
|
37
|
-
"fmt": "deno fmt",
|
38
|
-
"test": "vitest",
|
39
|
-
"coverage": "vitest --coverage",
|
40
|
-
"prepublishOnly": "npm run build"
|
41
|
-
},
|
42
|
-
"dependencies": {
|
43
|
-
"@hono/node-server": "^1.13.7",
|
44
|
-
"@hono/node-ws": "^1.0.6",
|
45
|
-
"
|
46
|
-
"
|
47
|
-
"
|
48
|
-
"
|
49
|
-
"
|
50
|
-
"
|
51
|
-
"
|
52
|
-
"
|
53
|
-
"
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
"@
|
61
|
-
"@
|
62
|
-
"@
|
63
|
-
"
|
64
|
-
"
|
65
|
-
"
|
66
|
-
"
|
67
|
-
"
|
68
|
-
"
|
69
|
-
"
|
70
|
-
|
71
|
-
|
72
|
-
"
|
73
|
-
|
74
|
-
|
1
|
+
{
|
2
|
+
"name": "imean-service-engine",
|
3
|
+
"version": "1.2.0",
|
4
|
+
"description": "microservice engine",
|
5
|
+
"keywords": [
|
6
|
+
"microservice",
|
7
|
+
"websocket",
|
8
|
+
"http",
|
9
|
+
"node"
|
10
|
+
],
|
11
|
+
"author": "imean",
|
12
|
+
"type": "module",
|
13
|
+
"license": "MIT",
|
14
|
+
"repository": {
|
15
|
+
"type": "git",
|
16
|
+
"url": "git+https://git.imean.tech/imean/imean-microservice-framework.git"
|
17
|
+
},
|
18
|
+
"main": "dist/mod.js",
|
19
|
+
"module": "dist/mod.js",
|
20
|
+
"types": "dist/mod.d.ts",
|
21
|
+
"exports": {
|
22
|
+
".": {
|
23
|
+
"types": "./dist/mod.d.ts",
|
24
|
+
"import": "./dist/mod.js",
|
25
|
+
"require": "./dist/mod.cjs"
|
26
|
+
}
|
27
|
+
},
|
28
|
+
"files": [
|
29
|
+
"dist",
|
30
|
+
"README.md",
|
31
|
+
"LICENSE"
|
32
|
+
],
|
33
|
+
"scripts": {
|
34
|
+
"dev": "tsx dev/index.ts",
|
35
|
+
"build": "tsup",
|
36
|
+
"lint": "deno lint",
|
37
|
+
"fmt": "deno fmt",
|
38
|
+
"test": "vitest",
|
39
|
+
"coverage": "vitest --coverage",
|
40
|
+
"prepublishOnly": "npm run build && npm run test"
|
41
|
+
},
|
42
|
+
"dependencies": {
|
43
|
+
"@hono/node-server": "^1.13.7",
|
44
|
+
"@hono/node-ws": "^1.0.6",
|
45
|
+
"dayjs": "^1.11.13",
|
46
|
+
"ejson": "^2.2.3",
|
47
|
+
"etcd3": "^1.1.2",
|
48
|
+
"fs-extra": "^11.3.0",
|
49
|
+
"hono": "^4.6.17",
|
50
|
+
"lru-cache": "^11.0.2",
|
51
|
+
"prettier": "^3.4.2",
|
52
|
+
"winston": "^3.17.0",
|
53
|
+
"zod": "^3.24.1"
|
54
|
+
},
|
55
|
+
"peerDependencies": {
|
56
|
+
"@opentelemetry/api": "^1.x"
|
57
|
+
},
|
58
|
+
"devDependencies": {
|
59
|
+
"@opentelemetry/auto-instrumentations-node": "^0.55.3",
|
60
|
+
"@opentelemetry/exporter-logs-otlp-proto": "^0.57.1",
|
61
|
+
"@opentelemetry/exporter-metrics-otlp-proto": "^0.57.1",
|
62
|
+
"@opentelemetry/exporter-trace-otlp-proto": "^0.57.1",
|
63
|
+
"@opentelemetry/instrumentation-winston": "^0.44.0",
|
64
|
+
"@opentelemetry/sdk-logs": "^0.57.1",
|
65
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
66
|
+
"@opentelemetry/sdk-node": "^0.57.1",
|
67
|
+
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
68
|
+
"@opentelemetry/winston-transport": "^0.10.0",
|
69
|
+
"@types/ejson": "^2.2.2",
|
70
|
+
"@types/fs-extra": "^11.0.4",
|
71
|
+
"@types/node": "^20.0.0",
|
72
|
+
"imean-service-client": "^1.5.0",
|
73
|
+
"opentelemetry-instrumentation-fetch-node": "^1.2.3",
|
74
|
+
"tslib": "^2.8.1",
|
75
|
+
"tsup": "^8.0.1",
|
76
|
+
"tsx": "^4.19.2",
|
77
|
+
"typescript": "^5.3.3",
|
78
|
+
"vite-tsconfig-paths": "^5.1.4",
|
79
|
+
"vitest": "^3.0.3"
|
80
|
+
},
|
81
|
+
"engines": {
|
82
|
+
"node": ">=20"
|
83
|
+
}
|
84
|
+
}
|