nebula-engine 1.0.0-beta3
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 +1255 -0
- package/dist/index.d.mts +1984 -0
- package/dist/index.d.ts +1984 -0
- package/dist/index.js +3533 -0
- package/dist/index.mjs +3457 -0
- package/docs/plugin-system.md +733 -0
- package/package.json +54 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3457 @@
|
|
|
1
|
+
import winston, { format } from 'winston';
|
|
2
|
+
import { html } from 'hono/html';
|
|
3
|
+
import { jsx, jsxs } from 'hono/jsx/jsx-runtime';
|
|
4
|
+
import * as ejson from 'ejson';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export { z } from 'zod';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
|
9
|
+
import { promises } from 'fs';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import prettier from 'prettier';
|
|
12
|
+
import { serve } from '@hono/node-server';
|
|
13
|
+
import { Hono } from 'hono';
|
|
14
|
+
export { Context, MiddlewareHandler } from 'hono';
|
|
15
|
+
import * as net from 'net';
|
|
16
|
+
import { customAlphabet } from 'nanoid';
|
|
17
|
+
|
|
18
|
+
// src/core/types.ts
|
|
19
|
+
var PluginPriority = /* @__PURE__ */ ((PluginPriority2) => {
|
|
20
|
+
PluginPriority2[PluginPriority2["SYSTEM"] = 50] = "SYSTEM";
|
|
21
|
+
PluginPriority2[PluginPriority2["SECURITY"] = 100] = "SECURITY";
|
|
22
|
+
PluginPriority2[PluginPriority2["LOGGING"] = 200] = "LOGGING";
|
|
23
|
+
PluginPriority2[PluginPriority2["BUSINESS"] = 300] = "BUSINESS";
|
|
24
|
+
PluginPriority2[PluginPriority2["PERFORMANCE"] = 400] = "PERFORMANCE";
|
|
25
|
+
PluginPriority2[PluginPriority2["ROUTE"] = 1e3] = "ROUTE";
|
|
26
|
+
return PluginPriority2;
|
|
27
|
+
})(PluginPriority || {});
|
|
28
|
+
|
|
29
|
+
// src/metadata/metadata.ts
|
|
30
|
+
var DECORATED_KEYS_KEY = /* @__PURE__ */ Symbol.for("nebula:decoratedKeys");
|
|
31
|
+
var keyToClassesMap = /* @__PURE__ */ new Map();
|
|
32
|
+
var classMetadataStore = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
var methodMetadataStore = /* @__PURE__ */ new WeakMap();
|
|
34
|
+
var fieldMetadataStore = /* @__PURE__ */ new WeakMap();
|
|
35
|
+
function getOrCreateClassMetadataMap(targetClass) {
|
|
36
|
+
let metadataMap = classMetadataStore.get(targetClass);
|
|
37
|
+
if (!metadataMap) {
|
|
38
|
+
metadataMap = /* @__PURE__ */ new Map();
|
|
39
|
+
classMetadataStore.set(targetClass, metadataMap);
|
|
40
|
+
}
|
|
41
|
+
return metadataMap;
|
|
42
|
+
}
|
|
43
|
+
function getOrCreateDecoratedKeysSet(targetClass) {
|
|
44
|
+
const metadataMap = getOrCreateClassMetadataMap(targetClass);
|
|
45
|
+
let decoratedKeys = metadataMap.get(DECORATED_KEYS_KEY);
|
|
46
|
+
if (!decoratedKeys) {
|
|
47
|
+
decoratedKeys = /* @__PURE__ */ new Set();
|
|
48
|
+
metadataMap.set(DECORATED_KEYS_KEY, decoratedKeys);
|
|
49
|
+
}
|
|
50
|
+
return decoratedKeys;
|
|
51
|
+
}
|
|
52
|
+
function registerKeyClassRelation(targetClass, metadataKey) {
|
|
53
|
+
const decoratedKeys = getOrCreateDecoratedKeysSet(targetClass);
|
|
54
|
+
decoratedKeys.add(metadataKey);
|
|
55
|
+
if (!keyToClassesMap.has(metadataKey)) {
|
|
56
|
+
keyToClassesMap.set(metadataKey, /* @__PURE__ */ new Set());
|
|
57
|
+
}
|
|
58
|
+
keyToClassesMap.get(metadataKey).add(targetClass);
|
|
59
|
+
}
|
|
60
|
+
function createClassDecorator(metadataKey = /* @__PURE__ */ Symbol.for("nebula:classMetadata")) {
|
|
61
|
+
return function(metadata) {
|
|
62
|
+
return function(target, context) {
|
|
63
|
+
context.addInitializer(function() {
|
|
64
|
+
registerKeyClassRelation(target, metadataKey);
|
|
65
|
+
const metadataMap = getOrCreateClassMetadataMap(target);
|
|
66
|
+
const existingMetadata = metadataMap.get(metadataKey) || {};
|
|
67
|
+
const mergedMetadata = {
|
|
68
|
+
...existingMetadata,
|
|
69
|
+
...metadata
|
|
70
|
+
};
|
|
71
|
+
metadataMap.set(metadataKey, mergedMetadata);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function createMethodDecorator(metadataKey = /* @__PURE__ */ Symbol.for("nebula:methodMetadata")) {
|
|
77
|
+
return function(metadata) {
|
|
78
|
+
return function(target, context) {
|
|
79
|
+
const methodName = context.name.toString();
|
|
80
|
+
const collectMetadata = (targetClass2) => {
|
|
81
|
+
if (!targetClass2) return;
|
|
82
|
+
registerKeyClassRelation(targetClass2, metadataKey);
|
|
83
|
+
let metadataMap = methodMetadataStore.get(targetClass2);
|
|
84
|
+
if (!metadataMap) {
|
|
85
|
+
metadataMap = /* @__PURE__ */ new Map();
|
|
86
|
+
methodMetadataStore.set(targetClass2, metadataMap);
|
|
87
|
+
}
|
|
88
|
+
const existingMetadataMap = metadataMap.get(metadataKey) || {};
|
|
89
|
+
const existingMetadata = existingMetadataMap[methodName] || [];
|
|
90
|
+
const newMetadata = {
|
|
91
|
+
...metadata,
|
|
92
|
+
type: metadata.type
|
|
93
|
+
};
|
|
94
|
+
existingMetadataMap[methodName] = [...existingMetadata, newMetadata];
|
|
95
|
+
metadataMap.set(metadataKey, existingMetadataMap);
|
|
96
|
+
};
|
|
97
|
+
let targetClass = null;
|
|
98
|
+
try {
|
|
99
|
+
if (target.constructor && typeof target.constructor === "function") {
|
|
100
|
+
targetClass = target.constructor;
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
}
|
|
104
|
+
if (targetClass) {
|
|
105
|
+
collectMetadata(targetClass);
|
|
106
|
+
}
|
|
107
|
+
context.addInitializer(function() {
|
|
108
|
+
const instanceClass = this.constructor;
|
|
109
|
+
if (instanceClass && instanceClass !== targetClass) {
|
|
110
|
+
collectMetadata(instanceClass);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createFieldDecorator(metadataKey = /* @__PURE__ */ Symbol.for("nebula:fieldMetadata")) {
|
|
117
|
+
return function(metadata) {
|
|
118
|
+
return function(target, context) {
|
|
119
|
+
const fieldName = context.name.toString();
|
|
120
|
+
const collectMetadata = (targetClass) => {
|
|
121
|
+
if (!targetClass) return;
|
|
122
|
+
registerKeyClassRelation(targetClass, metadataKey);
|
|
123
|
+
let metadataMap = fieldMetadataStore.get(targetClass);
|
|
124
|
+
if (!metadataMap) {
|
|
125
|
+
metadataMap = /* @__PURE__ */ new Map();
|
|
126
|
+
fieldMetadataStore.set(targetClass, metadataMap);
|
|
127
|
+
}
|
|
128
|
+
const existingMetadataMap = metadataMap.get(metadataKey) || {};
|
|
129
|
+
const existingMetadata = existingMetadataMap[fieldName] || [];
|
|
130
|
+
const newMetadata = {
|
|
131
|
+
...metadata,
|
|
132
|
+
type: metadata.type
|
|
133
|
+
};
|
|
134
|
+
existingMetadataMap[fieldName] = [...existingMetadata, newMetadata];
|
|
135
|
+
metadataMap.set(metadataKey, existingMetadataMap);
|
|
136
|
+
};
|
|
137
|
+
context.addInitializer(function() {
|
|
138
|
+
const instanceClass = this.constructor;
|
|
139
|
+
if (instanceClass) {
|
|
140
|
+
collectMetadata(instanceClass);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function getClassesByKey(metadataKey) {
|
|
147
|
+
return keyToClassesMap.get(metadataKey) || /* @__PURE__ */ new Set();
|
|
148
|
+
}
|
|
149
|
+
function getClassMetadata(target, metadataKey = /* @__PURE__ */ Symbol.for("nebula:classMetadata")) {
|
|
150
|
+
let targetClass = null;
|
|
151
|
+
if (target && typeof target === "function" && target.prototype) {
|
|
152
|
+
targetClass = target;
|
|
153
|
+
} else if (target && target.constructor && typeof target.constructor === "function") {
|
|
154
|
+
targetClass = target.constructor;
|
|
155
|
+
}
|
|
156
|
+
if (!targetClass) {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
const metadataMap = classMetadataStore.get(targetClass);
|
|
160
|
+
if (!metadataMap) {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
return metadataMap.get(metadataKey) || {};
|
|
164
|
+
}
|
|
165
|
+
function getAllMethodMetadata(target, metadataKey = /* @__PURE__ */ Symbol.for("nebula:methodMetadata")) {
|
|
166
|
+
const result = /* @__PURE__ */ new Map();
|
|
167
|
+
let targetClass = null;
|
|
168
|
+
if (target && typeof target === "function" && target.prototype) {
|
|
169
|
+
targetClass = target;
|
|
170
|
+
} else if (target && target.constructor && typeof target.constructor === "function") {
|
|
171
|
+
targetClass = target.constructor;
|
|
172
|
+
}
|
|
173
|
+
if (!targetClass) {
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
const metadataMap = methodMetadataStore.get(targetClass);
|
|
177
|
+
if (!metadataMap) {
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
const allMetadata = metadataMap.get(metadataKey) || {};
|
|
181
|
+
for (const [methodName, metadataList] of Object.entries(allMetadata)) {
|
|
182
|
+
if (metadataList.length > 0) {
|
|
183
|
+
result.set(methodName, metadataList);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
function getAllFieldMetadata(target, metadataKey = /* @__PURE__ */ Symbol.for("nebula:fieldMetadata")) {
|
|
189
|
+
const result = /* @__PURE__ */ new Map();
|
|
190
|
+
let targetClass = null;
|
|
191
|
+
if (target && typeof target === "function" && target.prototype) {
|
|
192
|
+
targetClass = target;
|
|
193
|
+
} else if (target && target.constructor && typeof target.constructor === "function") {
|
|
194
|
+
targetClass = target.constructor;
|
|
195
|
+
}
|
|
196
|
+
if (!targetClass) {
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
const metadataMap = fieldMetadataStore.get(targetClass);
|
|
200
|
+
if (!metadataMap) {
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
const allMetadata = metadataMap.get(metadataKey) || {};
|
|
204
|
+
for (const [fieldName, metadataList] of Object.entries(allMetadata)) {
|
|
205
|
+
if (metadataList.length > 0) {
|
|
206
|
+
result.set(fieldName, metadataList);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/core/decorators.ts
|
|
213
|
+
var HANDLER_METADATA_KEY = /* @__PURE__ */ Symbol.for("nebula:handlerMetadata");
|
|
214
|
+
var HANDLER_FIELD_METADATA_KEY = /* @__PURE__ */ Symbol.for("nebula:handlerFieldMetadata");
|
|
215
|
+
var createHandlerDecorator = createMethodDecorator(HANDLER_METADATA_KEY);
|
|
216
|
+
var createHandlerFieldDecorator = createFieldDecorator(HANDLER_FIELD_METADATA_KEY);
|
|
217
|
+
function Handler(config) {
|
|
218
|
+
return createHandlerDecorator({
|
|
219
|
+
type: config.type,
|
|
220
|
+
options: config.options || {}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function HandlerField(config) {
|
|
224
|
+
return createHandlerFieldDecorator({
|
|
225
|
+
type: config.type,
|
|
226
|
+
options: config.options || {}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function getAllHandlerMetadata(target) {
|
|
230
|
+
const allMetadata = getAllMethodMetadata(target, HANDLER_METADATA_KEY);
|
|
231
|
+
const result = /* @__PURE__ */ new Map();
|
|
232
|
+
for (const [methodName, metadataList] of allMetadata.entries()) {
|
|
233
|
+
result.set(methodName, metadataList);
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function getAllHandlerFieldMetadata(target) {
|
|
238
|
+
const allMetadata = getAllFieldMetadata(target, HANDLER_FIELD_METADATA_KEY);
|
|
239
|
+
const result = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const [fieldName, metadataList] of allMetadata.entries()) {
|
|
241
|
+
result.set(fieldName, metadataList);
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/core/errors.ts
|
|
247
|
+
var PluginNameRequiredError = class extends Error {
|
|
248
|
+
constructor(pluginName) {
|
|
249
|
+
super(
|
|
250
|
+
`Plugin name is required${pluginName ? ` (plugin: ${pluginName})` : ""}`
|
|
251
|
+
);
|
|
252
|
+
this.name = "PluginNameRequiredError";
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var ModuleConfigValidationError = class extends Error {
|
|
256
|
+
constructor(moduleName, pluginName, message) {
|
|
257
|
+
super(
|
|
258
|
+
`[ModuleConfigError] Module ${moduleName} (plugin ${pluginName}): ${message}`
|
|
259
|
+
);
|
|
260
|
+
this.name = "ModuleConfigValidationError";
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var DuplicateModuleError = class extends Error {
|
|
264
|
+
constructor(moduleName) {
|
|
265
|
+
super(`Module ${moduleName} is already registered`);
|
|
266
|
+
this.name = "DuplicateModuleError";
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var customFormat = format.printf((info) => {
|
|
270
|
+
const { level, timestamp, message, ...meta } = info;
|
|
271
|
+
const splat = info[/* @__PURE__ */ Symbol.for("splat")];
|
|
272
|
+
let msg = `${timestamp} [${level}] ${message}`;
|
|
273
|
+
if (splat && Array.isArray(splat) && splat.length > 0) {
|
|
274
|
+
const metaStr = splat.map((item) => {
|
|
275
|
+
if (item instanceof Error) {
|
|
276
|
+
return item.stack || item.message;
|
|
277
|
+
}
|
|
278
|
+
if (typeof item === "object") {
|
|
279
|
+
return JSON.stringify(item, null, 2);
|
|
280
|
+
}
|
|
281
|
+
return String(item);
|
|
282
|
+
}).join("\n");
|
|
283
|
+
msg += `
|
|
284
|
+
${metaStr}`;
|
|
285
|
+
}
|
|
286
|
+
const metaKeys = Object.keys(meta).filter(
|
|
287
|
+
(key) => key !== (/* @__PURE__ */ Symbol.for("splat")).toString()
|
|
288
|
+
);
|
|
289
|
+
if (metaKeys.length > 0) {
|
|
290
|
+
const metaObj = {};
|
|
291
|
+
for (const key of metaKeys) {
|
|
292
|
+
metaObj[key] = meta[key];
|
|
293
|
+
}
|
|
294
|
+
msg += `
|
|
295
|
+
${JSON.stringify(metaObj, null, 2)}`;
|
|
296
|
+
}
|
|
297
|
+
return msg;
|
|
298
|
+
});
|
|
299
|
+
var logger = winston.createLogger({
|
|
300
|
+
level: process.env.LOG_LEVEL || "info",
|
|
301
|
+
transports: [
|
|
302
|
+
new winston.transports.Console({
|
|
303
|
+
format: format.combine(
|
|
304
|
+
format.colorize({ all: true }),
|
|
305
|
+
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
|
|
306
|
+
customFormat
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
]
|
|
310
|
+
});
|
|
311
|
+
var logger_default = logger;
|
|
312
|
+
|
|
313
|
+
// src/core/checker.ts
|
|
314
|
+
async function startCheck(checkers, pass) {
|
|
315
|
+
logger_default.info("[ \u9884\u68C0\u5F00\u59CB ]");
|
|
316
|
+
for (const [index, checker] of checkers.entries()) {
|
|
317
|
+
const seq = index + 1;
|
|
318
|
+
logger_default.info(`${seq}. ${checker.name}`);
|
|
319
|
+
try {
|
|
320
|
+
if (checker.skip) {
|
|
321
|
+
logger_default.warn(`${seq}. ${checker.name} [\u8DF3\u8FC7]`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
await checker.check();
|
|
325
|
+
logger_default.info(`${seq}. ${checker.name} [\u6210\u529F]`);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
logger_default.error(`${seq}. ${checker.name} [\u5931\u8D25]`);
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
logger_default.info("[ \u9884\u68C0\u5B8C\u6210 ]");
|
|
332
|
+
if (pass) await pass();
|
|
333
|
+
}
|
|
334
|
+
var DEFAULT_FAVICON = /* @__PURE__ */ jsx(
|
|
335
|
+
"link",
|
|
336
|
+
{
|
|
337
|
+
rel: "icon",
|
|
338
|
+
href: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='100' height='100'><defs><linearGradient id='nodeGradient' x1='0%' y1='0%' x2='100%' y2='100%'><stop offset='0%' stop-color='%233498db'/><stop offset='100%' stop-color='%232980b9'/></linearGradient><linearGradient id='centerNodeGradient' x1='0%' y1='0%' x2='100%' y2='100%'><stop offset='0%' stop-color='%232ecc71'/><stop offset='100%' stop-color='%2327ae60'/></linearGradient></defs><circle cx='50' cy='50' r='45' fill='%23f5f7fa'/><path d='M30,30 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><path d='M70,30 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><path d='M30,70 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><path d='M70,70 L50,50' stroke='%23bdc3c7' stroke-width='2' stroke-linecap='round'/><polygon points='30,15 45,25 45,45 30,55 15,45 15,25' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='70,15 85,25 85,45 70,55 55,45 55,25' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='30,45 45,55 45,75 30,85 15,75 15,55' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='70,45 85,55 85,75 70,85 55,75 55,55' fill='url(%23nodeGradient)' stroke='%232980b9' stroke-width='1.5'/><polygon points='50,30 65,40 65,60 50,70 35,60 35,40' fill='url(%23centerNodeGradient)' stroke='%2327ae60' stroke-width='2'/><circle cx='30' cy='30' r='3' fill='%23ffffff'/><circle cx='70' cy='30' r='3' fill='%23ffffff'/><circle cx='30' cy='70' r='3' fill='%23ffffff'/><circle cx='70' cy='70' r='3' fill='%23ffffff'/><circle cx='50' cy='50' r='4' fill='%23ffffff'/></svg>",
|
|
339
|
+
type: "image/svg+xml"
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
var BaseLayout = (props = {
|
|
343
|
+
title: "Microservice Template"
|
|
344
|
+
}) => html`<!DOCTYPE html>
|
|
345
|
+
<html>
|
|
346
|
+
<head>
|
|
347
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
348
|
+
<title>${props.title}</title>
|
|
349
|
+
${props.heads}
|
|
350
|
+
</head>
|
|
351
|
+
<body>
|
|
352
|
+
${props.children}
|
|
353
|
+
</body>
|
|
354
|
+
</html>`;
|
|
355
|
+
var HtmxLayout = (props = {
|
|
356
|
+
title: "Microservice Template"
|
|
357
|
+
}) => BaseLayout({
|
|
358
|
+
title: props.title,
|
|
359
|
+
heads: html`
|
|
360
|
+
<script src="https://unpkg.com/htmx.org@latest"></script>
|
|
361
|
+
<script src="https://unpkg.com/hyperscript.org@latest"></script>
|
|
362
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
363
|
+
${props.favicon || DEFAULT_FAVICON}
|
|
364
|
+
`,
|
|
365
|
+
children: props.children
|
|
366
|
+
});
|
|
367
|
+
var InfoCard = ({
|
|
368
|
+
icon,
|
|
369
|
+
iconColor,
|
|
370
|
+
bgColor,
|
|
371
|
+
label,
|
|
372
|
+
value
|
|
373
|
+
}) => /* @__PURE__ */ jsxs("div", { className: `${bgColor} p-4 rounded-lg`, children: [
|
|
374
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center mb-2", children: [
|
|
375
|
+
/* @__PURE__ */ jsx(
|
|
376
|
+
"svg",
|
|
377
|
+
{
|
|
378
|
+
className: `w-5 h-5 ${iconColor} mr-2`,
|
|
379
|
+
fill: "none",
|
|
380
|
+
stroke: "currentColor",
|
|
381
|
+
viewBox: "0 0 24 24",
|
|
382
|
+
children: /* @__PURE__ */ jsx(
|
|
383
|
+
"path",
|
|
384
|
+
{
|
|
385
|
+
strokeLinecap: "round",
|
|
386
|
+
strokeLinejoin: "round",
|
|
387
|
+
strokeWidth: 2,
|
|
388
|
+
d: icon
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
),
|
|
393
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-600", children: label })
|
|
394
|
+
] }),
|
|
395
|
+
/* @__PURE__ */ jsx("p", { className: `text-xl font-semibold text-gray-900`, children: value })
|
|
396
|
+
] });
|
|
397
|
+
var getEnvironmentBadgeClass = (env) => {
|
|
398
|
+
const normalizedEnv = env.toLowerCase();
|
|
399
|
+
switch (normalizedEnv) {
|
|
400
|
+
case "production":
|
|
401
|
+
case "prod":
|
|
402
|
+
return "bg-red-100 text-red-800";
|
|
403
|
+
case "staging":
|
|
404
|
+
case "stg":
|
|
405
|
+
return "bg-yellow-100 text-yellow-800";
|
|
406
|
+
case "development":
|
|
407
|
+
case "dev":
|
|
408
|
+
default:
|
|
409
|
+
return "bg-blue-100 text-blue-800";
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
var getStatusBadgeClass = (status) => {
|
|
413
|
+
switch (status.toLowerCase()) {
|
|
414
|
+
case "running":
|
|
415
|
+
return "bg-green-100 text-green-800";
|
|
416
|
+
case "stopped":
|
|
417
|
+
return "bg-gray-100 text-gray-800";
|
|
418
|
+
case "error":
|
|
419
|
+
return "bg-red-100 text-red-800";
|
|
420
|
+
default:
|
|
421
|
+
return "bg-blue-100 text-blue-800";
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var ServiceInfoCards = ({
|
|
425
|
+
serviceInfo
|
|
426
|
+
}) => {
|
|
427
|
+
const env = serviceInfo.env || typeof process !== "undefined" && process.env?.NODE_ENV || "dev";
|
|
428
|
+
const status = serviceInfo.status || "running";
|
|
429
|
+
const infoCards = [
|
|
430
|
+
{
|
|
431
|
+
icon: "M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2m-9 0h10m-10 0a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V6a2 2 0 00-2-2",
|
|
432
|
+
iconColor: "text-blue-600",
|
|
433
|
+
bgColor: "bg-blue-50",
|
|
434
|
+
label: "\u670D\u52A1\u540D\u79F0",
|
|
435
|
+
value: serviceInfo.name
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
icon: "M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1",
|
|
439
|
+
iconColor: "text-orange-600",
|
|
440
|
+
bgColor: "bg-orange-50",
|
|
441
|
+
label: "\u670D\u52A1\u8DEF\u5F84",
|
|
442
|
+
value: serviceInfo.prefix || "/"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
446
|
+
iconColor: "text-green-600",
|
|
447
|
+
bgColor: "bg-green-50",
|
|
448
|
+
label: "\u8FD0\u884C\u73AF\u5883",
|
|
449
|
+
value: /* @__PURE__ */ jsx(
|
|
450
|
+
"span",
|
|
451
|
+
{
|
|
452
|
+
className: `px-2 py-1 rounded-full text-sm ${getEnvironmentBadgeClass(env)}`,
|
|
453
|
+
children: env
|
|
454
|
+
}
|
|
455
|
+
)
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z",
|
|
459
|
+
iconColor: "text-purple-600",
|
|
460
|
+
bgColor: "bg-purple-50",
|
|
461
|
+
label: "\u7248\u672C\u53F7",
|
|
462
|
+
value: serviceInfo.version || "unknown"
|
|
463
|
+
},
|
|
464
|
+
...serviceInfo.port ? [
|
|
465
|
+
{
|
|
466
|
+
icon: "M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01",
|
|
467
|
+
iconColor: "text-indigo-600",
|
|
468
|
+
bgColor: "bg-indigo-50",
|
|
469
|
+
label: "\u7AEF\u53E3",
|
|
470
|
+
value: serviceInfo.port
|
|
471
|
+
}
|
|
472
|
+
] : [],
|
|
473
|
+
{
|
|
474
|
+
icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
475
|
+
iconColor: "text-emerald-600",
|
|
476
|
+
bgColor: "bg-emerald-50",
|
|
477
|
+
label: "\u8FD0\u884C\u72B6\u6001",
|
|
478
|
+
value: /* @__PURE__ */ jsx(
|
|
479
|
+
"span",
|
|
480
|
+
{
|
|
481
|
+
className: `px-2 py-1 rounded-full text-sm ${getStatusBadgeClass(status)}`,
|
|
482
|
+
children: status
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
];
|
|
487
|
+
return /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-md p-6 mb-8", children: [
|
|
488
|
+
/* @__PURE__ */ jsxs("h2", { className: "text-2xl font-semibold text-gray-800 mb-6 flex items-center", children: [
|
|
489
|
+
/* @__PURE__ */ jsx(
|
|
490
|
+
"svg",
|
|
491
|
+
{
|
|
492
|
+
className: "w-6 h-6 mr-2 text-blue-600",
|
|
493
|
+
fill: "none",
|
|
494
|
+
stroke: "currentColor",
|
|
495
|
+
viewBox: "0 0 24 24",
|
|
496
|
+
children: /* @__PURE__ */ jsx(
|
|
497
|
+
"path",
|
|
498
|
+
{
|
|
499
|
+
strokeLinecap: "round",
|
|
500
|
+
strokeLinejoin: "round",
|
|
501
|
+
strokeWidth: 2,
|
|
502
|
+
d: "M13 10V3L4 14h7v7l9-11h-7z"
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
),
|
|
507
|
+
"\u670D\u52A1\u57FA\u672C\u4FE1\u606F"
|
|
508
|
+
] }),
|
|
509
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6", children: infoCards.map((card, index) => /* @__PURE__ */ jsx(InfoCard, { ...card }, index)) })
|
|
510
|
+
] });
|
|
511
|
+
};
|
|
512
|
+
var ServiceStatusPage = ({
|
|
513
|
+
serviceInfo
|
|
514
|
+
}) => {
|
|
515
|
+
return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-gray-50 py-8", children: /* @__PURE__ */ jsxs("div", { className: "max-w-6xl mx-auto px-4", children: [
|
|
516
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-8", children: [
|
|
517
|
+
/* @__PURE__ */ jsx("h1", { className: "text-4xl font-bold text-gray-900 mb-2", children: "Service Status" }),
|
|
518
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-600", children: "\u67E5\u770B\u670D\u52A1\u8FD0\u884C\u72B6\u6001\u548C\u57FA\u672C\u4FE1\u606F" })
|
|
519
|
+
] }),
|
|
520
|
+
/* @__PURE__ */ jsx(ServiceInfoCards, { serviceInfo })
|
|
521
|
+
] }) });
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/plugins/route/decorator.ts
|
|
525
|
+
function Route(options) {
|
|
526
|
+
return Handler({
|
|
527
|
+
type: "route",
|
|
528
|
+
options
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function Page(options) {
|
|
532
|
+
const pageOptions = {
|
|
533
|
+
method: options.method || "GET",
|
|
534
|
+
...options
|
|
535
|
+
};
|
|
536
|
+
return Route(pageOptions);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/plugins/route/plugin.ts
|
|
540
|
+
var RoutePlugin = class {
|
|
541
|
+
name = "route-plugin";
|
|
542
|
+
priority = 1e3 /* ROUTE */;
|
|
543
|
+
engine;
|
|
544
|
+
globalPrefix;
|
|
545
|
+
globalMiddlewares;
|
|
546
|
+
errorTransformer;
|
|
547
|
+
/**
|
|
548
|
+
* 构造函数
|
|
549
|
+
* @param options 插件配置选项
|
|
550
|
+
*/
|
|
551
|
+
constructor(options) {
|
|
552
|
+
this.globalPrefix = options?.prefix || "";
|
|
553
|
+
this.globalMiddlewares = options?.globalMiddlewares || [];
|
|
554
|
+
this.errorTransformer = options?.errorTransformer;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* 声明Module配置Schema(用于类型推导+运行时校验)
|
|
558
|
+
*/
|
|
559
|
+
getModuleOptionsSchema() {
|
|
560
|
+
return {
|
|
561
|
+
_type: {},
|
|
562
|
+
validate: (options) => {
|
|
563
|
+
if (options.routePrefix !== void 0 && typeof options.routePrefix !== "string") {
|
|
564
|
+
return "routePrefix must be a string";
|
|
565
|
+
}
|
|
566
|
+
if (options.routePrefix && !options.routePrefix.startsWith("/")) {
|
|
567
|
+
return `routePrefix must start with '/'`;
|
|
568
|
+
}
|
|
569
|
+
if (options.routeMiddlewares !== void 0 && !Array.isArray(options.routeMiddlewares)) {
|
|
570
|
+
return "routeMiddlewares must be an array";
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 引擎初始化钩子:获取Hono实例
|
|
578
|
+
*/
|
|
579
|
+
onInit(engine) {
|
|
580
|
+
this.engine = engine;
|
|
581
|
+
logger_default.info("RoutePlugin initialized");
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Handler加载钩子:解析type="route"的Handler元数据,注册HTTP路由
|
|
585
|
+
*/
|
|
586
|
+
onHandlerLoad(handlers) {
|
|
587
|
+
const routeHandlers = handlers.filter(
|
|
588
|
+
(handler) => handler.type === "route"
|
|
589
|
+
);
|
|
590
|
+
logger_default.info(`Found ${routeHandlers.length} route handler(s)`);
|
|
591
|
+
for (const handler of routeHandlers) {
|
|
592
|
+
const routeOptions = handler.options;
|
|
593
|
+
const methodName = handler.methodName;
|
|
594
|
+
const moduleClass = handler.module;
|
|
595
|
+
const moduleInstance = this.engine.get(moduleClass);
|
|
596
|
+
if (!moduleInstance) {
|
|
597
|
+
logger_default.warn(
|
|
598
|
+
`Module instance not found for ${moduleClass.name}, skipping route registration`
|
|
599
|
+
);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const moduleMetadata = this.engine.getModules().find((m) => m.clazz === moduleClass);
|
|
603
|
+
const moduleOptions = moduleMetadata?.options || {};
|
|
604
|
+
const routePrefix = moduleOptions.routePrefix || "";
|
|
605
|
+
const paths = Array.isArray(routeOptions.path) ? routeOptions.path : [routeOptions.path];
|
|
606
|
+
const fullPaths = paths.map((p) => this.globalPrefix + routePrefix + p);
|
|
607
|
+
const routeHandler = async (ctx) => {
|
|
608
|
+
const method = moduleInstance[methodName];
|
|
609
|
+
if (typeof method !== "function") {
|
|
610
|
+
return ctx.json({ error: "Handler method not found" }, 500);
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const result = await method.call(moduleInstance, ctx);
|
|
614
|
+
if (result === void 0) {
|
|
615
|
+
return new Response(null, { status: 204 });
|
|
616
|
+
}
|
|
617
|
+
if (result instanceof Response) {
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
if (typeof result === "string") {
|
|
621
|
+
return ctx.text(result);
|
|
622
|
+
}
|
|
623
|
+
if (result === null) {
|
|
624
|
+
return new Response(null, { status: 204 });
|
|
625
|
+
}
|
|
626
|
+
if (typeof result === "object" && result !== null && (result.isEscaped === true || result.constructor?.name === "HtmlEscaped")) {
|
|
627
|
+
return ctx.html(result);
|
|
628
|
+
}
|
|
629
|
+
if (typeof result === "object" && result !== null && "type" in result && (typeof result.type === "function" || typeof result.type === "string")) {
|
|
630
|
+
return ctx.html(result);
|
|
631
|
+
}
|
|
632
|
+
return ctx.json(result);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
logger_default.error(
|
|
635
|
+
`Error in route handler ${moduleClass.name}.${methodName}`,
|
|
636
|
+
error
|
|
637
|
+
);
|
|
638
|
+
if (this.errorTransformer) {
|
|
639
|
+
try {
|
|
640
|
+
const transformedResponse = await this.errorTransformer(
|
|
641
|
+
ctx,
|
|
642
|
+
error,
|
|
643
|
+
handler
|
|
644
|
+
);
|
|
645
|
+
return transformedResponse;
|
|
646
|
+
} catch (transformerError) {
|
|
647
|
+
logger_default.error(
|
|
648
|
+
`Error transformer failed for ${moduleClass.name}.${methodName}`,
|
|
649
|
+
transformerError
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return ctx.json(
|
|
654
|
+
{
|
|
655
|
+
error: "Internal server error",
|
|
656
|
+
message: error instanceof Error ? error.message : String(error)
|
|
657
|
+
},
|
|
658
|
+
500
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const hono = this.engine.getHono();
|
|
663
|
+
const methods = routeOptions.method ? Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method] : ["GET"];
|
|
664
|
+
for (const fullPath of fullPaths) {
|
|
665
|
+
let route = hono;
|
|
666
|
+
if (this.globalMiddlewares.length > 0) {
|
|
667
|
+
route = route.use(fullPath, ...this.globalMiddlewares);
|
|
668
|
+
}
|
|
669
|
+
if (moduleOptions.routeMiddlewares && moduleOptions.routeMiddlewares.length > 0) {
|
|
670
|
+
route = route.use(fullPath, ...moduleOptions.routeMiddlewares);
|
|
671
|
+
}
|
|
672
|
+
if (routeOptions.middlewares && routeOptions.middlewares.length > 0) {
|
|
673
|
+
route = route.use(fullPath, ...routeOptions.middlewares);
|
|
674
|
+
}
|
|
675
|
+
for (const method of methods) {
|
|
676
|
+
const methodLower = method.toLowerCase();
|
|
677
|
+
if (methodLower === "get" || methodLower === "post" || methodLower === "put" || methodLower === "delete" || methodLower === "patch" || methodLower === "head" || methodLower === "options") {
|
|
678
|
+
route[methodLower](fullPath, routeHandler);
|
|
679
|
+
const description = routeOptions.description ? ` (${routeOptions.description})` : "";
|
|
680
|
+
logger_default.info(
|
|
681
|
+
`Registered route: ${method.toUpperCase()} ${fullPath} -> ${moduleClass.name}.${methodName}${description}`
|
|
682
|
+
);
|
|
683
|
+
} else {
|
|
684
|
+
logger_default.warn(
|
|
685
|
+
`Unsupported HTTP method: ${method}, skipping route ${fullPath}`
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function buildParamsSchema(schemas) {
|
|
694
|
+
const shape = {};
|
|
695
|
+
for (let i = 0; i < schemas.length; i++) {
|
|
696
|
+
shape[String(i)] = schemas[i];
|
|
697
|
+
}
|
|
698
|
+
return z.object(shape);
|
|
699
|
+
}
|
|
700
|
+
function parseAndValidateParams(body, schemas) {
|
|
701
|
+
if (!body || typeof body !== "object") {
|
|
702
|
+
body = {};
|
|
703
|
+
}
|
|
704
|
+
if (schemas.length === 0) {
|
|
705
|
+
return { success: true, data: [] };
|
|
706
|
+
}
|
|
707
|
+
const paramsSchema = buildParamsSchema(schemas);
|
|
708
|
+
const validation = paramsSchema.safeParse(body);
|
|
709
|
+
if (!validation.success) {
|
|
710
|
+
return {
|
|
711
|
+
success: false,
|
|
712
|
+
error: validation.error
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const validatedData = [];
|
|
716
|
+
for (let i = 0; i < schemas.length; i++) {
|
|
717
|
+
validatedData[i] = validation.data[String(i)];
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
success: true,
|
|
721
|
+
data: validatedData
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function buildActionPath(enginePrefix, moduleName, handlerName) {
|
|
725
|
+
const normalizedPrefix = enginePrefix.replace(/\/+$/, "");
|
|
726
|
+
return `/${normalizedPrefix}/${moduleName}/${handlerName}`.replace(/\/+/g, "/").replace(/^\/\//, "/");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/plugins/action/plugin.ts
|
|
730
|
+
function isAsyncIterable(obj) {
|
|
731
|
+
return obj != null && typeof obj[Symbol.asyncIterator] === "function";
|
|
732
|
+
}
|
|
733
|
+
var ActionPlugin = class {
|
|
734
|
+
name = "action-plugin";
|
|
735
|
+
priority = 1e3 /* ROUTE */;
|
|
736
|
+
// 路由插件优先级最低,必须最后执行
|
|
737
|
+
engine;
|
|
738
|
+
/**
|
|
739
|
+
* 声明Module配置Schema(用于类型推导+运行时校验)
|
|
740
|
+
*/
|
|
741
|
+
getModuleOptionsSchema() {
|
|
742
|
+
return {
|
|
743
|
+
_type: {},
|
|
744
|
+
validate: (options) => {
|
|
745
|
+
if (options.actionMiddlewares !== void 0 && !Array.isArray(options.actionMiddlewares)) {
|
|
746
|
+
return "actionMiddlewares must be an array";
|
|
747
|
+
}
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* 引擎初始化钩子:获取Hono实例
|
|
754
|
+
*/
|
|
755
|
+
onInit(engine) {
|
|
756
|
+
this.engine = engine;
|
|
757
|
+
logger_default.info("ActionPlugin initialized");
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Handler加载钩子:解析type="action"的Handler元数据,注册HTTP路由
|
|
761
|
+
*/
|
|
762
|
+
onHandlerLoad(handlers) {
|
|
763
|
+
const actionHandlers = handlers.filter(
|
|
764
|
+
(handler) => handler.type === "action"
|
|
765
|
+
);
|
|
766
|
+
logger_default.info(`Found ${actionHandlers.length} action handler(s)`);
|
|
767
|
+
for (const handler of actionHandlers) {
|
|
768
|
+
const actionOptions = handler.options;
|
|
769
|
+
const methodName = handler.methodName;
|
|
770
|
+
const moduleClass = handler.module;
|
|
771
|
+
const moduleInstance = this.engine.get(moduleClass);
|
|
772
|
+
if (!moduleInstance) {
|
|
773
|
+
logger_default.warn(
|
|
774
|
+
`Module instance not found for ${moduleClass.name}, skipping action registration`
|
|
775
|
+
);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const moduleMetadata = this.engine.getModules().find((m) => m.clazz === moduleClass);
|
|
779
|
+
if (!moduleMetadata) {
|
|
780
|
+
logger_default.warn(
|
|
781
|
+
`Module metadata not found for ${moduleClass.name}, skipping action registration`
|
|
782
|
+
);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const moduleOptions = moduleMetadata.options || {};
|
|
786
|
+
const enginePrefix = this.engine.options.prefix || "";
|
|
787
|
+
const actionPath = buildActionPath(
|
|
788
|
+
enginePrefix,
|
|
789
|
+
moduleMetadata.name,
|
|
790
|
+
methodName
|
|
791
|
+
);
|
|
792
|
+
const paramSchemas = actionOptions.params || [];
|
|
793
|
+
const returnSchema = actionOptions.returns;
|
|
794
|
+
const actionHandler = async (ctx) => {
|
|
795
|
+
const method = moduleInstance[methodName];
|
|
796
|
+
if (typeof method !== "function") {
|
|
797
|
+
const errorResponse = ejson.stringify({
|
|
798
|
+
success: false,
|
|
799
|
+
error: "Action method not found"
|
|
800
|
+
});
|
|
801
|
+
return ctx.text(errorResponse, 500, {
|
|
802
|
+
"Content-Type": "application/json"
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
let body = {};
|
|
807
|
+
if (ctx.req.method === "GET") {
|
|
808
|
+
const url = new URL(ctx.req.url);
|
|
809
|
+
url.searchParams.forEach((value, key) => {
|
|
810
|
+
body[key] = value;
|
|
811
|
+
});
|
|
812
|
+
} else {
|
|
813
|
+
try {
|
|
814
|
+
const rawBody = await ctx.req.text();
|
|
815
|
+
if (rawBody) {
|
|
816
|
+
body = ejson.parse(rawBody);
|
|
817
|
+
}
|
|
818
|
+
} catch (parseError) {
|
|
819
|
+
try {
|
|
820
|
+
body = await ctx.req.json().catch(() => ({}));
|
|
821
|
+
} catch {
|
|
822
|
+
body = {};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const validation = parseAndValidateParams(body, paramSchemas);
|
|
827
|
+
if (!validation.success) {
|
|
828
|
+
const errors = validation.error.issues || [];
|
|
829
|
+
const errorMessage = `Validation failed: ${errors.map((e) => {
|
|
830
|
+
const path = e.path || [];
|
|
831
|
+
const pathStr = path.length > 0 ? path.map((p, i) => {
|
|
832
|
+
if (i === 0 && typeof p === "string" && /^\d+$/.test(p)) {
|
|
833
|
+
return `\u53C2\u6570[${p}]`;
|
|
834
|
+
}
|
|
835
|
+
return String(p);
|
|
836
|
+
}).join(".") : "unknown";
|
|
837
|
+
return `${pathStr}: ${e.message}`;
|
|
838
|
+
}).join(", ")}`;
|
|
839
|
+
const errorResponse = ejson.stringify({
|
|
840
|
+
success: false,
|
|
841
|
+
error: errorMessage
|
|
842
|
+
});
|
|
843
|
+
return ctx.text(errorResponse, 400, {
|
|
844
|
+
"Content-Type": "application/json"
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
const methodLength = method.length;
|
|
848
|
+
const paramsLength = paramSchemas.length;
|
|
849
|
+
const args = [...validation.data];
|
|
850
|
+
if (methodLength > paramsLength) {
|
|
851
|
+
args.unshift(ctx);
|
|
852
|
+
}
|
|
853
|
+
const result = await method.apply(moduleInstance, args);
|
|
854
|
+
if (actionOptions.stream) {
|
|
855
|
+
if (!isAsyncIterable(result)) {
|
|
856
|
+
const errorResponse = ejson.stringify({
|
|
857
|
+
success: false,
|
|
858
|
+
error: "Stream action must return AsyncIterable"
|
|
859
|
+
});
|
|
860
|
+
return ctx.text(errorResponse, 500, {
|
|
861
|
+
"Content-Type": "application/json"
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
const encoder = new TextEncoder();
|
|
865
|
+
const iterator = result[Symbol.asyncIterator]();
|
|
866
|
+
const stream = new ReadableStream({
|
|
867
|
+
async start(controller) {
|
|
868
|
+
try {
|
|
869
|
+
while (true) {
|
|
870
|
+
const { value, done } = await iterator.next();
|
|
871
|
+
if (done) {
|
|
872
|
+
controller.enqueue(
|
|
873
|
+
encoder.encode(ejson.stringify({ done: true }))
|
|
874
|
+
);
|
|
875
|
+
controller.close();
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
controller.enqueue(
|
|
879
|
+
encoder.encode(ejson.stringify({ value, done: false }))
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
} catch (error) {
|
|
883
|
+
controller.enqueue(
|
|
884
|
+
encoder.encode(
|
|
885
|
+
ejson.stringify({
|
|
886
|
+
error: error instanceof Error ? error.message : String(error),
|
|
887
|
+
done: true
|
|
888
|
+
})
|
|
889
|
+
)
|
|
890
|
+
);
|
|
891
|
+
controller.close();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
return new Response(stream, {
|
|
896
|
+
headers: {
|
|
897
|
+
"Content-Type": "text/event-stream",
|
|
898
|
+
"Cache-Control": "no-cache",
|
|
899
|
+
Connection: "keep-alive"
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
if (returnSchema) {
|
|
904
|
+
const returnValidation = returnSchema.safeParse(result);
|
|
905
|
+
if (!returnValidation.success) {
|
|
906
|
+
logger_default.error(
|
|
907
|
+
`Return value validation failed for ${moduleClass.name}.${methodName}`,
|
|
908
|
+
returnValidation.error
|
|
909
|
+
);
|
|
910
|
+
const returnErrors = returnValidation.error.issues || [];
|
|
911
|
+
const errorMessage = `Return value validation failed: ${returnErrors.map((e) => {
|
|
912
|
+
const path = e.path || [];
|
|
913
|
+
const pathStr = path.length > 0 ? path.map((p) => String(p)).join(".") : "root";
|
|
914
|
+
return `${pathStr}: ${e.message}`;
|
|
915
|
+
}).join(", ")}`;
|
|
916
|
+
const errorResponse = ejson.stringify({
|
|
917
|
+
success: false,
|
|
918
|
+
error: errorMessage
|
|
919
|
+
});
|
|
920
|
+
return ctx.text(errorResponse, 400, {
|
|
921
|
+
"Content-Type": "application/json"
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
const successResponse2 = ejson.stringify({
|
|
925
|
+
success: true,
|
|
926
|
+
data: returnValidation.data
|
|
927
|
+
});
|
|
928
|
+
return ctx.text(successResponse2, 200, {
|
|
929
|
+
"Content-Type": "application/json"
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
const successResponse = ejson.stringify({
|
|
933
|
+
success: true,
|
|
934
|
+
data: result
|
|
935
|
+
});
|
|
936
|
+
return ctx.text(successResponse, 200, {
|
|
937
|
+
"Content-Type": "application/json"
|
|
938
|
+
});
|
|
939
|
+
} catch (error) {
|
|
940
|
+
logger_default.error(
|
|
941
|
+
`Error in action handler ${moduleClass.name}.${methodName}`,
|
|
942
|
+
error
|
|
943
|
+
);
|
|
944
|
+
const errorResponse = ejson.stringify({
|
|
945
|
+
success: false,
|
|
946
|
+
error: error instanceof Error ? error.message : String(error)
|
|
947
|
+
});
|
|
948
|
+
return ctx.text(errorResponse, 500, {
|
|
949
|
+
"Content-Type": "application/json"
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
const hono = this.engine.getHono();
|
|
954
|
+
let route = hono;
|
|
955
|
+
if (moduleOptions.actionMiddlewares && moduleOptions.actionMiddlewares.length > 0) {
|
|
956
|
+
route = route.use(actionPath, ...moduleOptions.actionMiddlewares);
|
|
957
|
+
}
|
|
958
|
+
if (actionOptions.middlewares && actionOptions.middlewares.length > 0) {
|
|
959
|
+
route = route.use(actionPath, ...actionOptions.middlewares);
|
|
960
|
+
}
|
|
961
|
+
route.get(actionPath, actionHandler);
|
|
962
|
+
route.post(actionPath, actionHandler);
|
|
963
|
+
logger_default.info(
|
|
964
|
+
`[\u6CE8\u518C\u52A8\u4F5C] ${moduleClass.name}.${methodName} ${actionOptions.description}`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// src/plugins/action/decorator.ts
|
|
971
|
+
function Action(options) {
|
|
972
|
+
return Handler({
|
|
973
|
+
type: "action",
|
|
974
|
+
options
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/plugins/cache/adapter.ts
|
|
979
|
+
var MemoryCacheAdapter = class {
|
|
980
|
+
cache = /* @__PURE__ */ new Map();
|
|
981
|
+
async get(key) {
|
|
982
|
+
const item = this.cache.get(key);
|
|
983
|
+
if (!item) {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
if (item.expiresAt <= Date.now()) {
|
|
987
|
+
this.cache.delete(key);
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
return item;
|
|
991
|
+
}
|
|
992
|
+
async set(key, item) {
|
|
993
|
+
this.cache.set(key, item);
|
|
994
|
+
}
|
|
995
|
+
async delete(key) {
|
|
996
|
+
return this.cache.delete(key);
|
|
997
|
+
}
|
|
998
|
+
async clear() {
|
|
999
|
+
this.cache.clear();
|
|
1000
|
+
}
|
|
1001
|
+
async keys() {
|
|
1002
|
+
const now = Date.now();
|
|
1003
|
+
return Array.from(this.cache.entries()).filter(([_, item]) => item.expiresAt > now).map(([key]) => key);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 清理所有过期项(高效批量清理)
|
|
1007
|
+
* 这个方法比通过 keys() + get() + delete() 更高效
|
|
1008
|
+
*/
|
|
1009
|
+
async cleanupExpired() {
|
|
1010
|
+
const now = Date.now();
|
|
1011
|
+
let cleaned = 0;
|
|
1012
|
+
for (const [key, item] of this.cache.entries()) {
|
|
1013
|
+
if (item.expiresAt <= now) {
|
|
1014
|
+
this.cache.delete(key);
|
|
1015
|
+
cleaned++;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return cleaned;
|
|
1019
|
+
}
|
|
1020
|
+
async getStats() {
|
|
1021
|
+
const now = Date.now();
|
|
1022
|
+
const validEntries = Array.from(this.cache.entries()).filter(([_, item]) => item.expiresAt > now).map(([key, item]) => ({
|
|
1023
|
+
key,
|
|
1024
|
+
expiresAt: item.expiresAt,
|
|
1025
|
+
createdAt: item.createdAt
|
|
1026
|
+
}));
|
|
1027
|
+
return {
|
|
1028
|
+
size: validEntries.length,
|
|
1029
|
+
entries: validEntries
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
var RedisCacheAdapter = class {
|
|
1034
|
+
client;
|
|
1035
|
+
keyPrefix;
|
|
1036
|
+
constructor(options) {
|
|
1037
|
+
this.client = options.client;
|
|
1038
|
+
this.keyPrefix = options.keyPrefix || "cache:";
|
|
1039
|
+
}
|
|
1040
|
+
getKey(key) {
|
|
1041
|
+
return `${this.keyPrefix}${key}`;
|
|
1042
|
+
}
|
|
1043
|
+
async get(key) {
|
|
1044
|
+
const redisKey = this.getKey(key);
|
|
1045
|
+
const data = await this.client.get(redisKey);
|
|
1046
|
+
if (!data) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const item = JSON.parse(data);
|
|
1051
|
+
if (item.expiresAt <= Date.now()) {
|
|
1052
|
+
await this.delete(key);
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
return item;
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
await this.delete(key);
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async set(key, item) {
|
|
1062
|
+
const redisKey = this.getKey(key);
|
|
1063
|
+
const data = JSON.stringify(item);
|
|
1064
|
+
const ttl = Math.max(0, Math.floor((item.expiresAt - Date.now()) / 1e3));
|
|
1065
|
+
await this.client.set(redisKey, data, "EX", ttl);
|
|
1066
|
+
}
|
|
1067
|
+
async delete(key) {
|
|
1068
|
+
const redisKey = this.getKey(key);
|
|
1069
|
+
const result = await this.client.del(redisKey);
|
|
1070
|
+
return result > 0;
|
|
1071
|
+
}
|
|
1072
|
+
async clear() {
|
|
1073
|
+
const pattern = `${this.keyPrefix}*`;
|
|
1074
|
+
const keys = await this.client.keys(pattern);
|
|
1075
|
+
if (keys.length > 0) {
|
|
1076
|
+
await Promise.all(keys.map((key) => this.client.del(key)));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async keys() {
|
|
1080
|
+
const pattern = `${this.keyPrefix}*`;
|
|
1081
|
+
const redisKeys = await this.client.keys(pattern);
|
|
1082
|
+
return redisKeys.map((key) => key.replace(this.keyPrefix, ""));
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* 清理所有过期项
|
|
1086
|
+
*
|
|
1087
|
+
* 注意:Redis 本身支持自动过期(通过 SET key value EX seconds),
|
|
1088
|
+
* 过期键会被 Redis 自动删除。但在某些情况下(如测试环境、某些 Redis 客户端),
|
|
1089
|
+
* 可能需要手动清理。此方法会检查并清理:
|
|
1090
|
+
* 1. 已过期但尚未被 Redis 删除的键(双重保险)
|
|
1091
|
+
* 2. JSON 解析失败的数据
|
|
1092
|
+
*/
|
|
1093
|
+
async cleanupExpired() {
|
|
1094
|
+
const pattern = `${this.keyPrefix}*`;
|
|
1095
|
+
const redisKeys = await this.client.keys(pattern);
|
|
1096
|
+
let cleaned = 0;
|
|
1097
|
+
for (const redisKey of redisKeys) {
|
|
1098
|
+
const data = await this.client.get(redisKey);
|
|
1099
|
+
if (!data) {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
const item = JSON.parse(data);
|
|
1104
|
+
if (item.expiresAt <= Date.now()) {
|
|
1105
|
+
await this.client.del(redisKey);
|
|
1106
|
+
cleaned++;
|
|
1107
|
+
}
|
|
1108
|
+
} catch {
|
|
1109
|
+
await this.client.del(redisKey);
|
|
1110
|
+
cleaned++;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return cleaned;
|
|
1114
|
+
}
|
|
1115
|
+
async getStats() {
|
|
1116
|
+
const allKeys = await this.keys();
|
|
1117
|
+
const entries = [];
|
|
1118
|
+
for (const key of allKeys) {
|
|
1119
|
+
const item = await this.get(key);
|
|
1120
|
+
if (item) {
|
|
1121
|
+
entries.push({
|
|
1122
|
+
key,
|
|
1123
|
+
expiresAt: item.expiresAt,
|
|
1124
|
+
createdAt: item.createdAt
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
size: entries.length,
|
|
1130
|
+
entries
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
// src/plugins/cache/plugin.ts
|
|
1136
|
+
var CachePlugin = class {
|
|
1137
|
+
name = "cache-plugin";
|
|
1138
|
+
priority = 400 /* PERFORMANCE */;
|
|
1139
|
+
// 性能优化插件,优先级较低
|
|
1140
|
+
// 缓存适配器
|
|
1141
|
+
adapter;
|
|
1142
|
+
// 清理定时器
|
|
1143
|
+
cleanupTimer = null;
|
|
1144
|
+
// 引擎引用
|
|
1145
|
+
engine = null;
|
|
1146
|
+
// 模块配置
|
|
1147
|
+
defaultTtl = 6e4;
|
|
1148
|
+
// 默认1分钟
|
|
1149
|
+
cacheEnabled = true;
|
|
1150
|
+
cleanupInterval = 6e4;
|
|
1151
|
+
// 默认1分钟清理一次
|
|
1152
|
+
/**
|
|
1153
|
+
* 构造函数
|
|
1154
|
+
* @param adapter 缓存适配器,如果不提供则使用默认的内存缓存适配器
|
|
1155
|
+
*/
|
|
1156
|
+
constructor(adapter) {
|
|
1157
|
+
this.adapter = adapter || new MemoryCacheAdapter();
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* 声明Module配置Schema
|
|
1161
|
+
*/
|
|
1162
|
+
getModuleOptionsSchema() {
|
|
1163
|
+
return {
|
|
1164
|
+
_type: {},
|
|
1165
|
+
validate: (options) => {
|
|
1166
|
+
if (options.cacheDefaultTtl !== void 0 && options.cacheDefaultTtl < 0) {
|
|
1167
|
+
return `cacheDefaultTtl must be >= 0`;
|
|
1168
|
+
}
|
|
1169
|
+
if (options.cacheCleanupInterval !== void 0 && options.cacheCleanupInterval < 0) {
|
|
1170
|
+
return `cacheCleanupInterval must be >= 0`;
|
|
1171
|
+
}
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* 引擎初始化前钩子
|
|
1178
|
+
*/
|
|
1179
|
+
onInit(engine) {
|
|
1180
|
+
this.engine = engine;
|
|
1181
|
+
logger_default.info("CachePlugin initialized cache storage");
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Handler加载后钩子
|
|
1185
|
+
* 拦截带有 type="cache" 的 Handler,包装原始方法实现缓存逻辑
|
|
1186
|
+
*/
|
|
1187
|
+
onHandlerLoad(handlers) {
|
|
1188
|
+
const cacheHandlers = handlers.filter(
|
|
1189
|
+
(handler) => handler.type === "cache"
|
|
1190
|
+
);
|
|
1191
|
+
logger_default.info(`Found ${cacheHandlers.length} cache handler(s)`);
|
|
1192
|
+
for (const handler of cacheHandlers) {
|
|
1193
|
+
const methodName = handler.methodName;
|
|
1194
|
+
const moduleClass = handler.module;
|
|
1195
|
+
const cacheOptions = handler.options || {};
|
|
1196
|
+
const moduleMetadata = this.engine?.getModules().find((m) => m.clazz === moduleClass);
|
|
1197
|
+
if (!moduleMetadata) {
|
|
1198
|
+
logger_default.warn(`Module metadata not found for ${moduleClass.name}`);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const moduleOptions = moduleMetadata.options;
|
|
1202
|
+
const moduleDefaultTtl = moduleOptions?.cacheDefaultTtl ?? this.defaultTtl;
|
|
1203
|
+
const moduleCacheEnabled = moduleOptions?.cacheEnabled ?? this.cacheEnabled;
|
|
1204
|
+
const ttl = cacheOptions.ttl ?? moduleDefaultTtl;
|
|
1205
|
+
const enabled = cacheOptions.enabled ?? moduleCacheEnabled;
|
|
1206
|
+
if (!enabled) {
|
|
1207
|
+
logger_default.info(`Cache disabled for ${moduleClass.name}.${methodName}`);
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
const moduleName = moduleMetadata.name;
|
|
1211
|
+
handler.wrap(async (next, instance, ...args) => {
|
|
1212
|
+
const cacheKey = this.generateCacheKey(
|
|
1213
|
+
moduleName,
|
|
1214
|
+
methodName,
|
|
1215
|
+
cacheOptions.key,
|
|
1216
|
+
args
|
|
1217
|
+
);
|
|
1218
|
+
const cached = await this.adapter.get(cacheKey);
|
|
1219
|
+
if (cached) {
|
|
1220
|
+
logger_default.debug(`Cache hit for ${cacheKey}`);
|
|
1221
|
+
return cached.value;
|
|
1222
|
+
}
|
|
1223
|
+
logger_default.debug(`Cache miss for ${cacheKey}`);
|
|
1224
|
+
const result = await next();
|
|
1225
|
+
await this.adapter.set(cacheKey, {
|
|
1226
|
+
value: result,
|
|
1227
|
+
expiresAt: Date.now() + ttl,
|
|
1228
|
+
createdAt: Date.now()
|
|
1229
|
+
});
|
|
1230
|
+
return result;
|
|
1231
|
+
});
|
|
1232
|
+
logger_default.info(
|
|
1233
|
+
`Wrapped ${moduleClass.name}.${methodName} with cache (TTL: ${ttl}ms)`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* 引擎启动前钩子
|
|
1239
|
+
*/
|
|
1240
|
+
onBeforeStart(engine) {
|
|
1241
|
+
const moduleOptions = engine.options;
|
|
1242
|
+
const cleanupInterval = moduleOptions?.cacheCleanupInterval ?? this.cleanupInterval;
|
|
1243
|
+
if (cleanupInterval > 0) {
|
|
1244
|
+
this.cleanupTimer = setInterval(() => {
|
|
1245
|
+
this.cleanup().catch((error) => {
|
|
1246
|
+
logger_default.error("Cache cleanup failed", error);
|
|
1247
|
+
});
|
|
1248
|
+
}, cleanupInterval);
|
|
1249
|
+
logger_default.info(
|
|
1250
|
+
`Started cache cleanup timer (interval: ${cleanupInterval}ms)`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* 引擎停止时钩子
|
|
1256
|
+
*/
|
|
1257
|
+
async onDestroy() {
|
|
1258
|
+
if (this.cleanupTimer) {
|
|
1259
|
+
clearInterval(this.cleanupTimer);
|
|
1260
|
+
this.cleanupTimer = null;
|
|
1261
|
+
}
|
|
1262
|
+
await this.adapter.clear();
|
|
1263
|
+
logger_default.info("Cache storage cleared");
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* 生成缓存键
|
|
1267
|
+
* @param moduleName 模块名
|
|
1268
|
+
* @param methodName 方法名
|
|
1269
|
+
* @param keyFunction 可选的 key 函数
|
|
1270
|
+
* @param args 方法参数
|
|
1271
|
+
* @returns 缓存键(格式:模块名:方法名:hash)
|
|
1272
|
+
*/
|
|
1273
|
+
generateCacheKey(moduleName, methodName, keyFunction, args) {
|
|
1274
|
+
const keyData = keyFunction ? keyFunction(...args) : args;
|
|
1275
|
+
const serialized = ejson.stringify(keyData);
|
|
1276
|
+
const hash = createHash("sha256").update(serialized).digest("hex");
|
|
1277
|
+
return `${moduleName}:${methodName}:${hash}`;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* 清理过期缓存
|
|
1281
|
+
*/
|
|
1282
|
+
async cleanup() {
|
|
1283
|
+
if (typeof this.adapter.cleanupExpired === "function") {
|
|
1284
|
+
const cleaned = await this.adapter.cleanupExpired();
|
|
1285
|
+
if (cleaned > 0) {
|
|
1286
|
+
logger_default.debug(`Cleaned up ${cleaned} expired cache entry(ies)`);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
const now = Date.now();
|
|
1290
|
+
let cleaned = 0;
|
|
1291
|
+
const keys = await this.adapter.keys();
|
|
1292
|
+
for (const key of keys) {
|
|
1293
|
+
const item = await this.adapter.get(key);
|
|
1294
|
+
if (!item || item.expiresAt <= now) {
|
|
1295
|
+
await this.adapter.delete(key);
|
|
1296
|
+
cleaned++;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (cleaned > 0) {
|
|
1300
|
+
logger_default.debug(`Cleaned up ${cleaned} expired cache entry(ies)`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* 获取缓存统计信息
|
|
1306
|
+
*/
|
|
1307
|
+
async getStats() {
|
|
1308
|
+
return await this.adapter.getStats();
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* 清空所有缓存
|
|
1312
|
+
*/
|
|
1313
|
+
async clear() {
|
|
1314
|
+
await this.adapter.clear();
|
|
1315
|
+
logger_default.info("All cache cleared");
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* 删除指定的缓存项
|
|
1319
|
+
*/
|
|
1320
|
+
async delete(key) {
|
|
1321
|
+
return await this.adapter.delete(key);
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
// src/plugins/cache/decorator.ts
|
|
1326
|
+
function Cache(options = {}) {
|
|
1327
|
+
return Handler({
|
|
1328
|
+
type: "cache",
|
|
1329
|
+
options: {
|
|
1330
|
+
ttl: options.ttl ?? 6e4,
|
|
1331
|
+
// 默认1分钟
|
|
1332
|
+
key: options.key,
|
|
1333
|
+
// key 函数
|
|
1334
|
+
enabled: options.enabled ?? true
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/plugins/graceful-shutdown/plugin.ts
|
|
1340
|
+
var GracefulShutdownPlugin = class {
|
|
1341
|
+
name = "graceful-shutdown-plugin";
|
|
1342
|
+
priority = 50 /* SYSTEM */;
|
|
1343
|
+
// 系统级插件,最高优先级
|
|
1344
|
+
engine = null;
|
|
1345
|
+
options;
|
|
1346
|
+
// 正在执行的处理器计数
|
|
1347
|
+
activeHandlers = 0;
|
|
1348
|
+
// 是否正在停机
|
|
1349
|
+
isShuttingDown = false;
|
|
1350
|
+
// 停机超时定时器
|
|
1351
|
+
shutdownTimer = null;
|
|
1352
|
+
// 信号监听器(用于清理)
|
|
1353
|
+
signalListeners = /* @__PURE__ */ new Map();
|
|
1354
|
+
constructor(options) {
|
|
1355
|
+
this.options = {
|
|
1356
|
+
shutdownTimeout: options?.shutdownTimeout ?? 10 * 60 * 1e3,
|
|
1357
|
+
// 默认10分钟
|
|
1358
|
+
enabled: options?.enabled ?? true
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* 引擎初始化钩子
|
|
1363
|
+
*/
|
|
1364
|
+
onInit(engine) {
|
|
1365
|
+
this.engine = engine;
|
|
1366
|
+
logger_default.info("GracefulShutdownPlugin initialized");
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Handler加载钩子:拦截所有处理器,追踪执行状态
|
|
1370
|
+
*/
|
|
1371
|
+
onHandlerLoad(handlers) {
|
|
1372
|
+
if (!this.options.enabled) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
for (const handler of handlers) {
|
|
1376
|
+
handler.wrap(async (next, instance, ...args) => {
|
|
1377
|
+
if (this.isShuttingDown) {
|
|
1378
|
+
throw new Error("Service is shutting down, new requests are not accepted");
|
|
1379
|
+
}
|
|
1380
|
+
this.incrementActiveHandlers();
|
|
1381
|
+
try {
|
|
1382
|
+
const result = await next();
|
|
1383
|
+
return result;
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
throw error;
|
|
1386
|
+
} finally {
|
|
1387
|
+
this.decrementActiveHandlers();
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
logger_default.info(
|
|
1392
|
+
`GracefulShutdownPlugin: Tracking ${handlers.length} handler(s)`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* 引擎启动后钩子:注册系统信号监听器
|
|
1397
|
+
*/
|
|
1398
|
+
async onAfterStart(engine) {
|
|
1399
|
+
if (!this.options.enabled) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
this.engine = engine;
|
|
1403
|
+
this.registerSignalHandlers();
|
|
1404
|
+
logger_default.info(
|
|
1405
|
+
`GracefulShutdownPlugin: Signal handlers registered, shutdown timeout: ${this.options.shutdownTimeout}ms`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* 注册系统信号监听器
|
|
1410
|
+
*/
|
|
1411
|
+
registerSignalHandlers() {
|
|
1412
|
+
const signals = [
|
|
1413
|
+
"SIGINT",
|
|
1414
|
+
// Ctrl+C (Unix/Linux/Mac)
|
|
1415
|
+
"SIGTERM",
|
|
1416
|
+
// 终止信号 (Unix/Linux/Mac)
|
|
1417
|
+
"SIGBREAK"
|
|
1418
|
+
// Ctrl+Break (Windows)
|
|
1419
|
+
];
|
|
1420
|
+
for (const signal of signals) {
|
|
1421
|
+
if (process.platform === "win32" && signal === "SIGTERM") {
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
const handler = () => {
|
|
1425
|
+
logger_default.info(`GracefulShutdownPlugin: Received ${signal} signal`);
|
|
1426
|
+
this.initiateShutdown();
|
|
1427
|
+
};
|
|
1428
|
+
process.on(signal, handler);
|
|
1429
|
+
this.signalListeners.set(signal, handler);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* 增加活跃处理器计数
|
|
1434
|
+
*/
|
|
1435
|
+
incrementActiveHandlers() {
|
|
1436
|
+
this.activeHandlers++;
|
|
1437
|
+
logger_default.debug(
|
|
1438
|
+
`GracefulShutdownPlugin: Active handlers: ${this.activeHandlers}`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* 减少活跃处理器计数
|
|
1443
|
+
*/
|
|
1444
|
+
decrementActiveHandlers() {
|
|
1445
|
+
this.activeHandlers = Math.max(0, this.activeHandlers - 1);
|
|
1446
|
+
logger_default.debug(
|
|
1447
|
+
`GracefulShutdownPlugin: Active handlers: ${this.activeHandlers}`
|
|
1448
|
+
);
|
|
1449
|
+
if (this.isShuttingDown && this.activeHandlers === 0) {
|
|
1450
|
+
logger_default.info(
|
|
1451
|
+
"GracefulShutdownPlugin: All handlers completed, proceeding with shutdown"
|
|
1452
|
+
);
|
|
1453
|
+
this.completeShutdown();
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* 启动优雅停机流程
|
|
1458
|
+
*/
|
|
1459
|
+
async initiateShutdown() {
|
|
1460
|
+
if (this.isShuttingDown) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
this.isShuttingDown = true;
|
|
1464
|
+
logger_default.info(
|
|
1465
|
+
`GracefulShutdownPlugin: Initiating graceful shutdown, waiting for ${this.activeHandlers} active handler(s) to complete`
|
|
1466
|
+
);
|
|
1467
|
+
if (this.activeHandlers === 0) {
|
|
1468
|
+
logger_default.info(
|
|
1469
|
+
"GracefulShutdownPlugin: No active handlers, proceeding with shutdown immediately"
|
|
1470
|
+
);
|
|
1471
|
+
await this.completeShutdown();
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
this.shutdownTimer = setTimeout(() => {
|
|
1475
|
+
logger_default.warn(
|
|
1476
|
+
`GracefulShutdownPlugin: Shutdown timeout (${this.options.shutdownTimeout}ms) reached, forcing shutdown`
|
|
1477
|
+
);
|
|
1478
|
+
this.completeShutdown();
|
|
1479
|
+
}, this.options.shutdownTimeout);
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* 完成停机流程
|
|
1483
|
+
*/
|
|
1484
|
+
async completeShutdown() {
|
|
1485
|
+
if (this.shutdownTimer) {
|
|
1486
|
+
clearTimeout(this.shutdownTimer);
|
|
1487
|
+
this.shutdownTimer = null;
|
|
1488
|
+
}
|
|
1489
|
+
this.cleanupSignalHandlers();
|
|
1490
|
+
if (this.engine) {
|
|
1491
|
+
try {
|
|
1492
|
+
await this.engine.stop();
|
|
1493
|
+
logger_default.info("GracefulShutdownPlugin: Engine stopped successfully");
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
logger_default.error("GracefulShutdownPlugin: Failed to stop engine", error);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
logger_default.info("GracefulShutdownPlugin: Process exiting");
|
|
1499
|
+
process.exit(0);
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* 清理信号监听器
|
|
1503
|
+
*/
|
|
1504
|
+
cleanupSignalHandlers() {
|
|
1505
|
+
for (const [signal, handler] of this.signalListeners.entries()) {
|
|
1506
|
+
process.removeListener(signal, handler);
|
|
1507
|
+
}
|
|
1508
|
+
this.signalListeners.clear();
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* 引擎销毁钩子:清理资源
|
|
1512
|
+
*/
|
|
1513
|
+
async onDestroy() {
|
|
1514
|
+
this.cleanupSignalHandlers();
|
|
1515
|
+
if (this.shutdownTimer) {
|
|
1516
|
+
clearTimeout(this.shutdownTimer);
|
|
1517
|
+
this.shutdownTimer = null;
|
|
1518
|
+
}
|
|
1519
|
+
logger_default.info("GracefulShutdownPlugin: Cleaned up");
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* 获取当前活跃处理器数量(用于测试和监控)
|
|
1523
|
+
*/
|
|
1524
|
+
getActiveHandlersCount() {
|
|
1525
|
+
return this.activeHandlers;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* 检查是否正在停机(用于测试和监控)
|
|
1529
|
+
*/
|
|
1530
|
+
isShuttingDownNow() {
|
|
1531
|
+
return this.isShuttingDown;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// src/plugins/schedule/types.ts
|
|
1536
|
+
var ScheduleMode = /* @__PURE__ */ ((ScheduleMode2) => {
|
|
1537
|
+
ScheduleMode2["FIXED_RATE"] = "FIXED_RATE";
|
|
1538
|
+
ScheduleMode2["FIXED_DELAY"] = "FIXED_DELAY";
|
|
1539
|
+
return ScheduleMode2;
|
|
1540
|
+
})(ScheduleMode || {});
|
|
1541
|
+
|
|
1542
|
+
// src/plugins/schedule/decorator.ts
|
|
1543
|
+
function Schedule(options) {
|
|
1544
|
+
return Handler({
|
|
1545
|
+
type: "schedule",
|
|
1546
|
+
options: {
|
|
1547
|
+
interval: options.interval,
|
|
1548
|
+
mode: options.mode || "FIXED_RATE" /* FIXED_RATE */
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/plugins/schedule/mock-etcd.ts
|
|
1554
|
+
var MockEtcd3 = class {
|
|
1555
|
+
elections = /* @__PURE__ */ new Map();
|
|
1556
|
+
election(key, ttl) {
|
|
1557
|
+
if (!this.elections.has(key)) {
|
|
1558
|
+
this.elections.set(key, new MockElection(key));
|
|
1559
|
+
}
|
|
1560
|
+
return this.elections.get(key);
|
|
1561
|
+
}
|
|
1562
|
+
close() {
|
|
1563
|
+
}
|
|
1564
|
+
delete() {
|
|
1565
|
+
return {
|
|
1566
|
+
prefix: () => Promise.resolve()
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
get(key) {
|
|
1570
|
+
return {
|
|
1571
|
+
string: () => Promise.resolve(null)
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
getElection(key) {
|
|
1575
|
+
return this.elections.get(key);
|
|
1576
|
+
}
|
|
1577
|
+
clearElections() {
|
|
1578
|
+
this.elections.clear();
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
var MockElection = class {
|
|
1582
|
+
constructor(key) {
|
|
1583
|
+
this.key = key;
|
|
1584
|
+
}
|
|
1585
|
+
observers = [];
|
|
1586
|
+
campaigns = [];
|
|
1587
|
+
currentLeader = null;
|
|
1588
|
+
async observe() {
|
|
1589
|
+
const observer = new MockObserver(this);
|
|
1590
|
+
this.observers.push(observer);
|
|
1591
|
+
if (this.currentLeader) {
|
|
1592
|
+
setTimeout(() => {
|
|
1593
|
+
observer.notifyChange(this.currentLeader);
|
|
1594
|
+
}, 0);
|
|
1595
|
+
}
|
|
1596
|
+
return observer;
|
|
1597
|
+
}
|
|
1598
|
+
campaign(candidate) {
|
|
1599
|
+
const campaign = new MockCampaign(candidate, this);
|
|
1600
|
+
this.campaigns.push(campaign);
|
|
1601
|
+
if (!this.currentLeader) {
|
|
1602
|
+
setTimeout(() => {
|
|
1603
|
+
this.setLeader(candidate);
|
|
1604
|
+
}, 50);
|
|
1605
|
+
}
|
|
1606
|
+
return campaign;
|
|
1607
|
+
}
|
|
1608
|
+
setLeader(leader) {
|
|
1609
|
+
this.currentLeader = leader;
|
|
1610
|
+
for (const observer of this.observers) {
|
|
1611
|
+
observer.notifyChange(leader);
|
|
1612
|
+
}
|
|
1613
|
+
setTimeout(() => {
|
|
1614
|
+
for (const campaign of this.campaigns) {
|
|
1615
|
+
if (campaign.candidate === leader && !campaign.resigned) {
|
|
1616
|
+
campaign.notifyElected();
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}, 50);
|
|
1620
|
+
}
|
|
1621
|
+
getCurrentLeader() {
|
|
1622
|
+
return this.currentLeader;
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
var MockObserver = class {
|
|
1626
|
+
constructor(election) {
|
|
1627
|
+
this.election = election;
|
|
1628
|
+
}
|
|
1629
|
+
changeCallbacks = [];
|
|
1630
|
+
disconnectedCallbacks = [];
|
|
1631
|
+
errorCallbacks = [];
|
|
1632
|
+
on(event, callback) {
|
|
1633
|
+
if (event === "change") {
|
|
1634
|
+
this.changeCallbacks.push(callback);
|
|
1635
|
+
} else if (event === "disconnected") {
|
|
1636
|
+
this.disconnectedCallbacks.push(callback);
|
|
1637
|
+
} else if (event === "error") {
|
|
1638
|
+
this.errorCallbacks.push(callback);
|
|
1639
|
+
}
|
|
1640
|
+
return this;
|
|
1641
|
+
}
|
|
1642
|
+
notifyChange(leader) {
|
|
1643
|
+
for (const callback of this.changeCallbacks) {
|
|
1644
|
+
callback(leader);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
var MockCampaign = class {
|
|
1649
|
+
constructor(candidate, election) {
|
|
1650
|
+
this.candidate = candidate;
|
|
1651
|
+
this.election = election;
|
|
1652
|
+
}
|
|
1653
|
+
errorCallbacks = [];
|
|
1654
|
+
electedCallbacks = [];
|
|
1655
|
+
resigned = false;
|
|
1656
|
+
on(event, callback) {
|
|
1657
|
+
if (event === "error") {
|
|
1658
|
+
this.errorCallbacks.push(callback);
|
|
1659
|
+
} else if (event === "elected") {
|
|
1660
|
+
this.electedCallbacks.push(callback);
|
|
1661
|
+
}
|
|
1662
|
+
return this;
|
|
1663
|
+
}
|
|
1664
|
+
notifyElected() {
|
|
1665
|
+
if (!this.resigned) {
|
|
1666
|
+
for (const callback of this.electedCallbacks) {
|
|
1667
|
+
callback();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
notifyError(error) {
|
|
1672
|
+
for (const callback of this.errorCallbacks) {
|
|
1673
|
+
callback(error);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async resign() {
|
|
1677
|
+
this.resigned = true;
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
var tracer = trace.getTracer("scheduler");
|
|
1681
|
+
var Scheduler = class {
|
|
1682
|
+
constructor(etcdClient) {
|
|
1683
|
+
this.etcdClient = etcdClient;
|
|
1684
|
+
}
|
|
1685
|
+
campaigns = /* @__PURE__ */ new Map();
|
|
1686
|
+
timers = /* @__PURE__ */ new Map();
|
|
1687
|
+
isLeader = /* @__PURE__ */ new Map();
|
|
1688
|
+
/**
|
|
1689
|
+
* 启动调度任务
|
|
1690
|
+
*/
|
|
1691
|
+
async startSchedule(serviceId, moduleName, methodName, electionKey, metadata, method) {
|
|
1692
|
+
const election = this.etcdClient.election(electionKey, 10);
|
|
1693
|
+
const observe = await election.observe();
|
|
1694
|
+
observe.on("change", (leader) => {
|
|
1695
|
+
const isLeader = leader === serviceId;
|
|
1696
|
+
this.isLeader.set(serviceId, isLeader);
|
|
1697
|
+
if (!isLeader) {
|
|
1698
|
+
this.stopTimer(serviceId);
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
const campaign = election.campaign(serviceId);
|
|
1702
|
+
this.campaigns.set(serviceId, campaign);
|
|
1703
|
+
campaign.on("error", (error) => {
|
|
1704
|
+
logger_default.error(`Error in campaign for ${moduleName}.${methodName}:`, error);
|
|
1705
|
+
});
|
|
1706
|
+
campaign.on("elected", () => {
|
|
1707
|
+
this.isLeader.set(serviceId, true);
|
|
1708
|
+
this.startTimer(serviceId, metadata, moduleName, method);
|
|
1709
|
+
logger_default.info(`become leader for ${moduleName}.${methodName}`);
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* 启动定时器
|
|
1714
|
+
*/
|
|
1715
|
+
startTimer(serviceId, metadata, moduleName, method) {
|
|
1716
|
+
this.stopTimer(serviceId);
|
|
1717
|
+
const wrappedMethod = async () => {
|
|
1718
|
+
tracer.startActiveSpan(
|
|
1719
|
+
`ScheduleTask ${moduleName}.${metadata.name}`,
|
|
1720
|
+
{ root: true },
|
|
1721
|
+
async (span) => {
|
|
1722
|
+
span.setAttribute("serviceId", serviceId);
|
|
1723
|
+
span.setAttribute("methodName", metadata.name);
|
|
1724
|
+
span.setAttribute("moduleName", moduleName);
|
|
1725
|
+
span.setAttribute("interval", metadata.interval);
|
|
1726
|
+
span.setAttribute("mode", metadata.mode);
|
|
1727
|
+
try {
|
|
1728
|
+
await method();
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
span.setStatus({
|
|
1731
|
+
code: SpanStatusCode.ERROR,
|
|
1732
|
+
message: error.message
|
|
1733
|
+
});
|
|
1734
|
+
logger_default.error(
|
|
1735
|
+
`Error executing schedule task ${moduleName}.${metadata.name}:`,
|
|
1736
|
+
error
|
|
1737
|
+
);
|
|
1738
|
+
} finally {
|
|
1739
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
1740
|
+
span.end();
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
);
|
|
1744
|
+
};
|
|
1745
|
+
if (metadata.mode === "FIXED_DELAY" /* FIXED_DELAY */) {
|
|
1746
|
+
const runTask = async () => {
|
|
1747
|
+
if (!this.isLeader.get(serviceId)) return;
|
|
1748
|
+
try {
|
|
1749
|
+
await wrappedMethod();
|
|
1750
|
+
} finally {
|
|
1751
|
+
this.timers.set(serviceId, setTimeout(runTask, metadata.interval));
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
runTask();
|
|
1755
|
+
} else {
|
|
1756
|
+
this.timers.set(
|
|
1757
|
+
serviceId,
|
|
1758
|
+
setInterval(async () => {
|
|
1759
|
+
if (!this.isLeader.get(serviceId)) return;
|
|
1760
|
+
await wrappedMethod();
|
|
1761
|
+
}, metadata.interval)
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* 停止定时器
|
|
1767
|
+
*/
|
|
1768
|
+
stopTimer(serviceId) {
|
|
1769
|
+
const timer = this.timers.get(serviceId);
|
|
1770
|
+
if (timer) {
|
|
1771
|
+
clearTimeout(timer);
|
|
1772
|
+
clearInterval(timer);
|
|
1773
|
+
this.timers.delete(serviceId);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* 停止所有调度任务
|
|
1778
|
+
*/
|
|
1779
|
+
async stop() {
|
|
1780
|
+
for (const serviceId of this.timers.keys()) {
|
|
1781
|
+
this.stopTimer(serviceId);
|
|
1782
|
+
}
|
|
1783
|
+
for (const [serviceId, campaign] of this.campaigns.entries()) {
|
|
1784
|
+
try {
|
|
1785
|
+
await campaign.resign().catch(() => {
|
|
1786
|
+
});
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
logger_default.error(`Error stopping schedule ${serviceId}:`, error);
|
|
1789
|
+
} finally {
|
|
1790
|
+
this.campaigns.delete(serviceId);
|
|
1791
|
+
this.isLeader.delete(serviceId);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// src/plugins/schedule/utils.ts
|
|
1798
|
+
function extractScheduleMetadata(handlers) {
|
|
1799
|
+
const result = /* @__PURE__ */ new Map();
|
|
1800
|
+
const scheduleHandlers = handlers.filter(
|
|
1801
|
+
(handler) => handler.type === "schedule"
|
|
1802
|
+
);
|
|
1803
|
+
for (const handler of scheduleHandlers) {
|
|
1804
|
+
const moduleClass = handler.module;
|
|
1805
|
+
const methodName = handler.methodName;
|
|
1806
|
+
const options = handler.options || {};
|
|
1807
|
+
if (!result.has(moduleClass)) {
|
|
1808
|
+
result.set(moduleClass, /* @__PURE__ */ new Map());
|
|
1809
|
+
}
|
|
1810
|
+
const moduleMetadata = result.get(moduleClass);
|
|
1811
|
+
moduleMetadata.set(methodName, {
|
|
1812
|
+
name: methodName,
|
|
1813
|
+
interval: options.interval,
|
|
1814
|
+
mode: options.mode || "FIXED_RATE" /* FIXED_RATE */
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
return result;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/plugins/schedule/plugin.ts
|
|
1821
|
+
var SchedulePlugin = class {
|
|
1822
|
+
name = "schedule-plugin";
|
|
1823
|
+
priority = 300 /* BUSINESS */;
|
|
1824
|
+
// 业务逻辑优先级
|
|
1825
|
+
engine;
|
|
1826
|
+
scheduler = null;
|
|
1827
|
+
etcdClient = null;
|
|
1828
|
+
scheduleHandlers = [];
|
|
1829
|
+
useMockEtcd = false;
|
|
1830
|
+
constructor(options) {
|
|
1831
|
+
if (options?.useMockEtcd) {
|
|
1832
|
+
this.useMockEtcd = true;
|
|
1833
|
+
this.etcdClient = new MockEtcd3();
|
|
1834
|
+
logger_default.info("SchedulePlugin: Using MockEtcd3 for local development/testing");
|
|
1835
|
+
} else if (options?.etcdClient) {
|
|
1836
|
+
this.etcdClient = options.etcdClient;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* 引擎初始化钩子
|
|
1841
|
+
*/
|
|
1842
|
+
onInit(engine) {
|
|
1843
|
+
this.engine = engine;
|
|
1844
|
+
logger_default.info("SchedulePlugin initialized");
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Handler加载钩子:收集所有调度任务
|
|
1848
|
+
*/
|
|
1849
|
+
onHandlerLoad(handlers) {
|
|
1850
|
+
this.scheduleHandlers = handlers.filter(
|
|
1851
|
+
(handler) => handler.type === "schedule"
|
|
1852
|
+
);
|
|
1853
|
+
logger_default.info(
|
|
1854
|
+
`SchedulePlugin: Found ${this.scheduleHandlers.length} schedule handler(s)`
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* 引擎启动后钩子:启动所有调度任务
|
|
1859
|
+
*/
|
|
1860
|
+
async onAfterStart(engine) {
|
|
1861
|
+
if (!this.etcdClient) {
|
|
1862
|
+
logger_default.warn(
|
|
1863
|
+
"SchedulePlugin: etcdClient not configured and useMockEtcd is false, schedule tasks will not start"
|
|
1864
|
+
);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
this.scheduler = new Scheduler(this.etcdClient);
|
|
1868
|
+
const modules = engine.getModules();
|
|
1869
|
+
if (!this.scheduleHandlers) {
|
|
1870
|
+
logger_default.warn(
|
|
1871
|
+
"SchedulePlugin: No schedule handlers found, schedule tasks will not start"
|
|
1872
|
+
);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const scheduleMetadataMap = extractScheduleMetadata(this.scheduleHandlers);
|
|
1876
|
+
const serviceId = `${engine.options.name}-${Date.now()}-${Math.random()}`;
|
|
1877
|
+
for (const moduleMetadata of modules) {
|
|
1878
|
+
const moduleClass = moduleMetadata.clazz;
|
|
1879
|
+
const moduleName = moduleMetadata.name;
|
|
1880
|
+
const moduleInstance = engine.get(moduleClass);
|
|
1881
|
+
const moduleScheduleMetadata = scheduleMetadataMap.get(moduleClass);
|
|
1882
|
+
if (!moduleScheduleMetadata || moduleScheduleMetadata.size === 0) {
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
for (const [methodName, metadata] of moduleScheduleMetadata.entries()) {
|
|
1886
|
+
const method = moduleInstance[methodName];
|
|
1887
|
+
if (typeof method !== "function") {
|
|
1888
|
+
logger_default.warn(
|
|
1889
|
+
`SchedulePlugin: Method ${moduleName}.${methodName} is not a function, skipping`
|
|
1890
|
+
);
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
const electionKey = `/schedule/${engine.options.name}/${moduleName}/${methodName}`;
|
|
1894
|
+
const taskServiceId = `${serviceId}-${moduleName}-${methodName}`;
|
|
1895
|
+
try {
|
|
1896
|
+
await this.scheduler.startSchedule(
|
|
1897
|
+
taskServiceId,
|
|
1898
|
+
moduleName,
|
|
1899
|
+
methodName,
|
|
1900
|
+
electionKey,
|
|
1901
|
+
metadata,
|
|
1902
|
+
method.bind(moduleInstance)
|
|
1903
|
+
);
|
|
1904
|
+
logger_default.info(
|
|
1905
|
+
`SchedulePlugin: Started schedule task ${moduleName}.${methodName} (interval: ${metadata.interval}ms, mode: ${metadata.mode})`
|
|
1906
|
+
);
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
logger_default.error(
|
|
1909
|
+
`SchedulePlugin: Failed to start schedule task ${moduleName}.${methodName}:`,
|
|
1910
|
+
error
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* 引擎销毁钩子:停止所有调度任务
|
|
1918
|
+
*/
|
|
1919
|
+
async onDestroy() {
|
|
1920
|
+
if (this.scheduler) {
|
|
1921
|
+
try {
|
|
1922
|
+
await this.scheduler.stop();
|
|
1923
|
+
logger_default.info("SchedulePlugin: All schedule tasks stopped");
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
logger_default.error("SchedulePlugin: Error stopping schedule tasks:", error);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
async function formatCode(code) {
|
|
1931
|
+
try {
|
|
1932
|
+
return prettier.format(code, { parser: "typescript" });
|
|
1933
|
+
} catch {
|
|
1934
|
+
return code;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// src/plugins/client-code/generator.ts
|
|
1939
|
+
function getZodTypeString(schema, defaultOptional = false) {
|
|
1940
|
+
function processType(type) {
|
|
1941
|
+
if (!type) {
|
|
1942
|
+
return "unknown";
|
|
1943
|
+
}
|
|
1944
|
+
const def = type._def;
|
|
1945
|
+
const typeName = def.type;
|
|
1946
|
+
if (typeName === "nullable") {
|
|
1947
|
+
return `${processType(def.innerType)} | null`;
|
|
1948
|
+
}
|
|
1949
|
+
if (typeName === "optional") {
|
|
1950
|
+
return processType(def.innerType);
|
|
1951
|
+
}
|
|
1952
|
+
if (typeName === "pipe" && def.in) {
|
|
1953
|
+
return processType(def.in);
|
|
1954
|
+
}
|
|
1955
|
+
if (typeName === "effects" && def.schema) {
|
|
1956
|
+
return processType(def.schema);
|
|
1957
|
+
}
|
|
1958
|
+
switch (typeName) {
|
|
1959
|
+
case "string": {
|
|
1960
|
+
return "string";
|
|
1961
|
+
}
|
|
1962
|
+
case "number": {
|
|
1963
|
+
return "number";
|
|
1964
|
+
}
|
|
1965
|
+
case "bigint": {
|
|
1966
|
+
return "bigint";
|
|
1967
|
+
}
|
|
1968
|
+
case "boolean": {
|
|
1969
|
+
return "boolean";
|
|
1970
|
+
}
|
|
1971
|
+
case "array": {
|
|
1972
|
+
const elementType = processType(def.element);
|
|
1973
|
+
return `${elementType}[]`;
|
|
1974
|
+
}
|
|
1975
|
+
case "date": {
|
|
1976
|
+
return "Date";
|
|
1977
|
+
}
|
|
1978
|
+
case "object": {
|
|
1979
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1980
|
+
const props = Object.entries(shape).map(([key, value]) => {
|
|
1981
|
+
if (key.includes("-")) {
|
|
1982
|
+
key = `'${key}'`;
|
|
1983
|
+
}
|
|
1984
|
+
const fieldDef = value._def;
|
|
1985
|
+
const fieldTypeName = fieldDef.type;
|
|
1986
|
+
const isOptional = fieldTypeName === "optional";
|
|
1987
|
+
const isDefault = defaultOptional && fieldTypeName === "default";
|
|
1988
|
+
const fieldType = processType(
|
|
1989
|
+
isOptional ? fieldDef.innerType : value
|
|
1990
|
+
);
|
|
1991
|
+
return `${key}${isOptional || isDefault ? "?" : ""}: ${fieldType}`;
|
|
1992
|
+
}).join("; ");
|
|
1993
|
+
return `{ ${props} }`;
|
|
1994
|
+
}
|
|
1995
|
+
case "union": {
|
|
1996
|
+
return def.options.map((opt) => processType(opt)).join(" | ");
|
|
1997
|
+
}
|
|
1998
|
+
case "null": {
|
|
1999
|
+
return "null";
|
|
2000
|
+
}
|
|
2001
|
+
case "promise": {
|
|
2002
|
+
return `Promise<${processType(def.type)}>`;
|
|
2003
|
+
}
|
|
2004
|
+
case "void": {
|
|
2005
|
+
return "void";
|
|
2006
|
+
}
|
|
2007
|
+
case "record": {
|
|
2008
|
+
if (def.valueType) {
|
|
2009
|
+
const keyType = def.keyType ? processType(def.keyType) : "string";
|
|
2010
|
+
return `Record<${keyType}, ${processType(def.valueType)}>`;
|
|
2011
|
+
} else if (def.keyType) {
|
|
2012
|
+
return `Record<string, ${processType(def.keyType)}>`;
|
|
2013
|
+
}
|
|
2014
|
+
return "Record<string, any>";
|
|
2015
|
+
}
|
|
2016
|
+
case "map": {
|
|
2017
|
+
return `Map<${processType(def.keyType)}, ${processType(
|
|
2018
|
+
def.valueType
|
|
2019
|
+
)}>`;
|
|
2020
|
+
}
|
|
2021
|
+
case "any": {
|
|
2022
|
+
return "any";
|
|
2023
|
+
}
|
|
2024
|
+
case "unknown": {
|
|
2025
|
+
return "unknown";
|
|
2026
|
+
}
|
|
2027
|
+
case "enum": {
|
|
2028
|
+
const values = def.entries ? Object.values(def.entries) : [];
|
|
2029
|
+
return "(" + values.map((opt) => `"${String(opt)}"`).join(" | ") + ")";
|
|
2030
|
+
}
|
|
2031
|
+
case "default": {
|
|
2032
|
+
return processType(def.innerType);
|
|
2033
|
+
}
|
|
2034
|
+
default: {
|
|
2035
|
+
if (type.safeParse(new Uint8Array()).success) {
|
|
2036
|
+
return "Uint8Array";
|
|
2037
|
+
}
|
|
2038
|
+
return "unknown";
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return processType(schema);
|
|
2043
|
+
}
|
|
2044
|
+
async function generateClientCode(modules) {
|
|
2045
|
+
const imports = [
|
|
2046
|
+
"// \u8FD9\u4E2A\u6587\u4EF6\u662F\u81EA\u52A8\u751F\u6210\u7684\uFF0C\u8BF7\u4E0D\u8981\u624B\u52A8\u4FEE\u6539",
|
|
2047
|
+
"",
|
|
2048
|
+
'import { MicroserviceClient as BaseMicroserviceClient } from "imean-service-client";',
|
|
2049
|
+
'export * from "imean-service-client";',
|
|
2050
|
+
""
|
|
2051
|
+
].join("\n");
|
|
2052
|
+
function toPascalCase(moduleName) {
|
|
2053
|
+
return moduleName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
2054
|
+
}
|
|
2055
|
+
function toCamelCase(moduleName) {
|
|
2056
|
+
const parts = moduleName.split("-");
|
|
2057
|
+
return parts[0] + parts.slice(1).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
2058
|
+
}
|
|
2059
|
+
const interfaces = Object.entries(modules).map(([name, module]) => {
|
|
2060
|
+
const methods = Object.entries(module.actions).map(([actionName, action]) => {
|
|
2061
|
+
if (!action.params) {
|
|
2062
|
+
throw new Error(`Missing params for action ${actionName}`);
|
|
2063
|
+
}
|
|
2064
|
+
const paramNames = action.paramNames || [];
|
|
2065
|
+
const params = action.params.map((param, index) => {
|
|
2066
|
+
const paramName = paramNames[index] || param.description || `arg${index}`;
|
|
2067
|
+
const paramDef = param._def;
|
|
2068
|
+
const isOptional = param.isOptional();
|
|
2069
|
+
const hasDefault = paramDef?.type === "default";
|
|
2070
|
+
return `${paramName}${isOptional || hasDefault ? "?" : ""}: ${getZodTypeString(
|
|
2071
|
+
param,
|
|
2072
|
+
true
|
|
2073
|
+
)}`;
|
|
2074
|
+
}).join(", ");
|
|
2075
|
+
const returnType = action.returns ? getZodTypeString(action.returns) : "void";
|
|
2076
|
+
return `
|
|
2077
|
+
/**
|
|
2078
|
+
* ${action.description || ""}
|
|
2079
|
+
*/
|
|
2080
|
+
${actionName}: (${params}) => Promise<${action.stream ? `AsyncIterable<${returnType}>` : returnType}>;`;
|
|
2081
|
+
}).join("\n ");
|
|
2082
|
+
const interfaceName = `${toPascalCase(name)}Module`;
|
|
2083
|
+
return `export interface ${interfaceName} {
|
|
2084
|
+
${methods}
|
|
2085
|
+
}`;
|
|
2086
|
+
}).join("\n\n");
|
|
2087
|
+
const clientClass = `export class MicroserviceClient extends BaseMicroserviceClient {
|
|
2088
|
+
constructor(options: any) {
|
|
2089
|
+
super(options);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
${Object.entries(modules).map(([name, module]) => {
|
|
2093
|
+
const methods = Object.entries(module.actions).map(([actionName, action]) => {
|
|
2094
|
+
return `${actionName}: { idempotent: ${!!action.idempotence}, stream: ${!!action.stream} }`;
|
|
2095
|
+
}).join(",\n ");
|
|
2096
|
+
const propertyName = toCamelCase(name);
|
|
2097
|
+
const interfaceName = `${toPascalCase(name)}Module`;
|
|
2098
|
+
return `public readonly ${propertyName} = this.registerModule<${interfaceName}>("${name}", {
|
|
2099
|
+
${methods}
|
|
2100
|
+
});`;
|
|
2101
|
+
}).join("\n\n ")}
|
|
2102
|
+
}`;
|
|
2103
|
+
return await formatCode([imports, interfaces, clientClass].join("\n\n"));
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// src/plugins/client-code/utils.ts
|
|
2107
|
+
function extractParamNames(func) {
|
|
2108
|
+
if (!func || typeof func !== "function") {
|
|
2109
|
+
return [];
|
|
2110
|
+
}
|
|
2111
|
+
try {
|
|
2112
|
+
const funcStr = func.toString();
|
|
2113
|
+
const match = funcStr.match(
|
|
2114
|
+
/(?:async\s+)?(?:function\s+\w*\s*)?\(([^)]*)\)|(?:async\s+)?\(([^)]*)\)\s*=>/
|
|
2115
|
+
);
|
|
2116
|
+
if (!match) {
|
|
2117
|
+
return [];
|
|
2118
|
+
}
|
|
2119
|
+
const paramsStr = match[1] || match[2] || "";
|
|
2120
|
+
if (!paramsStr.trim()) {
|
|
2121
|
+
return [];
|
|
2122
|
+
}
|
|
2123
|
+
return paramsStr.split(",").map((param) => {
|
|
2124
|
+
return param.replace(/\/\*.*?\*\//g, "").replace(/\/\/.*$/g, "").replace(/:\s*[^=,]+/g, "").replace(/\s*=\s*[^,]+/g, "").trim();
|
|
2125
|
+
}).filter((name) => name.length > 0);
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
return [];
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
function convertHandlersToModuleInfo(handlers) {
|
|
2131
|
+
const modules = {};
|
|
2132
|
+
const actionHandlers = handlers.filter(
|
|
2133
|
+
(handler) => handler.type === "action"
|
|
2134
|
+
);
|
|
2135
|
+
for (const handler of actionHandlers) {
|
|
2136
|
+
const actionOptions = handler.options;
|
|
2137
|
+
const moduleClass = handler.module;
|
|
2138
|
+
const methodName = handler.methodName;
|
|
2139
|
+
const moduleName = moduleClass.name.toLowerCase().replace(/service$/, "");
|
|
2140
|
+
if (!modules[moduleName]) {
|
|
2141
|
+
modules[moduleName] = {
|
|
2142
|
+
name: moduleName,
|
|
2143
|
+
actions: {}
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
const paramNames = extractParamNames(handler.method);
|
|
2147
|
+
modules[moduleName].actions[methodName] = {
|
|
2148
|
+
description: actionOptions.description,
|
|
2149
|
+
params: actionOptions.params || [],
|
|
2150
|
+
returns: actionOptions.returns,
|
|
2151
|
+
stream: actionOptions.stream || false,
|
|
2152
|
+
idempotence: actionOptions.idempotence || false,
|
|
2153
|
+
paramNames
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
return modules;
|
|
2157
|
+
}
|
|
2158
|
+
function convertHandlersToModuleInfoWithMetadata(handlers, getModuleMetadata) {
|
|
2159
|
+
const modules = {};
|
|
2160
|
+
const actionHandlers = handlers.filter(
|
|
2161
|
+
(handler) => handler.type === "action"
|
|
2162
|
+
);
|
|
2163
|
+
for (const handler of actionHandlers) {
|
|
2164
|
+
const actionOptions = handler.options;
|
|
2165
|
+
const moduleClass = handler.module;
|
|
2166
|
+
const methodName = handler.methodName;
|
|
2167
|
+
const moduleMetadata = getModuleMetadata(moduleClass);
|
|
2168
|
+
if (!moduleMetadata) {
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
const moduleName = moduleMetadata.name;
|
|
2172
|
+
if (!modules[moduleName]) {
|
|
2173
|
+
modules[moduleName] = {
|
|
2174
|
+
name: moduleName,
|
|
2175
|
+
actions: {}
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
const paramNames = extractParamNames(handler.method);
|
|
2179
|
+
modules[moduleName].actions[methodName] = {
|
|
2180
|
+
description: actionOptions.description,
|
|
2181
|
+
params: actionOptions.params || [],
|
|
2182
|
+
returns: actionOptions.returns,
|
|
2183
|
+
stream: actionOptions.stream || false,
|
|
2184
|
+
idempotence: actionOptions.idempotence || false,
|
|
2185
|
+
paramNames
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
return modules;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/plugins/client-code/plugin.ts
|
|
2192
|
+
var ClientCodePlugin = class {
|
|
2193
|
+
name = "client-code-plugin";
|
|
2194
|
+
priority = 1e3 /* ROUTE */;
|
|
2195
|
+
// 路由插件优先级,在 ActionPlugin 之后
|
|
2196
|
+
engine;
|
|
2197
|
+
actionHandlers = [];
|
|
2198
|
+
generatedCode = null;
|
|
2199
|
+
clientSavePath;
|
|
2200
|
+
constructor(options) {
|
|
2201
|
+
this.clientSavePath = options?.clientSavePath;
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* 引擎初始化钩子
|
|
2205
|
+
*/
|
|
2206
|
+
onInit(engine) {
|
|
2207
|
+
this.engine = engine;
|
|
2208
|
+
logger_default.info("ClientCodePlugin initialized");
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Handler加载钩子:收集所有 Action handlers
|
|
2212
|
+
*/
|
|
2213
|
+
onHandlerLoad(handlers) {
|
|
2214
|
+
const actionHandlers = handlers.filter(
|
|
2215
|
+
(handler) => handler.type === "action"
|
|
2216
|
+
);
|
|
2217
|
+
this.actionHandlers = actionHandlers;
|
|
2218
|
+
logger_default.info(
|
|
2219
|
+
`ClientCodePlugin collected ${actionHandlers.length} action handler(s)`
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* 引擎启动后钩子:注册客户端代码下载路由
|
|
2224
|
+
*/
|
|
2225
|
+
async onAfterStart(engine) {
|
|
2226
|
+
await this.generateCode();
|
|
2227
|
+
const prefix = engine.options.prefix || "";
|
|
2228
|
+
const clientPath = prefix ? `${prefix}/client.ts` : "/client.ts";
|
|
2229
|
+
const hono = engine.getHono();
|
|
2230
|
+
hono.get(clientPath, async (ctx) => {
|
|
2231
|
+
if (!this.generatedCode) {
|
|
2232
|
+
await this.generateCode();
|
|
2233
|
+
}
|
|
2234
|
+
return ctx.text(this.generatedCode || "", 200, {
|
|
2235
|
+
"Content-Type": "text/typescript; charset=utf-8",
|
|
2236
|
+
"Content-Disposition": `attachment; filename="client.ts"`
|
|
2237
|
+
});
|
|
2238
|
+
});
|
|
2239
|
+
logger_default.info(`Client code available at ${clientPath}`);
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* 生成客户端代码
|
|
2243
|
+
*/
|
|
2244
|
+
async generateCode() {
|
|
2245
|
+
try {
|
|
2246
|
+
const getModuleMetadata = (moduleClass) => {
|
|
2247
|
+
const modules2 = this.engine.getModules();
|
|
2248
|
+
const moduleMetadata = modules2.find((m) => m.clazz === moduleClass);
|
|
2249
|
+
return moduleMetadata ? { name: moduleMetadata.name } : void 0;
|
|
2250
|
+
};
|
|
2251
|
+
const modules = convertHandlersToModuleInfoWithMetadata(
|
|
2252
|
+
this.actionHandlers,
|
|
2253
|
+
getModuleMetadata
|
|
2254
|
+
);
|
|
2255
|
+
this.generatedCode = await generateClientCode(modules);
|
|
2256
|
+
logger_default.debug(
|
|
2257
|
+
`Generated client code for ${Object.keys(modules).length} module(s)`
|
|
2258
|
+
);
|
|
2259
|
+
if (this.clientSavePath && this.generatedCode) {
|
|
2260
|
+
await this.saveCodeToFile(this.clientSavePath, this.generatedCode);
|
|
2261
|
+
}
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
logger_default.error("Failed to generate client code", error);
|
|
2264
|
+
this.generatedCode = "// Error: Failed to generate client code";
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* 保存代码到文件
|
|
2269
|
+
*/
|
|
2270
|
+
async saveCodeToFile(path, code) {
|
|
2271
|
+
try {
|
|
2272
|
+
const dir = dirname(path);
|
|
2273
|
+
await promises.mkdir(dir, { recursive: true });
|
|
2274
|
+
await promises.writeFile(path, code, "utf-8");
|
|
2275
|
+
logger_default.info(`Client code saved to ${path}`);
|
|
2276
|
+
} catch (error) {
|
|
2277
|
+
logger_default.error(`Failed to save client code to ${path}`, error);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
var EtcdConfigStorage = class {
|
|
2282
|
+
etcdClient;
|
|
2283
|
+
prefix;
|
|
2284
|
+
watchers = /* @__PURE__ */ new Map();
|
|
2285
|
+
cache = /* @__PURE__ */ new Map();
|
|
2286
|
+
constructor(etcdClient, prefix = "/config") {
|
|
2287
|
+
this.etcdClient = etcdClient;
|
|
2288
|
+
this.prefix = prefix;
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* 构建完整的 etcd 键
|
|
2292
|
+
*/
|
|
2293
|
+
buildKey(key) {
|
|
2294
|
+
return `${this.prefix}/${key}`;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* 获取配置
|
|
2298
|
+
*/
|
|
2299
|
+
async get(key) {
|
|
2300
|
+
try {
|
|
2301
|
+
const fullKey = this.buildKey(key);
|
|
2302
|
+
const value = await this.etcdClient.get(fullKey).string();
|
|
2303
|
+
if (value === null || value === void 0) {
|
|
2304
|
+
logger_default.warn(`get config from etcd: ${fullKey} - not found`);
|
|
2305
|
+
return null;
|
|
2306
|
+
}
|
|
2307
|
+
if (value.trim() === "") {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
const parsed = ejson.parse(value);
|
|
2311
|
+
this.cache.set(key, parsed);
|
|
2312
|
+
return parsed;
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
logger_default.error(`Failed to get config from etcd: ${key}`, error);
|
|
2315
|
+
return this.cache.get(key) || null;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* 同步获取缓存的配置(不访问 etcd)
|
|
2320
|
+
* 用于 configProxy 的同步访问
|
|
2321
|
+
*/
|
|
2322
|
+
getCached(key) {
|
|
2323
|
+
return this.cache.get(key);
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* 设置配置
|
|
2327
|
+
*/
|
|
2328
|
+
async set(key, value, metadata) {
|
|
2329
|
+
try {
|
|
2330
|
+
const fullKey = this.buildKey(key);
|
|
2331
|
+
const serialized = ejson.stringify(value);
|
|
2332
|
+
await this.etcdClient.put(fullKey).value(serialized);
|
|
2333
|
+
this.cache.set(key, value);
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
logger_default.error(`Failed to set config in etcd: ${key}`, error);
|
|
2336
|
+
throw error;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* 删除配置
|
|
2341
|
+
*/
|
|
2342
|
+
async delete(key) {
|
|
2343
|
+
try {
|
|
2344
|
+
const fullKey = this.buildKey(key);
|
|
2345
|
+
await this.etcdClient.delete().key(fullKey);
|
|
2346
|
+
this.cache.delete(key);
|
|
2347
|
+
} catch (error) {
|
|
2348
|
+
logger_default.error(`Failed to delete config from etcd: ${key}`, error);
|
|
2349
|
+
throw error;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* 获取所有配置
|
|
2354
|
+
*/
|
|
2355
|
+
async getAll(prefix) {
|
|
2356
|
+
try {
|
|
2357
|
+
const searchPrefix = prefix ? this.buildKey(prefix) : this.buildKey("");
|
|
2358
|
+
const response = await this.etcdClient.getAll().prefix(searchPrefix).strings();
|
|
2359
|
+
const result = /* @__PURE__ */ new Map();
|
|
2360
|
+
for (const key in response) {
|
|
2361
|
+
const cleanKey = key.replace(this.prefix + "/", "");
|
|
2362
|
+
const value = ejson.parse(response[key]);
|
|
2363
|
+
result.set(cleanKey, value);
|
|
2364
|
+
this.cache.set(cleanKey, value);
|
|
2365
|
+
}
|
|
2366
|
+
return result;
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
logger_default.error(`Failed to get all configs from etcd`, error);
|
|
2369
|
+
return /* @__PURE__ */ new Map();
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* 监听配置变化
|
|
2374
|
+
*/
|
|
2375
|
+
watch(key, callback) {
|
|
2376
|
+
const fullKey = this.buildKey(key);
|
|
2377
|
+
if (!this.watchers.has(key)) {
|
|
2378
|
+
this.watchers.set(key, /* @__PURE__ */ new Set());
|
|
2379
|
+
this.etcdClient.watch().key(fullKey).create().then((watcher) => {
|
|
2380
|
+
watcher.on("put", (kv) => {
|
|
2381
|
+
try {
|
|
2382
|
+
const newValue = ejson.parse(kv.value.toString());
|
|
2383
|
+
const oldValue = this.cache.get(key);
|
|
2384
|
+
this.cache.set(key, newValue);
|
|
2385
|
+
const callbacks = this.watchers.get(key);
|
|
2386
|
+
if (callbacks) {
|
|
2387
|
+
callbacks.forEach((cb) => {
|
|
2388
|
+
try {
|
|
2389
|
+
cb(newValue, oldValue);
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
logger_default.error(
|
|
2392
|
+
`Config watch callback error for key: ${key}`,
|
|
2393
|
+
error
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
logger_default.error(`Failed to parse config value for key: ${key}`, error);
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
watcher.on("delete", () => {
|
|
2403
|
+
const oldValue = this.cache.get(key);
|
|
2404
|
+
this.cache.delete(key);
|
|
2405
|
+
const callbacks = this.watchers.get(key);
|
|
2406
|
+
if (callbacks) {
|
|
2407
|
+
callbacks.forEach((cb) => {
|
|
2408
|
+
try {
|
|
2409
|
+
cb(null, oldValue);
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
logger_default.error(
|
|
2412
|
+
`Config watch callback error for key: ${key}`,
|
|
2413
|
+
error
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
}).catch((error) => {
|
|
2420
|
+
logger_default.error(`Failed to create etcd watcher for key: ${key}`, error);
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
this.watchers.get(key).add(callback);
|
|
2424
|
+
return () => {
|
|
2425
|
+
const callbacks = this.watchers.get(key);
|
|
2426
|
+
if (callbacks) {
|
|
2427
|
+
callbacks.delete(callback);
|
|
2428
|
+
if (callbacks.size === 0) {
|
|
2429
|
+
this.watchers.delete(key);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* 清理资源
|
|
2436
|
+
*/
|
|
2437
|
+
async destroy() {
|
|
2438
|
+
this.watchers.clear();
|
|
2439
|
+
this.cache.clear();
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
var MemoryConfigStorage = class {
|
|
2443
|
+
storage = /* @__PURE__ */ new Map();
|
|
2444
|
+
watchers = /* @__PURE__ */ new Map();
|
|
2445
|
+
async get(key) {
|
|
2446
|
+
return this.storage.get(key) || null;
|
|
2447
|
+
}
|
|
2448
|
+
getCached(key) {
|
|
2449
|
+
return this.storage.get(key);
|
|
2450
|
+
}
|
|
2451
|
+
async set(key, value, metadata) {
|
|
2452
|
+
const oldValue = this.storage.get(key);
|
|
2453
|
+
this.storage.set(key, value);
|
|
2454
|
+
const callbacks = this.watchers.get(key);
|
|
2455
|
+
if (callbacks) {
|
|
2456
|
+
callbacks.forEach((cb) => {
|
|
2457
|
+
try {
|
|
2458
|
+
cb(value, oldValue);
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
logger_default.error(`Config watch callback error for key: ${key}`, error);
|
|
2461
|
+
}
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
async delete(key) {
|
|
2466
|
+
const oldValue = this.storage.get(key);
|
|
2467
|
+
this.storage.delete(key);
|
|
2468
|
+
const callbacks = this.watchers.get(key);
|
|
2469
|
+
if (callbacks) {
|
|
2470
|
+
callbacks.forEach((cb) => {
|
|
2471
|
+
try {
|
|
2472
|
+
cb(null, oldValue);
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
logger_default.error(`Config watch callback error for key: ${key}`, error);
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
async getAll(prefix) {
|
|
2480
|
+
if (!prefix) {
|
|
2481
|
+
return new Map(this.storage);
|
|
2482
|
+
}
|
|
2483
|
+
const result = /* @__PURE__ */ new Map();
|
|
2484
|
+
for (const [key, value] of this.storage.entries()) {
|
|
2485
|
+
if (key.startsWith(prefix)) {
|
|
2486
|
+
result.set(key, value);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
return result;
|
|
2490
|
+
}
|
|
2491
|
+
watch(key, callback) {
|
|
2492
|
+
if (!this.watchers.has(key)) {
|
|
2493
|
+
this.watchers.set(key, /* @__PURE__ */ new Set());
|
|
2494
|
+
}
|
|
2495
|
+
this.watchers.get(key).add(callback);
|
|
2496
|
+
return () => {
|
|
2497
|
+
const callbacks = this.watchers.get(key);
|
|
2498
|
+
if (callbacks) {
|
|
2499
|
+
callbacks.delete(callback);
|
|
2500
|
+
if (callbacks.size === 0) {
|
|
2501
|
+
this.watchers.delete(key);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
async destroy() {
|
|
2507
|
+
this.storage.clear();
|
|
2508
|
+
this.watchers.clear();
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
|
|
2512
|
+
// src/plugins/dynamic-config/plugin.ts
|
|
2513
|
+
function keyToEnvName(key) {
|
|
2514
|
+
return key.toUpperCase().replace(/-/g, "_");
|
|
2515
|
+
}
|
|
2516
|
+
function getEnvValue(envKey) {
|
|
2517
|
+
const value = process.env[envKey];
|
|
2518
|
+
if (value === void 0 || value === null) {
|
|
2519
|
+
return void 0;
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
return JSON.parse(value);
|
|
2523
|
+
} catch {
|
|
2524
|
+
return value;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
var DynamicConfigPlugin = class {
|
|
2528
|
+
name = "dynamic-config-plugin";
|
|
2529
|
+
priority = 300 /* BUSINESS */;
|
|
2530
|
+
engine;
|
|
2531
|
+
storage;
|
|
2532
|
+
configHandlers = [];
|
|
2533
|
+
unwatchFunctions = /* @__PURE__ */ new Map();
|
|
2534
|
+
constructor(options) {
|
|
2535
|
+
if (options?.useMockEtcd) {
|
|
2536
|
+
this.storage = new MemoryConfigStorage();
|
|
2537
|
+
logger_default.info(
|
|
2538
|
+
"DynamicConfigPlugin: Using MemoryConfigStorage for local development/testing"
|
|
2539
|
+
);
|
|
2540
|
+
} else if (options?.etcdClient) {
|
|
2541
|
+
this.storage = new EtcdConfigStorage(
|
|
2542
|
+
options.etcdClient,
|
|
2543
|
+
options.etcdPrefix || "/config"
|
|
2544
|
+
);
|
|
2545
|
+
logger_default.info("DynamicConfigPlugin: Using EtcdConfigStorage");
|
|
2546
|
+
} else {
|
|
2547
|
+
this.storage = new MemoryConfigStorage();
|
|
2548
|
+
logger_default.warn(
|
|
2549
|
+
"DynamicConfigPlugin: No etcdClient provided, using MemoryConfigStorage"
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* 声明 Module 配置 Schema
|
|
2555
|
+
*/
|
|
2556
|
+
getModuleOptionsSchema() {
|
|
2557
|
+
return {
|
|
2558
|
+
_type: {},
|
|
2559
|
+
validate: (options) => {
|
|
2560
|
+
return true;
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* 引擎初始化钩子
|
|
2566
|
+
*/
|
|
2567
|
+
onInit(engine) {
|
|
2568
|
+
this.engine = engine;
|
|
2569
|
+
logger_default.info("DynamicConfigPlugin initialized");
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Handler 加载钩子:收集所有动态配置
|
|
2573
|
+
*/
|
|
2574
|
+
onHandlerLoad(handlers) {
|
|
2575
|
+
this.configHandlers = handlers.filter(
|
|
2576
|
+
(handler) => handler.type === "dynamic-config"
|
|
2577
|
+
);
|
|
2578
|
+
logger_default.info(
|
|
2579
|
+
`DynamicConfigPlugin: Found ${this.configHandlers.length} dynamic config handler(s)`
|
|
2580
|
+
);
|
|
2581
|
+
for (const handler of this.configHandlers) {
|
|
2582
|
+
this.wrapConfigHandler(handler);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
/**
|
|
2586
|
+
* 包装配置方法,实现动态配置注入
|
|
2587
|
+
*/
|
|
2588
|
+
wrapConfigHandler(handler) {
|
|
2589
|
+
const options = handler.options;
|
|
2590
|
+
const moduleName = this.getModuleName(handler.module);
|
|
2591
|
+
const methodName = handler.methodName;
|
|
2592
|
+
const configKey = this.buildConfigKey(moduleName, options.key);
|
|
2593
|
+
logger_default.info(
|
|
2594
|
+
`DynamicConfigPlugin: Wrapping ${moduleName}.${methodName} with config key: ${configKey}`
|
|
2595
|
+
);
|
|
2596
|
+
handler.wrap(async (next, instance, ...args) => {
|
|
2597
|
+
let configValue = await this.storage.get(configKey);
|
|
2598
|
+
const fromStorage = configValue !== null && configValue !== void 0;
|
|
2599
|
+
let fromEnv = false;
|
|
2600
|
+
if (!fromStorage) {
|
|
2601
|
+
const envKey = keyToEnvName(options.key);
|
|
2602
|
+
configValue = getEnvValue(envKey);
|
|
2603
|
+
if (configValue !== void 0) {
|
|
2604
|
+
fromEnv = true;
|
|
2605
|
+
logger_default.debug(
|
|
2606
|
+
`DynamicConfigPlugin: Loaded config from env: ${configKey} (${envKey})`
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
if (configValue === null || configValue === void 0) {
|
|
2611
|
+
configValue = options.defaultValue;
|
|
2612
|
+
}
|
|
2613
|
+
if (options.schema && configValue !== void 0) {
|
|
2614
|
+
try {
|
|
2615
|
+
configValue = options.schema.parse(configValue);
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
logger_default.error(
|
|
2618
|
+
`DynamicConfigPlugin: Config validation failed for ${configKey}`,
|
|
2619
|
+
error
|
|
2620
|
+
);
|
|
2621
|
+
configValue = options.defaultValue;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
if (!fromStorage && !fromEnv && configValue !== void 0 && configValue !== null) {
|
|
2625
|
+
try {
|
|
2626
|
+
await this.storage.set(configKey, configValue);
|
|
2627
|
+
} catch (error) {
|
|
2628
|
+
logger_default.error(
|
|
2629
|
+
`DynamicConfigPlugin: Failed to cache config value for ${configKey}`,
|
|
2630
|
+
error
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
return configValue;
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* 引擎启动后钩子:启动配置监听和预加载配置
|
|
2639
|
+
*/
|
|
2640
|
+
async onAfterStart(engine) {
|
|
2641
|
+
await this.preloadConfigs();
|
|
2642
|
+
for (const handler of this.configHandlers) {
|
|
2643
|
+
const options = handler.options;
|
|
2644
|
+
const moduleName = this.getModuleName(handler.module);
|
|
2645
|
+
const configKey = this.buildConfigKey(moduleName, options.key);
|
|
2646
|
+
if (options.onChange) {
|
|
2647
|
+
const unwatch = this.storage.watch(
|
|
2648
|
+
configKey,
|
|
2649
|
+
async (newValue, oldValue) => {
|
|
2650
|
+
try {
|
|
2651
|
+
logger_default.info(
|
|
2652
|
+
`DynamicConfigPlugin: Config changed for ${configKey}`,
|
|
2653
|
+
{
|
|
2654
|
+
sensitive: options.sensitive,
|
|
2655
|
+
oldValue: options.sensitive ? "***" : oldValue,
|
|
2656
|
+
newValue: options.sensitive ? "***" : newValue
|
|
2657
|
+
}
|
|
2658
|
+
);
|
|
2659
|
+
await options.onChange(newValue, oldValue);
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
logger_default.error(
|
|
2662
|
+
`DynamicConfigPlugin: onChange callback error for ${configKey}`,
|
|
2663
|
+
error
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
);
|
|
2668
|
+
this.unwatchFunctions.set(configKey, unwatch);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
logger_default.info(
|
|
2672
|
+
`DynamicConfigPlugin: Started watching ${this.unwatchFunctions.size} config(s)`
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* 预加载所有配置到缓存,并将配置方法/属性转换为同步 getter
|
|
2677
|
+
*/
|
|
2678
|
+
async preloadConfigs() {
|
|
2679
|
+
const modules = this.engine.getModules();
|
|
2680
|
+
for (const moduleMetadata of modules) {
|
|
2681
|
+
const moduleClass = moduleMetadata.clazz;
|
|
2682
|
+
const moduleInstance = this.engine.get(moduleClass);
|
|
2683
|
+
const moduleConfigHandlers = this.configHandlers.filter(
|
|
2684
|
+
(h) => h.module === moduleClass
|
|
2685
|
+
);
|
|
2686
|
+
const fieldMetadata = getAllHandlerFieldMetadata(moduleClass);
|
|
2687
|
+
const moduleConfigFields = Array.from(fieldMetadata.entries()).filter(
|
|
2688
|
+
([_, handlers]) => handlers.some((h) => h.type === "dynamic-config")
|
|
2689
|
+
);
|
|
2690
|
+
if (moduleConfigHandlers.length === 0 && moduleConfigFields.length === 0) {
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
for (const handler of moduleConfigHandlers) {
|
|
2694
|
+
const methodName = handler.methodName;
|
|
2695
|
+
const method = moduleInstance[methodName];
|
|
2696
|
+
if (typeof method === "function") {
|
|
2697
|
+
try {
|
|
2698
|
+
const configValue = await method.call(moduleInstance);
|
|
2699
|
+
const options = handler.options;
|
|
2700
|
+
const moduleName = this.getModuleName(moduleClass);
|
|
2701
|
+
const configKey = this.buildConfigKey(moduleName, options.key);
|
|
2702
|
+
const cachedValue = this.storage.getCached ? this.storage.getCached(configKey) : void 0;
|
|
2703
|
+
if (cachedValue === null || cachedValue === void 0) {
|
|
2704
|
+
if (configValue !== void 0 && configValue !== null) {
|
|
2705
|
+
await this.storage.set(configKey, configValue);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
Object.defineProperty(moduleInstance, methodName, {
|
|
2709
|
+
get: () => {
|
|
2710
|
+
let value = this.storage.getCached ? this.storage.getCached(configKey) : void 0;
|
|
2711
|
+
if (value === null || value === void 0) {
|
|
2712
|
+
const envKey = keyToEnvName(options.key);
|
|
2713
|
+
value = getEnvValue(envKey);
|
|
2714
|
+
}
|
|
2715
|
+
return value !== void 0 && value !== null ? value : options.defaultValue;
|
|
2716
|
+
},
|
|
2717
|
+
enumerable: true,
|
|
2718
|
+
configurable: true
|
|
2719
|
+
});
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
logger_default.error(
|
|
2722
|
+
`DynamicConfigPlugin: Failed to preload config ${moduleMetadata.name}.${methodName}`,
|
|
2723
|
+
error
|
|
2724
|
+
);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
for (const [fieldName, handlers] of moduleConfigFields) {
|
|
2729
|
+
const configHandler = handlers.find((h) => h.type === "dynamic-config");
|
|
2730
|
+
if (!configHandler) continue;
|
|
2731
|
+
try {
|
|
2732
|
+
const options = configHandler.options;
|
|
2733
|
+
const moduleName = this.getModuleName(moduleClass);
|
|
2734
|
+
const configKey = this.buildConfigKey(moduleName, options.key);
|
|
2735
|
+
let configValue = await this.storage.get(configKey);
|
|
2736
|
+
if (configValue === null || configValue === void 0) {
|
|
2737
|
+
const envKey = keyToEnvName(options.key);
|
|
2738
|
+
configValue = getEnvValue(envKey);
|
|
2739
|
+
}
|
|
2740
|
+
if (configValue === null || configValue === void 0) {
|
|
2741
|
+
configValue = options.defaultValue;
|
|
2742
|
+
}
|
|
2743
|
+
const cachedValue = this.storage.getCached ? this.storage.getCached(configKey) : void 0;
|
|
2744
|
+
if (cachedValue === null || cachedValue === void 0) {
|
|
2745
|
+
if (configValue !== void 0 && configValue !== null) {
|
|
2746
|
+
await this.storage.set(configKey, configValue);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
Object.defineProperty(moduleInstance, fieldName, {
|
|
2750
|
+
get: () => {
|
|
2751
|
+
let value = this.storage.getCached ? this.storage.getCached(configKey) : void 0;
|
|
2752
|
+
if (value === null || value === void 0) {
|
|
2753
|
+
const envKey = keyToEnvName(options.key);
|
|
2754
|
+
value = getEnvValue(envKey);
|
|
2755
|
+
}
|
|
2756
|
+
return value !== void 0 && value !== null ? value : options.defaultValue;
|
|
2757
|
+
},
|
|
2758
|
+
enumerable: true,
|
|
2759
|
+
configurable: true
|
|
2760
|
+
});
|
|
2761
|
+
const fieldHandler = {
|
|
2762
|
+
...configHandler,
|
|
2763
|
+
methodName: String(fieldName),
|
|
2764
|
+
module: moduleClass,
|
|
2765
|
+
method: () => {
|
|
2766
|
+
}
|
|
2767
|
+
// 属性没有 method,使用 any 绕过类型检查
|
|
2768
|
+
};
|
|
2769
|
+
this.configHandlers.push(fieldHandler);
|
|
2770
|
+
} catch (error) {
|
|
2771
|
+
logger_default.error(
|
|
2772
|
+
`DynamicConfigPlugin: Failed to preload config ${moduleMetadata.name}.${String(fieldName)}`,
|
|
2773
|
+
error
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
logger_default.info(
|
|
2779
|
+
`DynamicConfigPlugin: Preloaded ${this.configHandlers.length} config(s) and created sync accessors`
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* 引擎销毁钩子:清理资源
|
|
2784
|
+
*/
|
|
2785
|
+
async onDestroy() {
|
|
2786
|
+
for (const unwatch of this.unwatchFunctions.values()) {
|
|
2787
|
+
unwatch();
|
|
2788
|
+
}
|
|
2789
|
+
this.unwatchFunctions.clear();
|
|
2790
|
+
if (this.storage && typeof this.storage.destroy === "function") {
|
|
2791
|
+
await this.storage.destroy();
|
|
2792
|
+
}
|
|
2793
|
+
logger_default.info("DynamicConfigPlugin: Cleanup completed");
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* 获取模块名称
|
|
2797
|
+
*/
|
|
2798
|
+
getModuleName(moduleClass) {
|
|
2799
|
+
const moduleMetadata = this.engine.getModules().find((m) => m.clazz === moduleClass);
|
|
2800
|
+
return moduleMetadata?.name || moduleClass.name;
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* 构建完整的配置键
|
|
2804
|
+
*/
|
|
2805
|
+
buildConfigKey(moduleName, configKey) {
|
|
2806
|
+
const serviceName = this.engine.options.name;
|
|
2807
|
+
return `${serviceName}/${moduleName}/${configKey}`;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* 公共 API:获取配置
|
|
2811
|
+
*/
|
|
2812
|
+
async getConfig(key) {
|
|
2813
|
+
return await this.storage.get(key);
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* 公共 API:同步获取配置(从缓存)
|
|
2817
|
+
* 用于 configProxy 的同步访问
|
|
2818
|
+
*/
|
|
2819
|
+
getConfigCached(key) {
|
|
2820
|
+
return this.storage.getCached ? this.storage.getCached(key) : void 0;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* 公共 API:设置配置
|
|
2824
|
+
*/
|
|
2825
|
+
async setConfig(key, value, metadata) {
|
|
2826
|
+
await this.storage.set(key, value, metadata);
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* 公共 API:删除配置
|
|
2830
|
+
*/
|
|
2831
|
+
async deleteConfig(key) {
|
|
2832
|
+
await this.storage.delete(key);
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* 公共 API:获取所有配置
|
|
2836
|
+
*/
|
|
2837
|
+
async getAllConfigs(prefix) {
|
|
2838
|
+
return await this.storage.getAll(prefix);
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* 公共 API:监听配置变化
|
|
2842
|
+
*/
|
|
2843
|
+
watchConfig(key, callback) {
|
|
2844
|
+
return this.storage.watch(key, callback);
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
// src/plugins/dynamic-config/decorator.ts
|
|
2849
|
+
function Config(options) {
|
|
2850
|
+
return HandlerField({
|
|
2851
|
+
type: "dynamic-config",
|
|
2852
|
+
options
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
var Microservice3 = class {
|
|
2856
|
+
// 存储已注册的模块
|
|
2857
|
+
modules = [];
|
|
2858
|
+
// 存储已注册的插件(按注册顺序)
|
|
2859
|
+
plugins = [];
|
|
2860
|
+
// 插件注册表(以name为键,用于覆盖逻辑)
|
|
2861
|
+
pluginRegistry = /* @__PURE__ */ new Map();
|
|
2862
|
+
// 模块实例缓存(单例管理)
|
|
2863
|
+
moduleInstances = /* @__PURE__ */ new Map();
|
|
2864
|
+
// 引擎配置(冻结,只读)
|
|
2865
|
+
options;
|
|
2866
|
+
// 是否已启动
|
|
2867
|
+
started = false;
|
|
2868
|
+
// 模块元数据键(用于通过双向访问查找模块)
|
|
2869
|
+
moduleMetadataKey;
|
|
2870
|
+
// 实际使用的端口(启动后设置)
|
|
2871
|
+
actualPort = null;
|
|
2872
|
+
// Hono 实例(由引擎统一管理)
|
|
2873
|
+
hono = new Hono();
|
|
2874
|
+
// HTTP 服务器实例
|
|
2875
|
+
server = null;
|
|
2876
|
+
constructor(options, moduleMetadataKey) {
|
|
2877
|
+
this.options = Object.freeze({
|
|
2878
|
+
hostname: "0.0.0.0",
|
|
2879
|
+
...options
|
|
2880
|
+
});
|
|
2881
|
+
this.moduleMetadataKey = moduleMetadataKey;
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* 获取 Hono 实例(供插件使用)
|
|
2885
|
+
*/
|
|
2886
|
+
getHono() {
|
|
2887
|
+
return this.hono;
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* 内部注册插件方法(供构造函数和子类使用)
|
|
2891
|
+
* @protected
|
|
2892
|
+
*/
|
|
2893
|
+
registerPlugin(plugin) {
|
|
2894
|
+
if (!plugin.name || plugin.name.trim() === "") {
|
|
2895
|
+
throw new PluginNameRequiredError();
|
|
2896
|
+
}
|
|
2897
|
+
const existingPlugin = this.pluginRegistry.get(plugin.name);
|
|
2898
|
+
if (existingPlugin) {
|
|
2899
|
+
const oldIndex = this.plugins.indexOf(existingPlugin);
|
|
2900
|
+
if (oldIndex >= 0) {
|
|
2901
|
+
this.plugins.splice(oldIndex, 1);
|
|
2902
|
+
}
|
|
2903
|
+
logger_default.info(`Override plugin: ${plugin.name}`);
|
|
2904
|
+
}
|
|
2905
|
+
this.pluginRegistry.set(plugin.name, plugin);
|
|
2906
|
+
this.plugins.push(plugin);
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* 加载并注册所有模块(通过唯一的 key 查找)
|
|
2910
|
+
* 在引擎启动时调用,使用双向访问机制查找所有被装饰的类
|
|
2911
|
+
*
|
|
2912
|
+
* 注意:
|
|
2913
|
+
* - 每个引擎实例使用唯一的 moduleMetadataKey,实现隔离
|
|
2914
|
+
* - 模块类是静态的,可以被多个引擎实例共享
|
|
2915
|
+
* - 每个引擎实例会创建独立的模块实例,互不影响
|
|
2916
|
+
*/
|
|
2917
|
+
loadModules() {
|
|
2918
|
+
this.modules = [];
|
|
2919
|
+
const moduleClasses = getClassesByKey(this.moduleMetadataKey);
|
|
2920
|
+
for (const moduleClass of moduleClasses) {
|
|
2921
|
+
const metadata = getClassMetadata(moduleClass, this.moduleMetadataKey);
|
|
2922
|
+
const moduleName = metadata.name || moduleClass.name;
|
|
2923
|
+
const moduleMetadata = {
|
|
2924
|
+
name: moduleName,
|
|
2925
|
+
clazz: moduleClass,
|
|
2926
|
+
options: metadata.options || {}
|
|
2927
|
+
};
|
|
2928
|
+
this.modules.push(moduleMetadata);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* 加载Handler元数据(平铺结构)
|
|
2933
|
+
* 每个装饰器都是独立的HandlerMetadata条目
|
|
2934
|
+
* 为每个方法创建包装链管理器,提供简单的 wrap API
|
|
2935
|
+
*/
|
|
2936
|
+
loadHandlerMetadata() {
|
|
2937
|
+
const handlers = [];
|
|
2938
|
+
const wrapperChains = /* @__PURE__ */ new Map();
|
|
2939
|
+
const originalMethods = /* @__PURE__ */ new Map();
|
|
2940
|
+
for (const module of this.modules) {
|
|
2941
|
+
if (!this.moduleInstances.has(module.clazz)) {
|
|
2942
|
+
this.get(module.clazz);
|
|
2943
|
+
}
|
|
2944
|
+
const allMetadata = getAllHandlerMetadata(module.clazz);
|
|
2945
|
+
for (const [methodName, metadataList] of allMetadata.entries()) {
|
|
2946
|
+
const method = module.clazz.prototype[methodName];
|
|
2947
|
+
const methodNameStr = String(methodName);
|
|
2948
|
+
const methodKey = `${module.clazz.name}.${methodNameStr}`;
|
|
2949
|
+
if (!originalMethods.has(methodKey)) {
|
|
2950
|
+
originalMethods.set(methodKey, method);
|
|
2951
|
+
wrapperChains.set(methodKey, []);
|
|
2952
|
+
}
|
|
2953
|
+
for (const meta of metadataList) {
|
|
2954
|
+
const chain = wrapperChains.get(methodKey);
|
|
2955
|
+
handlers.push({
|
|
2956
|
+
...meta,
|
|
2957
|
+
method: originalMethods.get(methodKey),
|
|
2958
|
+
methodName: methodNameStr,
|
|
2959
|
+
module: module.clazz,
|
|
2960
|
+
// 提供简单的 wrap API:插件只需要调用这个方法
|
|
2961
|
+
wrap: (wrapper) => {
|
|
2962
|
+
chain.push(wrapper);
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
this.__wrapperChains__ = wrapperChains;
|
|
2969
|
+
this.__originalMethods__ = originalMethods;
|
|
2970
|
+
return handlers;
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* 应用所有包装链到原型
|
|
2974
|
+
* 在所有插件执行完 onHandlerLoad 后调用
|
|
2975
|
+
*/
|
|
2976
|
+
applyWrapperChains() {
|
|
2977
|
+
const wrapperChains = this.__wrapperChains__;
|
|
2978
|
+
const originalMethods = this.__originalMethods__;
|
|
2979
|
+
if (!wrapperChains || !originalMethods) return;
|
|
2980
|
+
for (const [methodKey, chain] of wrapperChains.entries()) {
|
|
2981
|
+
if (chain.length === 0) continue;
|
|
2982
|
+
const [moduleName, methodName] = methodKey.split(".");
|
|
2983
|
+
const module = this.modules.find((m) => m.clazz.name === moduleName);
|
|
2984
|
+
if (!module) continue;
|
|
2985
|
+
const originalMethod = originalMethods.get(methodKey);
|
|
2986
|
+
const prototype = module.clazz.prototype;
|
|
2987
|
+
let wrappedMethod = originalMethod;
|
|
2988
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
2989
|
+
const wrapper = chain[i];
|
|
2990
|
+
const next = wrappedMethod;
|
|
2991
|
+
wrappedMethod = async function(...args) {
|
|
2992
|
+
return wrapper(() => next.apply(this, args), this, ...args);
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
prototype[methodName] = wrappedMethod;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* 寻找一个随机的可用端口
|
|
3000
|
+
*/
|
|
3001
|
+
getRandomPort(hostname) {
|
|
3002
|
+
return new Promise((resolve, reject) => {
|
|
3003
|
+
const server = net.createServer();
|
|
3004
|
+
server.unref();
|
|
3005
|
+
server.on("error", reject);
|
|
3006
|
+
server.listen(0, hostname, () => {
|
|
3007
|
+
const port = server.address().port;
|
|
3008
|
+
server.close((err) => {
|
|
3009
|
+
if (err) {
|
|
3010
|
+
return reject(err);
|
|
3011
|
+
}
|
|
3012
|
+
resolve(port);
|
|
3013
|
+
});
|
|
3014
|
+
});
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* 启动引擎
|
|
3019
|
+
* @param requestedPort 启动端口(可选,默认0,表示随机端口)
|
|
3020
|
+
* @returns 实际使用的端口号
|
|
3021
|
+
*/
|
|
3022
|
+
async start(requestedPort = 0) {
|
|
3023
|
+
if (this.started) {
|
|
3024
|
+
throw new Error("Engine is already started");
|
|
3025
|
+
}
|
|
3026
|
+
try {
|
|
3027
|
+
const { port, hostname } = await this.determinePort(requestedPort);
|
|
3028
|
+
this.initializeModulesAndPlugins();
|
|
3029
|
+
this.processHandlers();
|
|
3030
|
+
await this.executePluginStartHooks();
|
|
3031
|
+
this.startHttpServer(port, hostname);
|
|
3032
|
+
this.started = true;
|
|
3033
|
+
logger_default.info(
|
|
3034
|
+
`${this.options.name} v${this.options.version} started successfully on port ${this.actualPort}`
|
|
3035
|
+
);
|
|
3036
|
+
return this.actualPort;
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
logger_default.error("Failed to start engine", error);
|
|
3039
|
+
throw error;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* 确定实际使用的端口
|
|
3044
|
+
*/
|
|
3045
|
+
async determinePort(requestedPort) {
|
|
3046
|
+
const hostname = this.options.hostname || "0.0.0.0";
|
|
3047
|
+
if (requestedPort !== 0) {
|
|
3048
|
+
this.actualPort = requestedPort;
|
|
3049
|
+
return { port: this.actualPort, hostname };
|
|
3050
|
+
}
|
|
3051
|
+
this.actualPort = await this.getRandomPort(hostname);
|
|
3052
|
+
return { port: this.actualPort, hostname };
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* 初始化模块和插件
|
|
3056
|
+
*/
|
|
3057
|
+
initializeModulesAndPlugins() {
|
|
3058
|
+
this.loadModules();
|
|
3059
|
+
this.executePluginHook("onInit", (plugin) => {
|
|
3060
|
+
plugin.onInit?.(this);
|
|
3061
|
+
});
|
|
3062
|
+
this.executePluginHook("onModuleLoad", (plugin) => {
|
|
3063
|
+
plugin.onModuleLoad?.(
|
|
3064
|
+
this.modules
|
|
3065
|
+
);
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* 处理 Handler 元数据和插件包装
|
|
3070
|
+
*/
|
|
3071
|
+
processHandlers() {
|
|
3072
|
+
const handlers = this.loadHandlerMetadata();
|
|
3073
|
+
const sortedPlugins = this.sortPluginsByPriority();
|
|
3074
|
+
const { wrapperPlugins, routePlugins } = this.separateWrapperAndRoutePlugins(sortedPlugins);
|
|
3075
|
+
this.executePluginHook(
|
|
3076
|
+
"onHandlerLoad",
|
|
3077
|
+
(plugin) => {
|
|
3078
|
+
plugin.onHandlerLoad?.(handlers);
|
|
3079
|
+
},
|
|
3080
|
+
wrapperPlugins
|
|
3081
|
+
);
|
|
3082
|
+
this.applyWrapperChains();
|
|
3083
|
+
this.executePluginHook(
|
|
3084
|
+
"onHandlerLoad",
|
|
3085
|
+
(plugin) => {
|
|
3086
|
+
plugin.onHandlerLoad?.(handlers);
|
|
3087
|
+
},
|
|
3088
|
+
routePlugins
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* 执行插件启动钩子
|
|
3093
|
+
*/
|
|
3094
|
+
async executePluginStartHooks() {
|
|
3095
|
+
this.executePluginHook("onBeforeStart", (plugin) => {
|
|
3096
|
+
plugin.onBeforeStart?.(this);
|
|
3097
|
+
});
|
|
3098
|
+
for (const plugin of this.plugins) {
|
|
3099
|
+
try {
|
|
3100
|
+
await plugin.onAfterStart?.(this);
|
|
3101
|
+
} catch (error) {
|
|
3102
|
+
throw new Error(
|
|
3103
|
+
`Plugin ${plugin.name} failed in onAfterStart: ${error instanceof Error ? error.message : String(error)}`
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
this.registerVersionRoute();
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* 注册版本路由(/prefix/version)
|
|
3111
|
+
* 用于健康检查和探针
|
|
3112
|
+
*/
|
|
3113
|
+
registerVersionRoute() {
|
|
3114
|
+
const prefix = this.options.prefix || "";
|
|
3115
|
+
const versionPath = prefix ? `${prefix}` : "/";
|
|
3116
|
+
const existingRoutes = this.hono.routes;
|
|
3117
|
+
const routeExists = existingRoutes.some(
|
|
3118
|
+
(route) => route.path === versionPath && route.method === "GET"
|
|
3119
|
+
);
|
|
3120
|
+
if (routeExists) {
|
|
3121
|
+
logger_default.info(
|
|
3122
|
+
`Version route ${versionPath} already exists, skipping registration`
|
|
3123
|
+
);
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
try {
|
|
3127
|
+
this.hono.get(versionPath, async (ctx) => {
|
|
3128
|
+
return ctx.json({
|
|
3129
|
+
name: this.options.name,
|
|
3130
|
+
version: this.options.version,
|
|
3131
|
+
status: "running"
|
|
3132
|
+
});
|
|
3133
|
+
});
|
|
3134
|
+
logger_default.info(`Registered version route: GET ${versionPath}`);
|
|
3135
|
+
} catch (error) {
|
|
3136
|
+
if (error instanceof Error && error.message.includes("matcher is already built")) {
|
|
3137
|
+
logger_default.warn(
|
|
3138
|
+
`Cannot register version route ${versionPath}: route matcher is already built. If you have already registered a route at ${versionPath}, it will be used instead.`
|
|
3139
|
+
);
|
|
3140
|
+
} else {
|
|
3141
|
+
throw error;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* 启动 HTTP 服务器
|
|
3147
|
+
*/
|
|
3148
|
+
startHttpServer(port, hostname) {
|
|
3149
|
+
this.server = serve({
|
|
3150
|
+
fetch: this.hono.fetch,
|
|
3151
|
+
port,
|
|
3152
|
+
hostname
|
|
3153
|
+
});
|
|
3154
|
+
logger_default.info(`HTTP server started on http://${hostname}:${port}`);
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* 按优先级排序插件
|
|
3158
|
+
*/
|
|
3159
|
+
sortPluginsByPriority() {
|
|
3160
|
+
return [...this.plugins].sort((a, b) => {
|
|
3161
|
+
const priorityA = this.getPluginPriority(a);
|
|
3162
|
+
const priorityB = this.getPluginPriority(b);
|
|
3163
|
+
if (priorityA !== priorityB) {
|
|
3164
|
+
return priorityA - priorityB;
|
|
3165
|
+
}
|
|
3166
|
+
const indexA = this.plugins.indexOf(a);
|
|
3167
|
+
const indexB = this.plugins.indexOf(b);
|
|
3168
|
+
return indexA - indexB;
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* 获取插件优先级
|
|
3173
|
+
*/
|
|
3174
|
+
getPluginPriority(plugin) {
|
|
3175
|
+
return plugin.priority ?? 300 /* BUSINESS */;
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* 分离包装插件和路由插件
|
|
3179
|
+
*/
|
|
3180
|
+
separateWrapperAndRoutePlugins(sortedPlugins) {
|
|
3181
|
+
const wrapperPlugins = [];
|
|
3182
|
+
const routePlugins = [];
|
|
3183
|
+
for (const plugin of sortedPlugins) {
|
|
3184
|
+
const priority = this.getPluginPriority(plugin);
|
|
3185
|
+
if (priority === 1e3 /* ROUTE */) {
|
|
3186
|
+
routePlugins.push(plugin);
|
|
3187
|
+
} else {
|
|
3188
|
+
wrapperPlugins.push(plugin);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
return { wrapperPlugins, routePlugins };
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* 执行插件钩子(通用方法)
|
|
3195
|
+
*/
|
|
3196
|
+
executePluginHook(hookName, callback, plugins = this.plugins) {
|
|
3197
|
+
for (const plugin of plugins) {
|
|
3198
|
+
try {
|
|
3199
|
+
callback(plugin);
|
|
3200
|
+
} catch (error) {
|
|
3201
|
+
throw new Error(
|
|
3202
|
+
`Plugin ${plugin.name} failed in ${hookName}: ${error instanceof Error ? error.message : String(error)}`
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* 停止引擎
|
|
3209
|
+
*/
|
|
3210
|
+
async stop() {
|
|
3211
|
+
if (!this.started) {
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
try {
|
|
3215
|
+
if (this.server) {
|
|
3216
|
+
if (typeof this.server.close === "function") {
|
|
3217
|
+
if (this.server.close.length > 0) {
|
|
3218
|
+
await new Promise((resolve, reject) => {
|
|
3219
|
+
this.server.close((err) => {
|
|
3220
|
+
if (err) {
|
|
3221
|
+
reject(err);
|
|
3222
|
+
} else {
|
|
3223
|
+
resolve();
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
});
|
|
3227
|
+
} else {
|
|
3228
|
+
this.server.close();
|
|
3229
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
this.server = null;
|
|
3233
|
+
logger_default.info("HTTP server stopped");
|
|
3234
|
+
}
|
|
3235
|
+
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
3236
|
+
const plugin = this.plugins[i];
|
|
3237
|
+
try {
|
|
3238
|
+
await plugin.onDestroy?.();
|
|
3239
|
+
} catch (error) {
|
|
3240
|
+
logger_default.error(`Plugin ${plugin.name} failed in onDestroy`, error);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
this.modules = [];
|
|
3244
|
+
this.moduleInstances.clear();
|
|
3245
|
+
this.started = false;
|
|
3246
|
+
this.actualPort = null;
|
|
3247
|
+
logger_default.info(`${this.options.name} stopped`);
|
|
3248
|
+
} catch (error) {
|
|
3249
|
+
logger_default.error("Failed to stop engine", error);
|
|
3250
|
+
throw error;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* 获取模块实例(单例)
|
|
3255
|
+
* @param moduleClass 模块类
|
|
3256
|
+
* @returns 模块实例
|
|
3257
|
+
*/
|
|
3258
|
+
get(moduleClass) {
|
|
3259
|
+
if (!this.moduleInstances.has(moduleClass)) {
|
|
3260
|
+
this.moduleInstances.set(moduleClass, new moduleClass());
|
|
3261
|
+
}
|
|
3262
|
+
return this.moduleInstances.get(moduleClass);
|
|
3263
|
+
}
|
|
3264
|
+
/**
|
|
3265
|
+
* 获取已注册的模块列表
|
|
3266
|
+
*/
|
|
3267
|
+
getModules() {
|
|
3268
|
+
if (this.modules.length === 0 && !this.started) {
|
|
3269
|
+
this.loadModules();
|
|
3270
|
+
}
|
|
3271
|
+
return [...this.modules];
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* 获取实际使用的端口
|
|
3275
|
+
*/
|
|
3276
|
+
getPort() {
|
|
3277
|
+
return this.actualPort;
|
|
3278
|
+
}
|
|
3279
|
+
/**
|
|
3280
|
+
* 确保引擎已初始化(模块和处理器已加载)
|
|
3281
|
+
* 如果引擎未启动,则执行必要的初始化步骤
|
|
3282
|
+
*/
|
|
3283
|
+
ensureInitialized() {
|
|
3284
|
+
if (this.started) {
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
if (this.modules.length === 0) {
|
|
3288
|
+
this.initializeModulesAndPlugins();
|
|
3289
|
+
}
|
|
3290
|
+
if (!this.__wrapperChains__) {
|
|
3291
|
+
this.processHandlers();
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* 获取模块处理器方法(不启动 HTTP 服务器)
|
|
3296
|
+
* 适用于测试场景,可以完整执行中间件和处理逻辑
|
|
3297
|
+
*
|
|
3298
|
+
* 返回一个已绑定模块和方法名的调用函数,调用时只需要传递方法参数
|
|
3299
|
+
* 类型推导:自动推导方法参数类型和返回值类型(无需显式指定泛型参数)
|
|
3300
|
+
*
|
|
3301
|
+
* @param moduleClass 模块类
|
|
3302
|
+
* @param methodName 方法名(handler 名称)
|
|
3303
|
+
* @returns 调用函数,只需要传递方法参数
|
|
3304
|
+
*
|
|
3305
|
+
* @example
|
|
3306
|
+
* ```typescript
|
|
3307
|
+
* @Module("users")
|
|
3308
|
+
* class UserService {
|
|
3309
|
+
* @Action({ params: [z.string(), z.number()] })
|
|
3310
|
+
* add(a: string, b: number): { result: number } {
|
|
3311
|
+
* return { result: Number(a) + b };
|
|
3312
|
+
* }
|
|
3313
|
+
*
|
|
3314
|
+
* @Action({ params: [z.string()] })
|
|
3315
|
+
* getUser(id: string): Promise<{ id: string; name: string }> {
|
|
3316
|
+
* return Promise.resolve({ id, name: "Alice" });
|
|
3317
|
+
* }
|
|
3318
|
+
* }
|
|
3319
|
+
*
|
|
3320
|
+
* // 获取 handler 并调用(类型自动推导,无需显式指定泛型)
|
|
3321
|
+
* const addHandler = engine.handler(UserService, "add");
|
|
3322
|
+
* const result1 = await addHandler("10", 20);
|
|
3323
|
+
* // result1 的类型是 { result: number }
|
|
3324
|
+
*
|
|
3325
|
+
* // 也可以链式调用
|
|
3326
|
+
* const result2 = await engine.handler(UserService, "getUser")("123");
|
|
3327
|
+
* // result2 的类型是 { id: string; name: string }(自动解包 Promise)
|
|
3328
|
+
* ```
|
|
3329
|
+
*/
|
|
3330
|
+
handler(moduleClass, methodName) {
|
|
3331
|
+
return async (...args) => {
|
|
3332
|
+
this.ensureInitialized();
|
|
3333
|
+
const instance = this.get(moduleClass);
|
|
3334
|
+
const method = instance[methodName];
|
|
3335
|
+
if (typeof method !== "function") {
|
|
3336
|
+
throw new Error(
|
|
3337
|
+
`Handler ${String(methodName)} not found in module ${moduleClass.name}`
|
|
3338
|
+
);
|
|
3339
|
+
}
|
|
3340
|
+
return await method.call(instance, ...args);
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* 使用 Hono 的 request 方法调用路由处理器(不启动 HTTP 服务器)
|
|
3345
|
+
* 适用于测试场景,可以完整执行中间件和处理逻辑
|
|
3346
|
+
*
|
|
3347
|
+
* @param input Request 对象、URL 字符串或相对路径
|
|
3348
|
+
* @param init RequestInit 选项(当 input 是字符串时使用)
|
|
3349
|
+
* @returns Response 对象
|
|
3350
|
+
*
|
|
3351
|
+
* @example
|
|
3352
|
+
* ```typescript
|
|
3353
|
+
* // 使用相对路径
|
|
3354
|
+
* const response = await engine.request("/api/users/123");
|
|
3355
|
+
*
|
|
3356
|
+
* // 使用完整 URL
|
|
3357
|
+
* const response = await engine.request("http://localhost/api/users/123");
|
|
3358
|
+
*
|
|
3359
|
+
* // 使用 Request 对象
|
|
3360
|
+
* const request = new Request("http://localhost/api/users/123", {
|
|
3361
|
+
* method: "POST",
|
|
3362
|
+
* headers: { "Content-Type": "application/json" },
|
|
3363
|
+
* body: JSON.stringify({ name: "Alice" }),
|
|
3364
|
+
* });
|
|
3365
|
+
* const response = await engine.request(request);
|
|
3366
|
+
* ```
|
|
3367
|
+
*/
|
|
3368
|
+
async request(input, init) {
|
|
3369
|
+
this.ensureInitialized();
|
|
3370
|
+
let request;
|
|
3371
|
+
if (input instanceof Request) {
|
|
3372
|
+
request = input;
|
|
3373
|
+
} else if (typeof input === "string") {
|
|
3374
|
+
const url = input.startsWith("http://") || input.startsWith("https://") ? input : `http://localhost${input}`;
|
|
3375
|
+
request = new Request(url, init);
|
|
3376
|
+
} else {
|
|
3377
|
+
request = new Request(input.toString(), init);
|
|
3378
|
+
}
|
|
3379
|
+
return await this.hono.request(request);
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
|
|
3383
|
+
// src/core/factory.ts
|
|
3384
|
+
var Factory = class {
|
|
3385
|
+
/**
|
|
3386
|
+
* 创建类型化的引擎工厂
|
|
3387
|
+
*
|
|
3388
|
+
* @param plugins 插件列表(必须显式提供所有需要的插件)
|
|
3389
|
+
* @returns 包含类型化的 Module 装饰器和 Microservice 类的对象
|
|
3390
|
+
*/
|
|
3391
|
+
static create(...plugins) {
|
|
3392
|
+
const moduleMetadataKey = /* @__PURE__ */ Symbol.for(
|
|
3393
|
+
`nebula:moduleMetadata:${Date.now()}:${Math.random()}`
|
|
3394
|
+
);
|
|
3395
|
+
class TypedMicroservice extends Microservice3 {
|
|
3396
|
+
constructor(options) {
|
|
3397
|
+
super(options, moduleMetadataKey);
|
|
3398
|
+
for (const plugin of plugins) {
|
|
3399
|
+
this.registerPlugin(plugin);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
const baseDecorator = createClassDecorator(moduleMetadataKey);
|
|
3404
|
+
const Module = (name, options) => {
|
|
3405
|
+
const classMetadata = {
|
|
3406
|
+
name: name || void 0,
|
|
3407
|
+
options: options || {}
|
|
3408
|
+
};
|
|
3409
|
+
return baseDecorator(classMetadata);
|
|
3410
|
+
};
|
|
3411
|
+
return {
|
|
3412
|
+
Module,
|
|
3413
|
+
Microservice: TypedMicroservice
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
};
|
|
3417
|
+
|
|
3418
|
+
// src/core/testing.ts
|
|
3419
|
+
var DEFAULT_TEST_OPTIONS = {
|
|
3420
|
+
name: "test-service",
|
|
3421
|
+
version: "1.0.0"
|
|
3422
|
+
};
|
|
3423
|
+
function createTestEngine(config) {
|
|
3424
|
+
const factory = Factory.create(...config.plugins);
|
|
3425
|
+
const Microservice4 = factory.Microservice;
|
|
3426
|
+
const engine = new Microservice4({
|
|
3427
|
+
...DEFAULT_TEST_OPTIONS,
|
|
3428
|
+
...config.options
|
|
3429
|
+
});
|
|
3430
|
+
return {
|
|
3431
|
+
...factory,
|
|
3432
|
+
engine
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
function wait(ms) {
|
|
3436
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3437
|
+
}
|
|
3438
|
+
var Testing = {
|
|
3439
|
+
createTestEngine,
|
|
3440
|
+
DEFAULT_TEST_OPTIONS,
|
|
3441
|
+
wait
|
|
3442
|
+
};
|
|
3443
|
+
var NEBULA_ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
3444
|
+
var DEFAULT_ID_LENGTH = 12;
|
|
3445
|
+
var generateNebulaId = customAlphabet(NEBULA_ID_ALPHABET, DEFAULT_ID_LENGTH);
|
|
3446
|
+
function nebulaId(length = DEFAULT_ID_LENGTH) {
|
|
3447
|
+
if (length <= 0) {
|
|
3448
|
+
throw new Error("ID length must be greater than 0");
|
|
3449
|
+
}
|
|
3450
|
+
if (length === DEFAULT_ID_LENGTH) {
|
|
3451
|
+
return generateNebulaId();
|
|
3452
|
+
}
|
|
3453
|
+
const customLengthGenerator = customAlphabet(NEBULA_ID_ALPHABET, length);
|
|
3454
|
+
return customLengthGenerator();
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
export { Action, ActionPlugin, BaseLayout, Cache, CachePlugin, ClientCodePlugin, Config, DEFAULT_TEST_OPTIONS, DuplicateModuleError, DynamicConfigPlugin, EtcdConfigStorage, Factory, GracefulShutdownPlugin, Handler, HtmxLayout, MemoryCacheAdapter, MemoryConfigStorage, MockEtcd3, ModuleConfigValidationError, NEBULA_ID_ALPHABET, Page, PluginNameRequiredError, PluginPriority, RedisCacheAdapter, Route, RoutePlugin, Schedule, ScheduleMode, SchedulePlugin, Scheduler, ServiceInfoCards, ServiceStatusPage, Testing, convertHandlersToModuleInfo, convertHandlersToModuleInfoWithMetadata, generateClientCode, getZodTypeString, logger_default as logger, nebulaId, startCheck };
|