imean-service-engine 1.0.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 +585 -0
- package/dist/mod.cjs +1217 -0
- package/dist/mod.d.cts +259 -0
- package/dist/mod.d.ts +259 -0
- package/dist/mod.js +1192 -0
- package/package.json +74 -0
package/dist/mod.cjs
ADDED
@@ -0,0 +1,1217 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
var zod = require('zod');
|
4
|
+
var nodeServer = require('@hono/node-server');
|
5
|
+
var ejson3 = require('ejson');
|
6
|
+
var etcd3 = require('etcd3');
|
7
|
+
var fs = require('fs-extra');
|
8
|
+
var hono = require('hono');
|
9
|
+
var lruCache = require('lru-cache');
|
10
|
+
var winston = require('winston');
|
11
|
+
var api = require('@opentelemetry/api');
|
12
|
+
var apiLogs = require('@opentelemetry/api-logs');
|
13
|
+
var crypto2 = require('crypto');
|
14
|
+
var zlib = require('zlib');
|
15
|
+
var nodeWs = require('@hono/node-ws');
|
16
|
+
var dayjs = require('dayjs');
|
17
|
+
|
18
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
19
|
+
|
20
|
+
var ejson3__default = /*#__PURE__*/_interopDefault(ejson3);
|
21
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
22
|
+
var winston__default = /*#__PURE__*/_interopDefault(winston);
|
23
|
+
var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
|
24
|
+
var dayjs__default = /*#__PURE__*/_interopDefault(dayjs);
|
25
|
+
|
26
|
+
// mod.ts
|
27
|
+
var ACTION_METADATA = Symbol("action:metadata");
|
28
|
+
function Action(options) {
|
29
|
+
options.params = options.params || [];
|
30
|
+
options.returns = options.returns || zod.z.void();
|
31
|
+
options.cache = options.cache ?? false;
|
32
|
+
options.ttl = options.ttl ?? 0;
|
33
|
+
options.stream = options.stream ?? false;
|
34
|
+
return function(target, context) {
|
35
|
+
const methodName = context.name;
|
36
|
+
context.addInitializer(function() {
|
37
|
+
const prototype = this.constructor.prototype;
|
38
|
+
const func = target.toString().split("\n")[0];
|
39
|
+
const params = func.slice(func.indexOf("(") + 1, func.indexOf(")")).split(",").map((p) => p.split("=").shift()).filter((param) => !!param?.trim());
|
40
|
+
params.forEach((param, index) => {
|
41
|
+
options.params[index] = options.params[index]?.describe(
|
42
|
+
param.trim()
|
43
|
+
);
|
44
|
+
});
|
45
|
+
const existingMetadata = prototype[ACTION_METADATA] || {};
|
46
|
+
existingMetadata[methodName] = {
|
47
|
+
name: methodName,
|
48
|
+
description: options.description || "",
|
49
|
+
params: options.params,
|
50
|
+
returns: options.returns,
|
51
|
+
idempotence: options.idempotence ?? false,
|
52
|
+
printError: options.printError ?? false,
|
53
|
+
cache: options.cache,
|
54
|
+
ttl: options.ttl,
|
55
|
+
stream: options.stream
|
56
|
+
};
|
57
|
+
prototype[ACTION_METADATA] = existingMetadata;
|
58
|
+
});
|
59
|
+
};
|
60
|
+
}
|
61
|
+
function getActionMetadata(target) {
|
62
|
+
return target.constructor.prototype[ACTION_METADATA] ?? {};
|
63
|
+
}
|
64
|
+
|
65
|
+
// decorators/module.ts
|
66
|
+
var moduleMetadataMap = /* @__PURE__ */ new WeakMap();
|
67
|
+
function Module(name, options = {}) {
|
68
|
+
return function(target, _context) {
|
69
|
+
const metadata = {
|
70
|
+
name,
|
71
|
+
options: {
|
72
|
+
name: options.name || name,
|
73
|
+
version: options.version || "1.0.0",
|
74
|
+
description: options.description || "",
|
75
|
+
printError: options.printError ?? false
|
76
|
+
}
|
77
|
+
};
|
78
|
+
moduleMetadataMap.set(target, metadata);
|
79
|
+
};
|
80
|
+
}
|
81
|
+
function getModuleMetadata(target) {
|
82
|
+
return moduleMetadataMap.get(target);
|
83
|
+
}
|
84
|
+
|
85
|
+
// core/types.ts
|
86
|
+
var ScheduleMode = /* @__PURE__ */ ((ScheduleMode2) => {
|
87
|
+
ScheduleMode2["FIXED_RATE"] = "FIXED_RATE";
|
88
|
+
ScheduleMode2["FIXED_DELAY"] = "FIXED_DELAY";
|
89
|
+
return ScheduleMode2;
|
90
|
+
})(ScheduleMode || {});
|
91
|
+
var logger = winston__default.default.createLogger({
|
92
|
+
level: "info",
|
93
|
+
transports: [
|
94
|
+
new winston__default.default.transports.Console({
|
95
|
+
level: "info",
|
96
|
+
format: winston.format.combine(
|
97
|
+
winston.format.colorize(),
|
98
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
|
99
|
+
winston.format.printf((info) => {
|
100
|
+
const splat = info[Symbol.for("splat")];
|
101
|
+
const msg = `${info.level} ${info.timestamp}: ${info.message} ${!!splat ? JSON.stringify(splat) : ""}`;
|
102
|
+
return msg;
|
103
|
+
})
|
104
|
+
)
|
105
|
+
})
|
106
|
+
]
|
107
|
+
});
|
108
|
+
var logger_default = logger;
|
109
|
+
|
110
|
+
// decorators/schedule.ts
|
111
|
+
var SCHEDULE_METADATA = Symbol("schedule:metadata");
|
112
|
+
function Schedule(options) {
|
113
|
+
return function(_originalMethod, context) {
|
114
|
+
const methodName = context.name.toString();
|
115
|
+
context.addInitializer(function() {
|
116
|
+
const prototype = this.constructor.prototype;
|
117
|
+
const moduleMetadata = getModuleMetadata(this.constructor);
|
118
|
+
if (!moduleMetadata) {
|
119
|
+
throw new Error(
|
120
|
+
`Class ${this.constructor.name} is not decorated with @Module decorator`
|
121
|
+
);
|
122
|
+
}
|
123
|
+
const existingMetadata = prototype[SCHEDULE_METADATA] || {};
|
124
|
+
existingMetadata[methodName] = {
|
125
|
+
name: methodName,
|
126
|
+
interval: options.interval,
|
127
|
+
mode: options.mode || "FIXED_RATE" /* FIXED_RATE */
|
128
|
+
};
|
129
|
+
prototype[SCHEDULE_METADATA] = existingMetadata;
|
130
|
+
});
|
131
|
+
};
|
132
|
+
}
|
133
|
+
function getScheduleMetadata(target) {
|
134
|
+
return target.constructor.prototype[SCHEDULE_METADATA] ?? {};
|
135
|
+
}
|
136
|
+
var Scheduler = class {
|
137
|
+
constructor(etcdClient) {
|
138
|
+
this.etcdClient = etcdClient;
|
139
|
+
}
|
140
|
+
campaigns = /* @__PURE__ */ new Map();
|
141
|
+
timers = /* @__PURE__ */ new Map();
|
142
|
+
isLeader = /* @__PURE__ */ new Map();
|
143
|
+
/**
|
144
|
+
* 启动调度任务
|
145
|
+
*/
|
146
|
+
async startSchedule(serviceId, moduleName, methodName, electionKey, metadata, method) {
|
147
|
+
const election = this.etcdClient.election(electionKey, 10);
|
148
|
+
const observe = await election.observe();
|
149
|
+
observe.on("change", (leader) => {
|
150
|
+
const isLeader = leader === serviceId;
|
151
|
+
this.isLeader.set(serviceId, isLeader);
|
152
|
+
if (!isLeader) {
|
153
|
+
this.stopTimer(serviceId);
|
154
|
+
}
|
155
|
+
});
|
156
|
+
const campaign = election.campaign(serviceId);
|
157
|
+
this.campaigns.set(serviceId, campaign);
|
158
|
+
campaign.on("error", (error) => {
|
159
|
+
logger_default.error(`Error in campaign for ${moduleName}.${methodName}:`, error);
|
160
|
+
});
|
161
|
+
campaign.on("elected", () => {
|
162
|
+
this.isLeader.set(serviceId, true);
|
163
|
+
this.startTimer(serviceId, metadata, method);
|
164
|
+
logger_default.info(`become leader for ${moduleName}.${methodName}`);
|
165
|
+
});
|
166
|
+
}
|
167
|
+
/**
|
168
|
+
* 启动定时器
|
169
|
+
*/
|
170
|
+
startTimer(serviceId, metadata, method) {
|
171
|
+
this.stopTimer(serviceId);
|
172
|
+
if (metadata.mode === "FIXED_DELAY" /* FIXED_DELAY */) {
|
173
|
+
const runTask = async () => {
|
174
|
+
if (!this.isLeader.get(serviceId)) return;
|
175
|
+
try {
|
176
|
+
await method();
|
177
|
+
} finally {
|
178
|
+
this.timers.set(serviceId, setTimeout(runTask, metadata.interval));
|
179
|
+
}
|
180
|
+
};
|
181
|
+
runTask();
|
182
|
+
} else {
|
183
|
+
this.timers.set(
|
184
|
+
serviceId,
|
185
|
+
setInterval(async () => {
|
186
|
+
if (!this.isLeader.get(serviceId)) return;
|
187
|
+
await method();
|
188
|
+
}, metadata.interval)
|
189
|
+
);
|
190
|
+
}
|
191
|
+
}
|
192
|
+
/**
|
193
|
+
* 停止定时器
|
194
|
+
*/
|
195
|
+
stopTimer(serviceId) {
|
196
|
+
const timer = this.timers.get(serviceId);
|
197
|
+
if (timer) {
|
198
|
+
clearTimeout(timer);
|
199
|
+
clearInterval(timer);
|
200
|
+
this.timers.delete(serviceId);
|
201
|
+
}
|
202
|
+
}
|
203
|
+
/**
|
204
|
+
* 停止所有调度任务
|
205
|
+
*/
|
206
|
+
async stop() {
|
207
|
+
for (const serviceId of this.timers.keys()) {
|
208
|
+
this.stopTimer(serviceId);
|
209
|
+
}
|
210
|
+
for (const [serviceId, campaign] of this.campaigns.entries()) {
|
211
|
+
try {
|
212
|
+
await campaign.resign().catch(() => {
|
213
|
+
});
|
214
|
+
} catch (error) {
|
215
|
+
console.error(`Error stopping schedule ${serviceId}:`, error);
|
216
|
+
} finally {
|
217
|
+
this.campaigns.delete(serviceId);
|
218
|
+
this.isLeader.delete(serviceId);
|
219
|
+
}
|
220
|
+
}
|
221
|
+
}
|
222
|
+
};
|
223
|
+
|
224
|
+
// utils/format.ts
|
225
|
+
async function formatCode(code) {
|
226
|
+
try {
|
227
|
+
return code;
|
228
|
+
} catch {
|
229
|
+
return code;
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
// core/generator.ts
|
234
|
+
function getZodTypeString(schema, defaultOptional = false) {
|
235
|
+
function processType(type) {
|
236
|
+
const def = type._def;
|
237
|
+
if (def.typeName === "ZodNullable") {
|
238
|
+
return `${processType(def.innerType)} | null`;
|
239
|
+
}
|
240
|
+
if (def.typeName === "ZodOptional") {
|
241
|
+
return processType(def.innerType);
|
242
|
+
}
|
243
|
+
switch (def.typeName) {
|
244
|
+
case "ZodString": {
|
245
|
+
return "string";
|
246
|
+
}
|
247
|
+
case "ZodNumber": {
|
248
|
+
return "number";
|
249
|
+
}
|
250
|
+
case "ZodBoolean": {
|
251
|
+
return "boolean";
|
252
|
+
}
|
253
|
+
case "ZodArray": {
|
254
|
+
const elementType = processType(def.type);
|
255
|
+
return `${elementType}[]`;
|
256
|
+
}
|
257
|
+
case "ZodDate": {
|
258
|
+
return "Date";
|
259
|
+
}
|
260
|
+
case "ZodObject": {
|
261
|
+
const shape = def.shape();
|
262
|
+
const props = Object.entries(shape).map(([key, value]) => {
|
263
|
+
const fieldDef = value._def;
|
264
|
+
const isOptional = fieldDef.typeName === "ZodOptional";
|
265
|
+
const isDefault = defaultOptional && fieldDef.typeName === "ZodDefault";
|
266
|
+
const fieldType = processType(
|
267
|
+
isOptional ? fieldDef.innerType : value
|
268
|
+
);
|
269
|
+
return `${key}${isOptional || isDefault ? "?" : ""}: ${fieldType}`;
|
270
|
+
}).join("; ");
|
271
|
+
return `{ ${props} }`;
|
272
|
+
}
|
273
|
+
case "ZodEnum": {
|
274
|
+
return def.values.map((opt) => `"${opt}"`).join(" | ");
|
275
|
+
}
|
276
|
+
case "ZodUnion": {
|
277
|
+
return def.options.map((opt) => processType(opt)).join(" | ");
|
278
|
+
}
|
279
|
+
case "ZodNull": {
|
280
|
+
return "null";
|
281
|
+
}
|
282
|
+
case "ZodPromise": {
|
283
|
+
return `Promise<${processType(def.type)}>`;
|
284
|
+
}
|
285
|
+
case "ZodVoid": {
|
286
|
+
return "void";
|
287
|
+
}
|
288
|
+
case "ZodRecord": {
|
289
|
+
return `Record<${processType(def.keyType)}, ${processType(def.valueType)}>`;
|
290
|
+
}
|
291
|
+
case "ZodMap": {
|
292
|
+
return `Map<${processType(def.keyType)}, ${processType(def.valueType)}>`;
|
293
|
+
}
|
294
|
+
case "ZodAny": {
|
295
|
+
return "any";
|
296
|
+
}
|
297
|
+
case "ZodUnknown": {
|
298
|
+
return "unknown";
|
299
|
+
}
|
300
|
+
case "ZodEnum ": {
|
301
|
+
return def.values.map((opt) => `"${opt}"`).join(" | ");
|
302
|
+
}
|
303
|
+
case "ZodDefault": {
|
304
|
+
return processType(def.innerType);
|
305
|
+
}
|
306
|
+
default: {
|
307
|
+
if (type.safeParse(new Uint8Array()).success) {
|
308
|
+
return "Uint8Array";
|
309
|
+
}
|
310
|
+
return "unknown";
|
311
|
+
}
|
312
|
+
}
|
313
|
+
}
|
314
|
+
return processType(schema);
|
315
|
+
}
|
316
|
+
async function generateClientCode(modules) {
|
317
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
318
|
+
const imports = [
|
319
|
+
"// \u8FD9\u4E2A\u6587\u4EF6\u662F\u81EA\u52A8\u751F\u6210\u7684\uFF0C\u8BF7\u4E0D\u8981\u624B\u52A8\u4FEE\u6539",
|
320
|
+
`// Generated at ${timestamp}`,
|
321
|
+
"",
|
322
|
+
'import { MicroserviceClient as BaseMicroserviceClient } from "imean-service-client";',
|
323
|
+
'export * from "imean-service-client";',
|
324
|
+
""
|
325
|
+
].join("\n");
|
326
|
+
const interfaces = Object.entries(modules).map(([name, module]) => {
|
327
|
+
const methods = Object.entries(module.actions).map(([actionName, action]) => {
|
328
|
+
if (!action.params) {
|
329
|
+
throw new Error(`Missing params for action ${actionName}`);
|
330
|
+
}
|
331
|
+
const params = action.params.map((param, index) => {
|
332
|
+
const name2 = param.description || `arg${index}`;
|
333
|
+
return `${name2}${param.isOptional() ? "?" : ""}: ${getZodTypeString(param, true)}`;
|
334
|
+
}).join(", ");
|
335
|
+
const returnType = action.returns ? getZodTypeString(action.returns) : "void";
|
336
|
+
return `
|
337
|
+
/**
|
338
|
+
* ${action.description}
|
339
|
+
*/
|
340
|
+
${actionName}: (${params}) => Promise<${action.stream ? `AsyncIterable<${returnType}>` : returnType}>;`;
|
341
|
+
}).join("\n ");
|
342
|
+
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
|
343
|
+
return `export interface ${capitalizedName}Module {
|
344
|
+
${methods}
|
345
|
+
}`;
|
346
|
+
}).join("\n\n");
|
347
|
+
const clientClass = `export class MicroserviceClient extends BaseMicroserviceClient {
|
348
|
+
${Object.entries(modules).map(([name, module]) => {
|
349
|
+
const methods = Object.entries(module.actions).map(([actionName, action]) => {
|
350
|
+
return `${actionName}: { idempotent: ${!!action.idempotence}, stream: ${!!action.stream} }`;
|
351
|
+
}).join(",\n ");
|
352
|
+
return `public readonly ${name} = this.registerModule<${name.charAt(0).toUpperCase() + name.slice(1)}Module>("${name}", {
|
353
|
+
${methods}
|
354
|
+
});`;
|
355
|
+
}).join("\n\n ")}
|
356
|
+
}`;
|
357
|
+
return await formatCode([
|
358
|
+
imports,
|
359
|
+
interfaces,
|
360
|
+
clientClass
|
361
|
+
].join("\n\n"));
|
362
|
+
}
|
363
|
+
function hashText(text) {
|
364
|
+
const hash = crypto2__default.default.createHash("sha256");
|
365
|
+
hash.update(text);
|
366
|
+
return hash.digest("hex");
|
367
|
+
}
|
368
|
+
var brotli = {
|
369
|
+
compress: (data, quality = 6) => {
|
370
|
+
return new Promise((resolve, reject) => {
|
371
|
+
zlib.brotliCompress(
|
372
|
+
data,
|
373
|
+
{ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: quality } },
|
374
|
+
(err, compressed) => {
|
375
|
+
if (err) reject(err);
|
376
|
+
else resolve(compressed);
|
377
|
+
}
|
378
|
+
);
|
379
|
+
});
|
380
|
+
},
|
381
|
+
decompress: (data) => {
|
382
|
+
return new Promise((resolve, reject) => {
|
383
|
+
zlib.brotliDecompress(data, (err, decompressed) => {
|
384
|
+
if (err) reject(err);
|
385
|
+
else resolve(decompressed);
|
386
|
+
});
|
387
|
+
});
|
388
|
+
}
|
389
|
+
};
|
390
|
+
|
391
|
+
// core/handler.ts
|
392
|
+
var tracer = api.trace.getTracer("action-handler");
|
393
|
+
var logger2 = apiLogs.logs.getLogger("action-handler");
|
394
|
+
var ActionHandler = class {
|
395
|
+
constructor(moduleInstance, actionName, metadata, microservice, moduleName) {
|
396
|
+
this.moduleInstance = moduleInstance;
|
397
|
+
this.actionName = actionName;
|
398
|
+
this.metadata = metadata;
|
399
|
+
this.microservice = microservice;
|
400
|
+
this.moduleName = moduleName;
|
401
|
+
}
|
402
|
+
async handle(req) {
|
403
|
+
const span = tracer.startSpan("handle");
|
404
|
+
span.addEvent("logs");
|
405
|
+
logger2.emit({
|
406
|
+
attributes: { module: this.moduleName, action: this.actionName }
|
407
|
+
});
|
408
|
+
const startTime = Date.now();
|
409
|
+
let args;
|
410
|
+
if (typeof req === "string") {
|
411
|
+
try {
|
412
|
+
args = Object.values(ejson3__default.default.parse(req));
|
413
|
+
} catch (error) {
|
414
|
+
throw new Error(`Invalid request body: ${error.message}`);
|
415
|
+
}
|
416
|
+
} else {
|
417
|
+
args = req;
|
418
|
+
}
|
419
|
+
if (this.metadata.params) {
|
420
|
+
args = args.map((arg, index) => {
|
421
|
+
try {
|
422
|
+
return this.metadata.params[index].parse(arg);
|
423
|
+
} catch (error) {
|
424
|
+
throw new Error(
|
425
|
+
`Invalid argument ${index}: ${error.message}`
|
426
|
+
);
|
427
|
+
}
|
428
|
+
});
|
429
|
+
}
|
430
|
+
const requestHash = hashText(ejson3__default.default.stringify(args));
|
431
|
+
if (!this.metadata.stream && this.metadata.cache && !this.microservice.options.disableCache) {
|
432
|
+
const cacheKey = `${this.moduleName}.${this.actionName}.${requestHash}`;
|
433
|
+
const cached = await this.microservice.cache.get(cacheKey);
|
434
|
+
const now = Date.now();
|
435
|
+
if (cached.value !== null && (!this.metadata.ttl || cached.value?.expireAt > now)) {
|
436
|
+
this.microservice.updateMethodStats(
|
437
|
+
this.moduleName,
|
438
|
+
this.actionName,
|
439
|
+
0,
|
440
|
+
true,
|
441
|
+
true
|
442
|
+
);
|
443
|
+
return cached.value.data;
|
444
|
+
}
|
445
|
+
}
|
446
|
+
try {
|
447
|
+
const result = await this.moduleInstance[this.actionName].apply(
|
448
|
+
this.moduleInstance,
|
449
|
+
args
|
450
|
+
);
|
451
|
+
if (this.metadata.stream) {
|
452
|
+
if (!isAsyncIterable(result)) {
|
453
|
+
throw new Error("Stream action must return AsyncIterator");
|
454
|
+
}
|
455
|
+
let count = 0;
|
456
|
+
const self = this;
|
457
|
+
return {
|
458
|
+
[Symbol.asyncIterator]() {
|
459
|
+
const iterator = result[Symbol.asyncIterator]();
|
460
|
+
return {
|
461
|
+
async next() {
|
462
|
+
try {
|
463
|
+
const { value, done } = await iterator.next();
|
464
|
+
if (!done) count++;
|
465
|
+
if (done) {
|
466
|
+
const responseTime = Date.now() - startTime;
|
467
|
+
self.microservice.updateMethodStats(
|
468
|
+
self.moduleName,
|
469
|
+
self.actionName,
|
470
|
+
responseTime / count,
|
471
|
+
true
|
472
|
+
);
|
473
|
+
}
|
474
|
+
return { value, done };
|
475
|
+
} catch (error) {
|
476
|
+
self.microservice.updateMethodStats(
|
477
|
+
self.moduleName,
|
478
|
+
self.actionName,
|
479
|
+
0,
|
480
|
+
false
|
481
|
+
);
|
482
|
+
throw error;
|
483
|
+
}
|
484
|
+
}
|
485
|
+
};
|
486
|
+
}
|
487
|
+
};
|
488
|
+
}
|
489
|
+
let parsedResult = result;
|
490
|
+
if (this.metadata.returns) {
|
491
|
+
try {
|
492
|
+
parsedResult = this.metadata.returns.parse(result);
|
493
|
+
} catch (error) {
|
494
|
+
throw new Error(`Invalid return value: ${error.message}`);
|
495
|
+
}
|
496
|
+
}
|
497
|
+
if (this.metadata.cache && !this.microservice.options.disableCache) {
|
498
|
+
const cacheKey = `${this.moduleName}.${this.actionName}.${requestHash}`;
|
499
|
+
const now = Date.now();
|
500
|
+
this.microservice.cache.set(cacheKey, {
|
501
|
+
data: parsedResult,
|
502
|
+
expireAt: this.metadata.ttl ? now + this.metadata.ttl : void 0
|
503
|
+
});
|
504
|
+
}
|
505
|
+
this.microservice.updateMethodStats(
|
506
|
+
this.moduleName,
|
507
|
+
this.actionName,
|
508
|
+
0,
|
509
|
+
true
|
510
|
+
);
|
511
|
+
return parsedResult;
|
512
|
+
} catch (error) {
|
513
|
+
if (this.metadata.printError !== false && this.microservice.options.printError !== false) {
|
514
|
+
console.error(`Error in ${this.moduleName}.${this.actionName}:`, error);
|
515
|
+
}
|
516
|
+
if (this.microservice.options.events?.onError) {
|
517
|
+
this.microservice.options.events.onError({
|
518
|
+
requestHash,
|
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
|
+
}
|
532
|
+
throw error;
|
533
|
+
} finally {
|
534
|
+
span.end();
|
535
|
+
}
|
536
|
+
}
|
537
|
+
};
|
538
|
+
function isAsyncIterable(obj) {
|
539
|
+
return obj != null && typeof obj[Symbol.asyncIterator] === "function";
|
540
|
+
}
|
541
|
+
var WebSocketHandler = class {
|
542
|
+
constructor(microservice, options) {
|
543
|
+
this.microservice = microservice;
|
544
|
+
this.options = {
|
545
|
+
timeout: options?.timeout || 3e4
|
546
|
+
};
|
547
|
+
}
|
548
|
+
sockets = /* @__PURE__ */ new Set();
|
549
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
550
|
+
options;
|
551
|
+
onOpen(ws) {
|
552
|
+
this.sockets.add(ws);
|
553
|
+
}
|
554
|
+
async encodeMessage(message) {
|
555
|
+
const jsonStr = ejson3__default.default.stringify(message);
|
556
|
+
const data = new TextEncoder().encode(jsonStr);
|
557
|
+
const compressed = await brotli.compress(data, 6);
|
558
|
+
return compressed;
|
559
|
+
}
|
560
|
+
async decodeMessage(data) {
|
561
|
+
try {
|
562
|
+
const decompressed = await brotli.decompress(new Uint8Array(data));
|
563
|
+
const jsonStr = new TextDecoder().decode(decompressed);
|
564
|
+
return ejson3__default.default.parse(jsonStr);
|
565
|
+
} catch (error) {
|
566
|
+
console.error("Error decoding message", error);
|
567
|
+
throw error;
|
568
|
+
}
|
569
|
+
}
|
570
|
+
async sendMessage(ws, message) {
|
571
|
+
const encoded = await this.encodeMessage(message);
|
572
|
+
ws.send(encoded);
|
573
|
+
}
|
574
|
+
async onMessage(event, ws) {
|
575
|
+
let messageId = "";
|
576
|
+
try {
|
577
|
+
const message = await this.decodeMessage(event.data);
|
578
|
+
messageId = message.id || "";
|
579
|
+
if (message.type === "ping") {
|
580
|
+
await this.sendMessage(ws, { type: "pong" });
|
581
|
+
return;
|
582
|
+
}
|
583
|
+
const response = await this.handleRequest(ws, message);
|
584
|
+
if (response) {
|
585
|
+
await this.sendMessage(ws, response);
|
586
|
+
}
|
587
|
+
} catch (error) {
|
588
|
+
if (messageId) {
|
589
|
+
await this.sendMessage(ws, {
|
590
|
+
id: messageId,
|
591
|
+
success: false,
|
592
|
+
error: error.message
|
593
|
+
});
|
594
|
+
}
|
595
|
+
console.error("Failed to handle WebSocket message:", error);
|
596
|
+
}
|
597
|
+
}
|
598
|
+
onClose(ws) {
|
599
|
+
this.sockets.delete(ws);
|
600
|
+
for (const [id, pending] of this.pendingRequests.entries()) {
|
601
|
+
if (pending) {
|
602
|
+
clearTimeout(pending.timer);
|
603
|
+
this.pendingRequests.delete(id);
|
604
|
+
}
|
605
|
+
}
|
606
|
+
}
|
607
|
+
onError(_error, ws) {
|
608
|
+
this.onClose(ws);
|
609
|
+
}
|
610
|
+
async handleRequest(ws, message) {
|
611
|
+
if (!message.id || !message.module || !message.action) {
|
612
|
+
throw new Error("Invalid request message");
|
613
|
+
}
|
614
|
+
const timer = setTimeout(() => {
|
615
|
+
const pending = this.pendingRequests.get(message.id);
|
616
|
+
if (pending) {
|
617
|
+
this.pendingRequests.delete(message.id);
|
618
|
+
this.microservice.updateMethodStats(
|
619
|
+
pending.module,
|
620
|
+
pending.action,
|
621
|
+
0,
|
622
|
+
false
|
623
|
+
);
|
624
|
+
ws.send(
|
625
|
+
ejson3__default.default.stringify({
|
626
|
+
id: message.id,
|
627
|
+
success: false,
|
628
|
+
error: "Request timeout"
|
629
|
+
})
|
630
|
+
);
|
631
|
+
}
|
632
|
+
}, this.options.timeout);
|
633
|
+
this.pendingRequests.set(message.id, {
|
634
|
+
timer,
|
635
|
+
module: message.module,
|
636
|
+
action: message.action
|
637
|
+
});
|
638
|
+
return (async () => {
|
639
|
+
const startTime = Date.now();
|
640
|
+
try {
|
641
|
+
const handler = this.microservice.getActionHandler(
|
642
|
+
message.module,
|
643
|
+
message.action
|
644
|
+
);
|
645
|
+
const args = message.args ? Object.values(message.args) : [];
|
646
|
+
const result = await handler.handle(args);
|
647
|
+
if (handler.metadata.stream) {
|
648
|
+
try {
|
649
|
+
for await (const value of result) {
|
650
|
+
await this.sendMessage(ws, {
|
651
|
+
id: message.id,
|
652
|
+
type: "stream",
|
653
|
+
data: value,
|
654
|
+
done: false
|
655
|
+
});
|
656
|
+
}
|
657
|
+
await this.sendMessage(ws, {
|
658
|
+
id: message.id,
|
659
|
+
type: "stream",
|
660
|
+
done: true
|
661
|
+
});
|
662
|
+
return null;
|
663
|
+
} catch (error) {
|
664
|
+
throw error;
|
665
|
+
}
|
666
|
+
}
|
667
|
+
const responseTime = Date.now() - startTime;
|
668
|
+
this.microservice.updateMethodStats(
|
669
|
+
message.module,
|
670
|
+
message.action,
|
671
|
+
responseTime,
|
672
|
+
true
|
673
|
+
);
|
674
|
+
return {
|
675
|
+
id: message.id,
|
676
|
+
success: true,
|
677
|
+
data: result
|
678
|
+
};
|
679
|
+
} catch (error) {
|
680
|
+
this.microservice.updateMethodStats(
|
681
|
+
message.module,
|
682
|
+
message.action,
|
683
|
+
0,
|
684
|
+
false
|
685
|
+
);
|
686
|
+
return {
|
687
|
+
id: message.id,
|
688
|
+
success: false,
|
689
|
+
error: error.message
|
690
|
+
};
|
691
|
+
} finally {
|
692
|
+
const pending = this.pendingRequests.get(message.id);
|
693
|
+
if (pending) {
|
694
|
+
clearTimeout(pending.timer);
|
695
|
+
this.pendingRequests.delete(message.id);
|
696
|
+
}
|
697
|
+
}
|
698
|
+
})();
|
699
|
+
}
|
700
|
+
close() {
|
701
|
+
for (const socket of this.sockets) {
|
702
|
+
socket.close();
|
703
|
+
}
|
704
|
+
for (const pending of this.pendingRequests.values()) {
|
705
|
+
clearTimeout(pending.timer);
|
706
|
+
}
|
707
|
+
this.sockets.clear();
|
708
|
+
this.pendingRequests.clear();
|
709
|
+
}
|
710
|
+
};
|
711
|
+
|
712
|
+
// core/core.ts
|
713
|
+
var ServiceContext = {};
|
714
|
+
var Microservice = class {
|
715
|
+
app;
|
716
|
+
nodeWebSocket;
|
717
|
+
codeCache;
|
718
|
+
waitingInitialization;
|
719
|
+
etcdClient;
|
720
|
+
serviceKey;
|
721
|
+
statsMap = /* @__PURE__ */ new Map();
|
722
|
+
lease;
|
723
|
+
scheduler;
|
724
|
+
abortController;
|
725
|
+
isShuttingDown = false;
|
726
|
+
statisticsTimer;
|
727
|
+
wsHandler;
|
728
|
+
actionHandlers = /* @__PURE__ */ new Map();
|
729
|
+
modules = /* @__PURE__ */ new Map();
|
730
|
+
fetch;
|
731
|
+
options;
|
732
|
+
cache;
|
733
|
+
serviceId;
|
734
|
+
constructor(options) {
|
735
|
+
this.app = new hono.Hono();
|
736
|
+
this.nodeWebSocket = nodeWs.createNodeWebSocket({ app: this.app });
|
737
|
+
this.serviceId = crypto.randomUUID();
|
738
|
+
this.options = {
|
739
|
+
prefix: "/api",
|
740
|
+
name: "microservice",
|
741
|
+
...options
|
742
|
+
};
|
743
|
+
this.fetch = this.app.request;
|
744
|
+
this.waitingInitialization = this.initialize();
|
745
|
+
ServiceContext.service = this;
|
746
|
+
}
|
747
|
+
async initialize() {
|
748
|
+
this.cache = new lruCache.LRUCache({
|
749
|
+
max: 1e3,
|
750
|
+
ttl: 1e3 * 60 * 10
|
751
|
+
});
|
752
|
+
if (this.options.etcd) {
|
753
|
+
this.initEtcd(this.options.etcd);
|
754
|
+
}
|
755
|
+
await this.initModules();
|
756
|
+
this.initRoutes();
|
757
|
+
this.initShutdownHandlers();
|
758
|
+
if (this.options.events?.onStats) {
|
759
|
+
this.initStatsEventManager();
|
760
|
+
}
|
761
|
+
await this.registerService(true);
|
762
|
+
}
|
763
|
+
async initModules() {
|
764
|
+
for (const ModuleClass of this.options.modules) {
|
765
|
+
const moduleInstance = new ModuleClass();
|
766
|
+
const metadata = getModuleMetadata(ModuleClass);
|
767
|
+
if (!metadata) {
|
768
|
+
throw new Error(
|
769
|
+
`Module ${ModuleClass.name} is not decorated with @Module`
|
770
|
+
);
|
771
|
+
}
|
772
|
+
const moduleName = metadata.name;
|
773
|
+
logger_default.info(`[ \u6CE8\u518C\u6A21\u5757 ] ${moduleName} ${metadata.options.description}`);
|
774
|
+
this.modules.set(moduleName, moduleInstance);
|
775
|
+
const actions = getActionMetadata(ModuleClass.prototype);
|
776
|
+
for (const [actionName, actionMetadata] of Object.entries(actions)) {
|
777
|
+
const handler = new ActionHandler(
|
778
|
+
moduleInstance,
|
779
|
+
actionName,
|
780
|
+
actionMetadata,
|
781
|
+
this,
|
782
|
+
moduleName
|
783
|
+
);
|
784
|
+
this.actionHandlers.set(`${moduleName}.${actionName}`, handler);
|
785
|
+
logger_default.info(
|
786
|
+
`[ \u6CE8\u518C\u52A8\u4F5C ] ${moduleName}.${actionName} ${actionMetadata.description}`
|
787
|
+
);
|
788
|
+
}
|
789
|
+
const schedules = getScheduleMetadata(ModuleClass.prototype);
|
790
|
+
if (schedules && Object.keys(schedules).length > 0) {
|
791
|
+
if (!this.scheduler && this.etcdClient) {
|
792
|
+
this.scheduler = new Scheduler(this.etcdClient);
|
793
|
+
}
|
794
|
+
for (const [methodName, metadata2] of Object.entries(schedules)) {
|
795
|
+
const electionKey = `${this.options.name}/${moduleName}/schedules/${metadata2.name}`;
|
796
|
+
this.scheduler.startSchedule(
|
797
|
+
this.serviceId,
|
798
|
+
moduleName,
|
799
|
+
methodName,
|
800
|
+
electionKey,
|
801
|
+
metadata2,
|
802
|
+
ModuleClass.prototype[methodName].bind(moduleInstance)
|
803
|
+
);
|
804
|
+
logger_default.info(
|
805
|
+
`[ \u542F\u52A8\u8C03\u5EA6\u4EFB\u52A1 ] ${moduleName}.${metadata2.name} interval = ${metadata2.interval} mode = ${metadata2.mode}`
|
806
|
+
);
|
807
|
+
}
|
808
|
+
}
|
809
|
+
}
|
810
|
+
if (this.options.generateClient) {
|
811
|
+
logger_default.info(`[ \u751F\u6210\u5BA2\u6237\u7AEF\u4EE3\u7801 ] ${this.options.generateClient}`);
|
812
|
+
await this.generateClientCode();
|
813
|
+
}
|
814
|
+
}
|
815
|
+
initRoutes() {
|
816
|
+
const startTime = Date.now();
|
817
|
+
const prefix = this.options.prefix || "/api";
|
818
|
+
this.app.get(prefix, (ctx) => {
|
819
|
+
const name = this.options.name ?? "Microservice";
|
820
|
+
const version = this.options.version ?? "1.0.0";
|
821
|
+
return ctx.text(`${name} is running. version: ${version}`);
|
822
|
+
});
|
823
|
+
this.app.get(`${prefix}/health`, (ctx) => {
|
824
|
+
return ctx.json({
|
825
|
+
status: "ok",
|
826
|
+
uptime: Date.now() - startTime,
|
827
|
+
timestamp: Date.now()
|
828
|
+
});
|
829
|
+
});
|
830
|
+
this.app.get(`${prefix}/status`, (ctx) => {
|
831
|
+
return ctx.json({
|
832
|
+
id: this.serviceId,
|
833
|
+
name: this.options.name,
|
834
|
+
version: this.options.version,
|
835
|
+
env: this.options.env,
|
836
|
+
modules: this.getModules(),
|
837
|
+
stats: Object.fromEntries(this.statsMap)
|
838
|
+
});
|
839
|
+
});
|
840
|
+
this.app.get(`${prefix}/client.ts`, async (ctx) => {
|
841
|
+
ctx.header("Content-Type", "application/typescript; charset=utf-8");
|
842
|
+
ctx.header("Content-Disposition", "attachment; filename=client.ts");
|
843
|
+
ctx.header("Cache-Control", "public, max-age=3600");
|
844
|
+
return ctx.body(await this.clientCode());
|
845
|
+
});
|
846
|
+
this.app.post(`${prefix}/:moduleName/:actionName`, this.handleRequest);
|
847
|
+
if (this.options.websocket?.enabled) {
|
848
|
+
this.wsHandler = new WebSocketHandler(this);
|
849
|
+
this.app.get(
|
850
|
+
`${prefix}/ws`,
|
851
|
+
this.nodeWebSocket.upgradeWebSocket((_ctx) => {
|
852
|
+
const wsHandler = new WebSocketHandler(this, {
|
853
|
+
timeout: this.options.websocket?.timeout
|
854
|
+
});
|
855
|
+
return {
|
856
|
+
onOpen(_event, ws) {
|
857
|
+
wsHandler.onOpen(ws);
|
858
|
+
},
|
859
|
+
onMessage(message, ws) {
|
860
|
+
wsHandler.onMessage(message, ws);
|
861
|
+
},
|
862
|
+
onClose(_evt, ws) {
|
863
|
+
wsHandler.onClose(ws);
|
864
|
+
},
|
865
|
+
onError(error, ws) {
|
866
|
+
wsHandler.onError(error, ws);
|
867
|
+
}
|
868
|
+
};
|
869
|
+
})
|
870
|
+
);
|
871
|
+
}
|
872
|
+
}
|
873
|
+
async generateClientCode() {
|
874
|
+
if (typeof this.options.generateClient === "string" || this.options.generateClient instanceof URL) {
|
875
|
+
const code = await this.clientCode();
|
876
|
+
await fs__default.default.writeFile(this.options.generateClient, code);
|
877
|
+
}
|
878
|
+
}
|
879
|
+
async clientCode() {
|
880
|
+
if (!this.codeCache) {
|
881
|
+
this.codeCache = await formatCode(
|
882
|
+
await generateClientCode(this.getModules(true))
|
883
|
+
);
|
884
|
+
}
|
885
|
+
return this.codeCache;
|
886
|
+
}
|
887
|
+
initEtcd(config) {
|
888
|
+
this.etcdClient = new etcd3.Etcd3({
|
889
|
+
hosts: config.hosts,
|
890
|
+
auth: config.auth
|
891
|
+
});
|
892
|
+
const serviceName = this.options.name || "microservice";
|
893
|
+
const namespace = config.namespace ? `${config.namespace}/` : "";
|
894
|
+
this.serviceKey = `${namespace}services/${serviceName}/${this.serviceId}`;
|
895
|
+
const ttl = config.ttl || 10;
|
896
|
+
this.lease = this.etcdClient.lease(ttl);
|
897
|
+
ServiceContext.lease = this.lease;
|
898
|
+
ServiceContext.etcdClient = this.etcdClient;
|
899
|
+
}
|
900
|
+
/**
|
901
|
+
* 注册服务
|
902
|
+
*/
|
903
|
+
async registerService(update = false) {
|
904
|
+
const serviceInfo = {
|
905
|
+
id: this.serviceId,
|
906
|
+
name: this.options.name,
|
907
|
+
version: this.options.version,
|
908
|
+
prefix: this.options.prefix,
|
909
|
+
env: this.options.env,
|
910
|
+
modules: this.getModules(false)
|
911
|
+
};
|
912
|
+
if (update) {
|
913
|
+
await this.options.events?.onRegister?.(serviceInfo)?.catch((e) => {
|
914
|
+
logger_default.error(`Failed to emit register event: ${e}`);
|
915
|
+
});
|
916
|
+
}
|
917
|
+
}
|
918
|
+
/**
|
919
|
+
* 更新方法统计信息
|
920
|
+
*/
|
921
|
+
updateMethodStats(moduleName, methodName, responseTime, success, cacheHit = false) {
|
922
|
+
const key = `${moduleName}.${methodName}`;
|
923
|
+
let stats = this.statsMap.get(key);
|
924
|
+
if (!stats) {
|
925
|
+
stats = {
|
926
|
+
totalCalls: 0,
|
927
|
+
successCalls: 0,
|
928
|
+
failureCalls: 0,
|
929
|
+
avgResponseTime: 0,
|
930
|
+
maxResponseTime: 0,
|
931
|
+
minResponseTime: Number.MAX_SAFE_INTEGER,
|
932
|
+
lastUpdateTime: Date.now(),
|
933
|
+
cacheHit: 0
|
934
|
+
};
|
935
|
+
this.statsMap.set(key, stats);
|
936
|
+
}
|
937
|
+
stats.totalCalls++;
|
938
|
+
if (success) {
|
939
|
+
stats.successCalls++;
|
940
|
+
} else {
|
941
|
+
stats.failureCalls++;
|
942
|
+
}
|
943
|
+
stats.avgResponseTime = (stats.avgResponseTime * (stats.totalCalls - 1) + responseTime) / stats.totalCalls;
|
944
|
+
stats.maxResponseTime = Math.max(stats.maxResponseTime, responseTime);
|
945
|
+
stats.minResponseTime = Math.min(stats.minResponseTime, responseTime);
|
946
|
+
stats.lastUpdateTime = Date.now();
|
947
|
+
stats.cacheHit += cacheHit ? 1 : 0;
|
948
|
+
}
|
949
|
+
/**
|
950
|
+
* 获取 Hono 应用实例
|
951
|
+
*/
|
952
|
+
getApp() {
|
953
|
+
return this.app;
|
954
|
+
}
|
955
|
+
/**
|
956
|
+
* 启动服务
|
957
|
+
*/
|
958
|
+
async start(port = 3e3, silent = false) {
|
959
|
+
await this.waitingInitialization;
|
960
|
+
const prefix = this.options.prefix ?? "/api";
|
961
|
+
this.abortController = new AbortController();
|
962
|
+
!silent && console.log("");
|
963
|
+
const server = nodeServer.serve({ fetch: this.app.fetch, port });
|
964
|
+
this.nodeWebSocket.injectWebSocket(server);
|
965
|
+
if (!silent) {
|
966
|
+
console.log(
|
967
|
+
`\u{1F680} Microservice is running on http://localhost:${port}${prefix}`
|
968
|
+
);
|
969
|
+
console.log(
|
970
|
+
`\u{1F4DA} Client SDK available at http://localhost:${port}${prefix}/client.ts`
|
971
|
+
);
|
972
|
+
this.options.websocket?.enabled && console.info(
|
973
|
+
`\u{1F517} WebSocket available at http://localhost:${port}${prefix}/ws`
|
974
|
+
);
|
975
|
+
}
|
976
|
+
}
|
977
|
+
/**
|
978
|
+
* 获取所有模块的元数据
|
979
|
+
*/
|
980
|
+
getModules(withTypes = false) {
|
981
|
+
const modules = {};
|
982
|
+
for (const [moduleName, moduleInstance] of this.modules) {
|
983
|
+
const metadata = getModuleMetadata(moduleInstance.constructor);
|
984
|
+
if (!metadata) continue;
|
985
|
+
modules[moduleName] = {
|
986
|
+
name: metadata.name,
|
987
|
+
version: metadata.options.version,
|
988
|
+
description: metadata.options.description,
|
989
|
+
printError: metadata.options.printError,
|
990
|
+
actions: Object.fromEntries(
|
991
|
+
Array.from(this.actionHandlers.entries()).filter(([key]) => key.startsWith(moduleName + ".")).map(([key, handler]) => {
|
992
|
+
const metadata2 = { ...handler.metadata };
|
993
|
+
if (!withTypes) {
|
994
|
+
delete metadata2.params;
|
995
|
+
delete metadata2.returns;
|
996
|
+
}
|
997
|
+
return [key.slice(moduleName.length + 1), metadata2];
|
998
|
+
})
|
999
|
+
)
|
1000
|
+
};
|
1001
|
+
}
|
1002
|
+
return modules;
|
1003
|
+
}
|
1004
|
+
/**
|
1005
|
+
* 停止服务
|
1006
|
+
*/
|
1007
|
+
async stop() {
|
1008
|
+
if (this.wsHandler) {
|
1009
|
+
this.wsHandler.close();
|
1010
|
+
}
|
1011
|
+
if (this.abortController) {
|
1012
|
+
this.abortController.abort();
|
1013
|
+
}
|
1014
|
+
if (this.scheduler) {
|
1015
|
+
await this.scheduler.stop();
|
1016
|
+
}
|
1017
|
+
if (this.options.events?.onStats && this.options.events.forceEmitOnShutdown) {
|
1018
|
+
await this.updateStats().catch((e) => {
|
1019
|
+
logger_default.error("Failed to emit final stats:", e);
|
1020
|
+
});
|
1021
|
+
}
|
1022
|
+
if (this.etcdClient && this.serviceKey) {
|
1023
|
+
await this.etcdClient.delete().key(this.serviceKey);
|
1024
|
+
this.etcdClient.close();
|
1025
|
+
}
|
1026
|
+
}
|
1027
|
+
/**
|
1028
|
+
* 初始化停机处理
|
1029
|
+
*/
|
1030
|
+
initShutdownHandlers() {
|
1031
|
+
process.on("SIGINT", () => {
|
1032
|
+
logger_default.info(`
|
1033
|
+
Received SIGINT signal`);
|
1034
|
+
this.gracefulShutdown();
|
1035
|
+
});
|
1036
|
+
process.on("unhandledrejection", (event) => {
|
1037
|
+
logger_default.error("Unhandled rejection:", event.reason);
|
1038
|
+
});
|
1039
|
+
process.on("error", (event) => {
|
1040
|
+
logger_default.error("Uncaught error:", event.error);
|
1041
|
+
});
|
1042
|
+
}
|
1043
|
+
/**
|
1044
|
+
* 优雅停机
|
1045
|
+
*/
|
1046
|
+
async gracefulShutdown() {
|
1047
|
+
if (this.isShuttingDown) return;
|
1048
|
+
this.isShuttingDown = true;
|
1049
|
+
logger_default.info("\nGraceful shutdown initiated...");
|
1050
|
+
if (this.statisticsTimer) clearInterval(this.statisticsTimer);
|
1051
|
+
try {
|
1052
|
+
await this.stop();
|
1053
|
+
logger_default.info("Graceful shutdown completed");
|
1054
|
+
} catch (error) {
|
1055
|
+
logger_default.error("Error during shutdown:", error);
|
1056
|
+
process.exit(1);
|
1057
|
+
} finally {
|
1058
|
+
process.exit(0);
|
1059
|
+
}
|
1060
|
+
}
|
1061
|
+
initStatsEventManager() {
|
1062
|
+
this.statisticsTimer = setInterval(async () => {
|
1063
|
+
await this.updateStats();
|
1064
|
+
}, this.options.events.interval ?? 1e4);
|
1065
|
+
}
|
1066
|
+
async updateStats() {
|
1067
|
+
const now = Date.now();
|
1068
|
+
const { onStats } = this.options.events;
|
1069
|
+
this.registerService(true);
|
1070
|
+
for (const [key, stats] of this.statsMap.entries()) {
|
1071
|
+
if (stats) {
|
1072
|
+
const [moduleName, actionName] = key.split(".");
|
1073
|
+
try {
|
1074
|
+
await onStats?.({
|
1075
|
+
service: {
|
1076
|
+
env: this.options.env,
|
1077
|
+
id: this.serviceId,
|
1078
|
+
name: this.options.name,
|
1079
|
+
version: this.options.version
|
1080
|
+
},
|
1081
|
+
module: moduleName,
|
1082
|
+
action: actionName,
|
1083
|
+
stats,
|
1084
|
+
startTime: stats.lastUpdateTime,
|
1085
|
+
endTime: now
|
1086
|
+
});
|
1087
|
+
this.statsMap.delete(key);
|
1088
|
+
} catch (error) {
|
1089
|
+
logger_default.error(`Failed to emit stats event for ${key}:`, error);
|
1090
|
+
}
|
1091
|
+
}
|
1092
|
+
}
|
1093
|
+
}
|
1094
|
+
getActionHandler(moduleName, actionName) {
|
1095
|
+
const key = `${moduleName}.${actionName}`;
|
1096
|
+
const handler = this.actionHandlers.get(key);
|
1097
|
+
if (!handler) {
|
1098
|
+
throw new Error(`Action ${actionName} not found in module ${moduleName}`);
|
1099
|
+
}
|
1100
|
+
return handler;
|
1101
|
+
}
|
1102
|
+
handleRequest = async (ctx) => {
|
1103
|
+
const { moduleName, actionName } = ctx.req.param();
|
1104
|
+
const handler = this.getActionHandler(moduleName, actionName);
|
1105
|
+
try {
|
1106
|
+
const paramsText = await ctx.req.text();
|
1107
|
+
const result = await handler.handle(paramsText);
|
1108
|
+
if (handler.metadata.stream) {
|
1109
|
+
const encoder = new TextEncoder();
|
1110
|
+
const stream = new ReadableStream({
|
1111
|
+
async start(controller) {
|
1112
|
+
try {
|
1113
|
+
for await (const value of result) {
|
1114
|
+
const response = { value, done: false };
|
1115
|
+
const chunk = encoder.encode(ejson3__default.default.stringify(response) + "\n");
|
1116
|
+
controller.enqueue(chunk);
|
1117
|
+
}
|
1118
|
+
controller.enqueue(
|
1119
|
+
encoder.encode(
|
1120
|
+
ejson3__default.default.stringify({ done: true }) + "\n"
|
1121
|
+
)
|
1122
|
+
);
|
1123
|
+
controller.close();
|
1124
|
+
} catch (error) {
|
1125
|
+
const response = {
|
1126
|
+
error: error.message,
|
1127
|
+
done: true
|
1128
|
+
};
|
1129
|
+
controller.enqueue(
|
1130
|
+
encoder.encode(ejson3__default.default.stringify(response) + "\n")
|
1131
|
+
);
|
1132
|
+
controller.close();
|
1133
|
+
}
|
1134
|
+
}
|
1135
|
+
});
|
1136
|
+
return new Response(stream, {
|
1137
|
+
headers: {
|
1138
|
+
"Content-Type": "text/event-stream",
|
1139
|
+
"Cache-Control": "no-cache",
|
1140
|
+
Connection: "keep-alive"
|
1141
|
+
}
|
1142
|
+
});
|
1143
|
+
}
|
1144
|
+
return ctx.text(ejson3__default.default.stringify({ success: true, data: result }));
|
1145
|
+
} catch (error) {
|
1146
|
+
return ctx.json({ success: false, error: error.message });
|
1147
|
+
}
|
1148
|
+
};
|
1149
|
+
async init() {
|
1150
|
+
await this.waitingInitialization;
|
1151
|
+
}
|
1152
|
+
/**
|
1153
|
+
* 获取所有注册的服务
|
1154
|
+
*/
|
1155
|
+
async getServices() {
|
1156
|
+
if (!this.etcdClient) {
|
1157
|
+
throw new Error("etcd is not configured");
|
1158
|
+
}
|
1159
|
+
const namespace = this.options.etcd?.namespace ? `${this.options.etcd.namespace}/` : "";
|
1160
|
+
const servicesKey = `${namespace}services`;
|
1161
|
+
const services = await this.etcdClient.getAll().prefix(servicesKey);
|
1162
|
+
return Object.values(services).map((value) => JSON.parse(value));
|
1163
|
+
}
|
1164
|
+
/**
|
1165
|
+
* 获取指定服务的实例
|
1166
|
+
*/
|
1167
|
+
async getServiceInstances(serviceName) {
|
1168
|
+
if (!this.etcdClient) {
|
1169
|
+
throw new Error("etcd is not configured");
|
1170
|
+
}
|
1171
|
+
const namespace = this.options.etcd?.namespace ? `${this.options.etcd.namespace}/` : "";
|
1172
|
+
const serviceKey = `${namespace}services/${serviceName}`;
|
1173
|
+
const instances = await this.etcdClient.getAll().prefix(serviceKey);
|
1174
|
+
return Object.values(instances).map((value) => JSON.parse(value));
|
1175
|
+
}
|
1176
|
+
};
|
1177
|
+
|
1178
|
+
// utils/checker.ts
|
1179
|
+
async function startCheck(checkers, pass) {
|
1180
|
+
logger_default.info("[ \u9884\u68C0\u5F00\u59CB ]");
|
1181
|
+
for (const [index, checker] of checkers.entries()) {
|
1182
|
+
const seq = index + 1;
|
1183
|
+
logger_default.info(`${seq}. ${checker.name}`);
|
1184
|
+
try {
|
1185
|
+
if (checker.skip) {
|
1186
|
+
logger_default.warn(`${seq}. ${checker.name} [\u8DF3\u8FC7]`);
|
1187
|
+
continue;
|
1188
|
+
}
|
1189
|
+
await checker.check();
|
1190
|
+
logger_default.info(`${seq}. ${checker.name} [\u6210\u529F]`);
|
1191
|
+
} catch (error) {
|
1192
|
+
logger_default.error(`${seq}. ${checker.name} [\u5931\u8D25]`);
|
1193
|
+
throw error;
|
1194
|
+
}
|
1195
|
+
}
|
1196
|
+
logger_default.info("[ \u9884\u68C0\u5B8C\u6210 ]");
|
1197
|
+
if (pass) await pass();
|
1198
|
+
}
|
1199
|
+
|
1200
|
+
Object.defineProperty(exports, "dayjs", {
|
1201
|
+
enumerable: true,
|
1202
|
+
get: function () { return dayjs__default.default; }
|
1203
|
+
});
|
1204
|
+
exports.Action = Action;
|
1205
|
+
exports.Microservice = Microservice;
|
1206
|
+
exports.Module = Module;
|
1207
|
+
exports.Schedule = Schedule;
|
1208
|
+
exports.ScheduleMode = ScheduleMode;
|
1209
|
+
exports.ServiceContext = ServiceContext;
|
1210
|
+
exports.logger = logger_default;
|
1211
|
+
exports.startCheck = startCheck;
|
1212
|
+
Object.keys(zod).forEach(function (k) {
|
1213
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
1214
|
+
enumerable: true,
|
1215
|
+
get: function () { return zod[k]; }
|
1216
|
+
});
|
1217
|
+
});
|