imean-service-engine 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +77 -52
- package/dist/index.d.ts +77 -52
- package/dist/index.js +2078 -1945
- package/dist/index.mjs +2076 -1944
- package/package.json +9 -2
- package/.vscode/settings.json +0 -8
- package/src/core/checker.ts +0 -33
- package/src/core/decorators.test.ts +0 -96
- package/src/core/decorators.ts +0 -68
- package/src/core/engine.test.ts +0 -218
- package/src/core/engine.ts +0 -635
- package/src/core/errors.ts +0 -28
- package/src/core/factory.test.ts +0 -73
- package/src/core/factory.ts +0 -92
- package/src/core/logger.ts +0 -65
- package/src/core/testing.ts +0 -73
- package/src/core/types.ts +0 -191
- package/src/index.ts +0 -49
- package/src/metadata/README.md +0 -422
- package/src/metadata/metadata.test.ts +0 -369
- package/src/metadata/metadata.ts +0 -512
- package/src/plugins/action/action-plugin.test.ts +0 -660
- package/src/plugins/action/decorator.ts +0 -14
- package/src/plugins/action/index.ts +0 -4
- package/src/plugins/action/plugin.ts +0 -349
- package/src/plugins/action/types.ts +0 -49
- package/src/plugins/action/utils.test.ts +0 -196
- package/src/plugins/action/utils.ts +0 -111
- package/src/plugins/cache/adapter.test.ts +0 -689
- package/src/plugins/cache/adapter.ts +0 -324
- package/src/plugins/cache/cache-plugin.test.ts +0 -269
- package/src/plugins/cache/decorator.ts +0 -26
- package/src/plugins/cache/index.ts +0 -20
- package/src/plugins/cache/plugin.ts +0 -299
- package/src/plugins/cache/types.ts +0 -69
- package/src/plugins/client-code/client-code-plugin.test.ts +0 -511
- package/src/plugins/client-code/format.ts +0 -9
- package/src/plugins/client-code/generator.test.ts +0 -52
- package/src/plugins/client-code/generator.ts +0 -263
- package/src/plugins/client-code/index.ts +0 -15
- package/src/plugins/client-code/plugin.ts +0 -158
- package/src/plugins/client-code/types.ts +0 -52
- package/src/plugins/client-code/utils.ts +0 -164
- package/src/plugins/graceful-shutdown/graceful-shutdown-plugin.test.ts +0 -401
- package/src/plugins/graceful-shutdown/index.ts +0 -3
- package/src/plugins/graceful-shutdown/plugin.ts +0 -279
- package/src/plugins/graceful-shutdown/types.ts +0 -17
- package/src/plugins/rate-limit/rate-limit-plugin.example.ts +0 -171
- package/src/plugins/route/components/Layout.tsx +0 -42
- package/src/plugins/route/components/ServiceStatusPage.tsx +0 -141
- package/src/plugins/route/decorator.ts +0 -50
- package/src/plugins/route/index.ts +0 -16
- package/src/plugins/route/plugin.ts +0 -218
- package/src/plugins/route/route-plugin.test.ts +0 -759
- package/src/plugins/route/types.ts +0 -72
- package/src/plugins/schedule/README.md +0 -309
- package/src/plugins/schedule/decorator.ts +0 -25
- package/src/plugins/schedule/index.ts +0 -12
- package/src/plugins/schedule/mock-etcd.ts +0 -145
- package/src/plugins/schedule/plugin.ts +0 -164
- package/src/plugins/schedule/schedule-plugin.test.ts +0 -312
- package/src/plugins/schedule/scheduler.ts +0 -164
- package/src/plugins/schedule/types.ts +0 -94
- package/src/plugins/schedule/utils.test.ts +0 -163
- package/src/plugins/schedule/utils.ts +0 -41
- package/tests/integration/client.test.ts +0 -203
- package/tests/integration/dev-service.ts +0 -301
- package/tests/integration/generated/client.ts +0 -123
- package/tests/integration/start-service.ts +0 -21
- package/tsconfig.json +0 -27
- package/tsup.config.ts +0 -16
- package/vitest.config.ts +0 -19
package/dist/index.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var nodeServer = require('@hono/node-server');
|
|
4
|
-
var hono = require('hono');
|
|
5
|
-
var net = require('net');
|
|
6
3
|
var winston = require('winston');
|
|
7
4
|
var html = require('hono/html');
|
|
8
5
|
var jsxRuntime = require('hono/jsx/jsx-runtime');
|
|
@@ -12,6 +9,9 @@ var crypto = require('crypto');
|
|
|
12
9
|
var fs = require('fs');
|
|
13
10
|
var path = require('path');
|
|
14
11
|
var prettier = require('prettier');
|
|
12
|
+
var nodeServer = require('@hono/node-server');
|
|
13
|
+
var hono = require('hono');
|
|
14
|
+
var net = require('net');
|
|
15
15
|
|
|
16
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
17
|
|
|
@@ -33,10 +33,10 @@ function _interopNamespace(e) {
|
|
|
33
33
|
return Object.freeze(n);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
var net__namespace = /*#__PURE__*/_interopNamespace(net);
|
|
37
36
|
var winston__default = /*#__PURE__*/_interopDefault(winston);
|
|
38
37
|
var ejson__namespace = /*#__PURE__*/_interopNamespace(ejson);
|
|
39
38
|
var prettier__default = /*#__PURE__*/_interopDefault(prettier);
|
|
39
|
+
var net__namespace = /*#__PURE__*/_interopNamespace(net);
|
|
40
40
|
|
|
41
41
|
var __create = Object.create;
|
|
42
42
|
var __defProp = Object.defineProperty;
|
|
@@ -1668,6 +1668,17 @@ var require_src = __commonJS({
|
|
|
1668
1668
|
}
|
|
1669
1669
|
});
|
|
1670
1670
|
|
|
1671
|
+
// src/core/types.ts
|
|
1672
|
+
var PluginPriority = /* @__PURE__ */ ((PluginPriority2) => {
|
|
1673
|
+
PluginPriority2[PluginPriority2["SYSTEM"] = 50] = "SYSTEM";
|
|
1674
|
+
PluginPriority2[PluginPriority2["SECURITY"] = 100] = "SECURITY";
|
|
1675
|
+
PluginPriority2[PluginPriority2["LOGGING"] = 200] = "LOGGING";
|
|
1676
|
+
PluginPriority2[PluginPriority2["BUSINESS"] = 300] = "BUSINESS";
|
|
1677
|
+
PluginPriority2[PluginPriority2["PERFORMANCE"] = 400] = "PERFORMANCE";
|
|
1678
|
+
PluginPriority2[PluginPriority2["ROUTE"] = 1e3] = "ROUTE";
|
|
1679
|
+
return PluginPriority2;
|
|
1680
|
+
})(PluginPriority || {});
|
|
1681
|
+
|
|
1671
1682
|
// src/metadata/metadata.ts
|
|
1672
1683
|
var DECORATED_KEYS_KEY = /* @__PURE__ */ Symbol.for("imean:decoratedKeys");
|
|
1673
1684
|
var keyToClassesMap = /* @__PURE__ */ new Map();
|
|
@@ -1882,2234 +1893,2355 @@ var logger = winston__default.default.createLogger({
|
|
|
1882
1893
|
});
|
|
1883
1894
|
var logger_default = logger;
|
|
1884
1895
|
|
|
1885
|
-
// src/core/
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1896
|
+
// src/core/checker.ts
|
|
1897
|
+
async function startCheck(checkers, pass) {
|
|
1898
|
+
logger_default.info("[ \u9884\u68C0\u5F00\u59CB ]");
|
|
1899
|
+
for (const [index, checker] of checkers.entries()) {
|
|
1900
|
+
const seq = index + 1;
|
|
1901
|
+
logger_default.info(`${seq}. ${checker.name}`);
|
|
1902
|
+
try {
|
|
1903
|
+
if (checker.skip) {
|
|
1904
|
+
logger_default.warn(`${seq}. ${checker.name} [\u8DF3\u8FC7]`);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
await checker.check();
|
|
1908
|
+
logger_default.info(`${seq}. ${checker.name} [\u6210\u529F]`);
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
logger_default.error(`${seq}. ${checker.name} [\u5931\u8D25]`);
|
|
1911
|
+
throw error;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
logger_default.info("[ \u9884\u68C0\u5B8C\u6210 ]");
|
|
1915
|
+
if (pass) await pass();
|
|
1916
|
+
}
|
|
1917
|
+
var DEFAULT_FAVICON = /* @__PURE__ */ jsxRuntime.jsx(
|
|
1918
|
+
"link",
|
|
1919
|
+
{
|
|
1920
|
+
rel: "icon",
|
|
1921
|
+
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>",
|
|
1922
|
+
type: "image/svg+xml"
|
|
1923
|
+
}
|
|
1924
|
+
);
|
|
1925
|
+
var BaseLayout = (props = {
|
|
1926
|
+
title: "Microservice Template"
|
|
1927
|
+
}) => html.html`<!DOCTYPE html>
|
|
1928
|
+
<html>
|
|
1929
|
+
<head>
|
|
1930
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1931
|
+
<title>${props.title}</title>
|
|
1932
|
+
${props.heads}
|
|
1933
|
+
</head>
|
|
1934
|
+
<body>
|
|
1935
|
+
${props.children}
|
|
1936
|
+
</body>
|
|
1937
|
+
</html>`;
|
|
1938
|
+
var HtmxLayout = (props = {
|
|
1939
|
+
title: "Microservice Template"
|
|
1940
|
+
}) => BaseLayout({
|
|
1941
|
+
title: props.title,
|
|
1942
|
+
heads: html.html`
|
|
1943
|
+
<script src="https://unpkg.com/htmx.org@latest"></script>
|
|
1944
|
+
<script src="https://unpkg.com/hyperscript.org@latest"></script>
|
|
1945
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
1946
|
+
${props.favicon || DEFAULT_FAVICON}
|
|
1947
|
+
`,
|
|
1948
|
+
children: props.children
|
|
1949
|
+
});
|
|
1950
|
+
var InfoCard = ({
|
|
1951
|
+
icon,
|
|
1952
|
+
iconColor,
|
|
1953
|
+
bgColor,
|
|
1954
|
+
label,
|
|
1955
|
+
value
|
|
1956
|
+
}) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${bgColor} p-4 rounded-lg`, children: [
|
|
1957
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center mb-2", children: [
|
|
1958
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1959
|
+
"svg",
|
|
1960
|
+
{
|
|
1961
|
+
className: `w-5 h-5 ${iconColor} mr-2`,
|
|
1962
|
+
fill: "none",
|
|
1963
|
+
stroke: "currentColor",
|
|
1964
|
+
viewBox: "0 0 24 24",
|
|
1965
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1966
|
+
"path",
|
|
1967
|
+
{
|
|
1968
|
+
strokeLinecap: "round",
|
|
1969
|
+
strokeLinejoin: "round",
|
|
1970
|
+
strokeWidth: 2,
|
|
1971
|
+
d: icon
|
|
1972
|
+
}
|
|
1973
|
+
)
|
|
1974
|
+
}
|
|
1975
|
+
),
|
|
1976
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-600", children: label })
|
|
1977
|
+
] }),
|
|
1978
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-xl font-semibold text-gray-900`, children: value })
|
|
1979
|
+
] });
|
|
1980
|
+
var getEnvironmentBadgeClass = (env) => {
|
|
1981
|
+
const normalizedEnv = env.toLowerCase();
|
|
1982
|
+
switch (normalizedEnv) {
|
|
1983
|
+
case "production":
|
|
1984
|
+
case "prod":
|
|
1985
|
+
return "bg-red-100 text-red-800";
|
|
1986
|
+
case "staging":
|
|
1987
|
+
case "stg":
|
|
1988
|
+
return "bg-yellow-100 text-yellow-800";
|
|
1989
|
+
case "development":
|
|
1990
|
+
case "dev":
|
|
1991
|
+
default:
|
|
1992
|
+
return "bg-blue-100 text-blue-800";
|
|
1913
1993
|
}
|
|
1994
|
+
};
|
|
1995
|
+
var getStatusBadgeClass = (status) => {
|
|
1996
|
+
switch (status.toLowerCase()) {
|
|
1997
|
+
case "running":
|
|
1998
|
+
return "bg-green-100 text-green-800";
|
|
1999
|
+
case "stopped":
|
|
2000
|
+
return "bg-gray-100 text-gray-800";
|
|
2001
|
+
case "error":
|
|
2002
|
+
return "bg-red-100 text-red-800";
|
|
2003
|
+
default:
|
|
2004
|
+
return "bg-blue-100 text-blue-800";
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
var ServiceInfoCards = ({
|
|
2008
|
+
serviceInfo
|
|
2009
|
+
}) => {
|
|
2010
|
+
const env = serviceInfo.env || typeof process !== "undefined" && process.env?.NODE_ENV || "dev";
|
|
2011
|
+
const status = serviceInfo.status || "running";
|
|
2012
|
+
const infoCards = [
|
|
2013
|
+
{
|
|
2014
|
+
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",
|
|
2015
|
+
iconColor: "text-blue-600",
|
|
2016
|
+
bgColor: "bg-blue-50",
|
|
2017
|
+
label: "\u670D\u52A1\u540D\u79F0",
|
|
2018
|
+
value: serviceInfo.name
|
|
2019
|
+
},
|
|
2020
|
+
{
|
|
2021
|
+
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",
|
|
2022
|
+
iconColor: "text-orange-600",
|
|
2023
|
+
bgColor: "bg-orange-50",
|
|
2024
|
+
label: "\u670D\u52A1\u8DEF\u5F84",
|
|
2025
|
+
value: serviceInfo.prefix || "/"
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
2029
|
+
iconColor: "text-green-600",
|
|
2030
|
+
bgColor: "bg-green-50",
|
|
2031
|
+
label: "\u8FD0\u884C\u73AF\u5883",
|
|
2032
|
+
value: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2033
|
+
"span",
|
|
2034
|
+
{
|
|
2035
|
+
className: `px-2 py-1 rounded-full text-sm ${getEnvironmentBadgeClass(env)}`,
|
|
2036
|
+
children: env
|
|
2037
|
+
}
|
|
2038
|
+
)
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
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",
|
|
2042
|
+
iconColor: "text-purple-600",
|
|
2043
|
+
bgColor: "bg-purple-50",
|
|
2044
|
+
label: "\u7248\u672C\u53F7",
|
|
2045
|
+
value: serviceInfo.version || "unknown"
|
|
2046
|
+
},
|
|
2047
|
+
...serviceInfo.port ? [
|
|
2048
|
+
{
|
|
2049
|
+
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",
|
|
2050
|
+
iconColor: "text-indigo-600",
|
|
2051
|
+
bgColor: "bg-indigo-50",
|
|
2052
|
+
label: "\u7AEF\u53E3",
|
|
2053
|
+
value: serviceInfo.port
|
|
2054
|
+
}
|
|
2055
|
+
] : [],
|
|
2056
|
+
{
|
|
2057
|
+
icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
2058
|
+
iconColor: "text-emerald-600",
|
|
2059
|
+
bgColor: "bg-emerald-50",
|
|
2060
|
+
label: "\u8FD0\u884C\u72B6\u6001",
|
|
2061
|
+
value: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2062
|
+
"span",
|
|
2063
|
+
{
|
|
2064
|
+
className: `px-2 py-1 rounded-full text-sm ${getStatusBadgeClass(status)}`,
|
|
2065
|
+
children: status
|
|
2066
|
+
}
|
|
2067
|
+
)
|
|
2068
|
+
}
|
|
2069
|
+
];
|
|
2070
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white rounded-lg shadow-md p-6 mb-8", children: [
|
|
2071
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "text-2xl font-semibold text-gray-800 mb-6 flex items-center", children: [
|
|
2072
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2073
|
+
"svg",
|
|
2074
|
+
{
|
|
2075
|
+
className: "w-6 h-6 mr-2 text-blue-600",
|
|
2076
|
+
fill: "none",
|
|
2077
|
+
stroke: "currentColor",
|
|
2078
|
+
viewBox: "0 0 24 24",
|
|
2079
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2080
|
+
"path",
|
|
2081
|
+
{
|
|
2082
|
+
strokeLinecap: "round",
|
|
2083
|
+
strokeLinejoin: "round",
|
|
2084
|
+
strokeWidth: 2,
|
|
2085
|
+
d: "M13 10V3L4 14h7v7l9-11h-7z"
|
|
2086
|
+
}
|
|
2087
|
+
)
|
|
2088
|
+
}
|
|
2089
|
+
),
|
|
2090
|
+
"\u670D\u52A1\u57FA\u672C\u4FE1\u606F"
|
|
2091
|
+
] }),
|
|
2092
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6", children: infoCards.map((card, index) => /* @__PURE__ */ jsxRuntime.jsx(InfoCard, { ...card }, index)) })
|
|
2093
|
+
] });
|
|
2094
|
+
};
|
|
2095
|
+
var ServiceStatusPage = ({
|
|
2096
|
+
serviceInfo
|
|
2097
|
+
}) => {
|
|
2098
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-screen bg-gray-50 py-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-6xl mx-auto px-4", children: [
|
|
2099
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-8", children: [
|
|
2100
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-4xl font-bold text-gray-900 mb-2", children: "Service Status" }),
|
|
2101
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600", children: "\u67E5\u770B\u670D\u52A1\u8FD0\u884C\u72B6\u6001\u548C\u57FA\u672C\u4FE1\u606F" })
|
|
2102
|
+
] }),
|
|
2103
|
+
/* @__PURE__ */ jsxRuntime.jsx(ServiceInfoCards, { serviceInfo })
|
|
2104
|
+
] }) });
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
// src/plugins/route/decorator.ts
|
|
2108
|
+
function Route(options) {
|
|
2109
|
+
return Handler({
|
|
2110
|
+
type: "route",
|
|
2111
|
+
options
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
function Page(options) {
|
|
2115
|
+
const pageOptions = {
|
|
2116
|
+
method: options.method || "GET",
|
|
2117
|
+
...options
|
|
2118
|
+
};
|
|
2119
|
+
return Route(pageOptions);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// src/plugins/route/plugin.ts
|
|
2123
|
+
var RoutePlugin = class {
|
|
2124
|
+
name = "route-plugin";
|
|
2125
|
+
priority = 1e3 /* ROUTE */;
|
|
2126
|
+
engine;
|
|
2127
|
+
globalPrefix;
|
|
2128
|
+
globalMiddlewares;
|
|
1914
2129
|
/**
|
|
1915
|
-
*
|
|
2130
|
+
* 构造函数
|
|
2131
|
+
* @param options 插件配置选项
|
|
1916
2132
|
*/
|
|
1917
|
-
|
|
1918
|
-
|
|
2133
|
+
constructor(options) {
|
|
2134
|
+
this.globalPrefix = options?.prefix || "";
|
|
2135
|
+
this.globalMiddlewares = options?.globalMiddlewares || [];
|
|
1919
2136
|
}
|
|
1920
2137
|
/**
|
|
1921
|
-
*
|
|
1922
|
-
* @protected
|
|
2138
|
+
* 声明Module配置Schema(用于类型推导+运行时校验)
|
|
1923
2139
|
*/
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2140
|
+
getModuleOptionsSchema() {
|
|
2141
|
+
return {
|
|
2142
|
+
_type: {},
|
|
2143
|
+
validate: (options) => {
|
|
2144
|
+
if (options.routePrefix !== void 0 && typeof options.routePrefix !== "string") {
|
|
2145
|
+
return "routePrefix must be a string";
|
|
2146
|
+
}
|
|
2147
|
+
if (options.routePrefix && !options.routePrefix.startsWith("/")) {
|
|
2148
|
+
return `routePrefix must start with '/'`;
|
|
2149
|
+
}
|
|
2150
|
+
if (options.routeMiddlewares !== void 0 && !Array.isArray(options.routeMiddlewares)) {
|
|
2151
|
+
return "routeMiddlewares must be an array";
|
|
2152
|
+
}
|
|
2153
|
+
return true;
|
|
1933
2154
|
}
|
|
1934
|
-
|
|
1935
|
-
}
|
|
1936
|
-
this.pluginRegistry.set(plugin.name, plugin);
|
|
1937
|
-
this.plugins.push(plugin);
|
|
2155
|
+
};
|
|
1938
2156
|
}
|
|
1939
2157
|
/**
|
|
1940
|
-
*
|
|
1941
|
-
* 在引擎启动时调用,使用双向访问机制查找所有被装饰的类
|
|
1942
|
-
*
|
|
1943
|
-
* 注意:
|
|
1944
|
-
* - 每个引擎实例使用唯一的 moduleMetadataKey,实现隔离
|
|
1945
|
-
* - 模块类是静态的,可以被多个引擎实例共享
|
|
1946
|
-
* - 每个引擎实例会创建独立的模块实例,互不影响
|
|
2158
|
+
* 引擎初始化钩子:获取Hono实例
|
|
1947
2159
|
*/
|
|
1948
|
-
|
|
1949
|
-
this.
|
|
1950
|
-
|
|
1951
|
-
for (const moduleClass of moduleClasses) {
|
|
1952
|
-
const metadata = getClassMetadata(moduleClass, this.moduleMetadataKey);
|
|
1953
|
-
const moduleName = metadata.name || moduleClass.name;
|
|
1954
|
-
const existingModule = this.modules.find((m) => m.name === moduleName);
|
|
1955
|
-
if (existingModule) {
|
|
1956
|
-
throw new DuplicateModuleError(moduleName);
|
|
1957
|
-
}
|
|
1958
|
-
const moduleMetadata = {
|
|
1959
|
-
name: moduleName,
|
|
1960
|
-
clazz: moduleClass,
|
|
1961
|
-
options: metadata.options || {}
|
|
1962
|
-
};
|
|
1963
|
-
this.modules.push(moduleMetadata);
|
|
1964
|
-
}
|
|
2160
|
+
onInit(engine) {
|
|
2161
|
+
this.engine = engine;
|
|
2162
|
+
logger_default.info("RoutePlugin initialized");
|
|
1965
2163
|
}
|
|
1966
2164
|
/**
|
|
1967
|
-
*
|
|
1968
|
-
* 每个装饰器都是独立的HandlerMetadata条目
|
|
1969
|
-
* 为每个方法创建包装链管理器,提供简单的 wrap API
|
|
2165
|
+
* Handler加载钩子:解析type="route"的Handler元数据,注册HTTP路由
|
|
1970
2166
|
*/
|
|
1971
|
-
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2167
|
+
onHandlerLoad(handlers) {
|
|
2168
|
+
const routeHandlers = handlers.filter(
|
|
2169
|
+
(handler) => handler.type === "route"
|
|
2170
|
+
);
|
|
2171
|
+
logger_default.info(`Found ${routeHandlers.length} route handler(s)`);
|
|
2172
|
+
for (const handler of routeHandlers) {
|
|
2173
|
+
const routeOptions = handler.options;
|
|
2174
|
+
const methodName = handler.methodName;
|
|
2175
|
+
const moduleClass = handler.module;
|
|
2176
|
+
const moduleInstance = this.engine.get(moduleClass);
|
|
2177
|
+
if (!moduleInstance) {
|
|
2178
|
+
logger_default.warn(
|
|
2179
|
+
`Module instance not found for ${moduleClass.name}, skipping route registration`
|
|
2180
|
+
);
|
|
2181
|
+
continue;
|
|
1978
2182
|
}
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2183
|
+
const moduleMetadata = this.engine.getModules().find((m) => m.clazz === moduleClass);
|
|
2184
|
+
const moduleOptions = moduleMetadata?.options || {};
|
|
2185
|
+
const routePrefix = moduleOptions.routePrefix || "";
|
|
2186
|
+
const paths = Array.isArray(routeOptions.path) ? routeOptions.path : [routeOptions.path];
|
|
2187
|
+
const fullPaths = paths.map((p) => this.globalPrefix + routePrefix + p);
|
|
2188
|
+
const routeHandler = async (ctx) => {
|
|
2189
|
+
const method = moduleInstance[methodName];
|
|
2190
|
+
if (typeof method !== "function") {
|
|
2191
|
+
return ctx.json({ error: "Handler method not found" }, 500);
|
|
2192
|
+
}
|
|
2193
|
+
try {
|
|
2194
|
+
const result = await method.call(moduleInstance, ctx);
|
|
2195
|
+
if (result instanceof Response) {
|
|
2196
|
+
return result;
|
|
2197
|
+
}
|
|
2198
|
+
if (typeof result === "string" || typeof result === "object" && result !== null && "type" in result) {
|
|
2199
|
+
return result;
|
|
2200
|
+
}
|
|
2201
|
+
return ctx.json(result);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
logger_default.error(
|
|
2204
|
+
`Error in route handler ${moduleClass.name}.${methodName}`,
|
|
2205
|
+
error
|
|
2206
|
+
);
|
|
2207
|
+
return ctx.json(
|
|
2208
|
+
{
|
|
2209
|
+
error: "Internal server error",
|
|
2210
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2211
|
+
},
|
|
2212
|
+
500
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
const hono = this.engine.getHono();
|
|
2217
|
+
const methods = routeOptions.method ? Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method] : ["GET"];
|
|
2218
|
+
for (const fullPath of fullPaths) {
|
|
2219
|
+
let route = hono;
|
|
2220
|
+
if (this.globalMiddlewares.length > 0) {
|
|
2221
|
+
route = route.use(fullPath, ...this.globalMiddlewares);
|
|
2222
|
+
}
|
|
2223
|
+
if (moduleOptions.routeMiddlewares && moduleOptions.routeMiddlewares.length > 0) {
|
|
2224
|
+
route = route.use(fullPath, ...moduleOptions.routeMiddlewares);
|
|
2225
|
+
}
|
|
2226
|
+
if (routeOptions.middlewares && routeOptions.middlewares.length > 0) {
|
|
2227
|
+
route = route.use(fullPath, ...routeOptions.middlewares);
|
|
2228
|
+
}
|
|
2229
|
+
for (const method of methods) {
|
|
2230
|
+
const methodLower = method.toLowerCase();
|
|
2231
|
+
if (methodLower === "get" || methodLower === "post" || methodLower === "put" || methodLower === "delete" || methodLower === "patch" || methodLower === "head" || methodLower === "options") {
|
|
2232
|
+
route[methodLower](fullPath, routeHandler);
|
|
2233
|
+
const description = routeOptions.description ? ` (${routeOptions.description})` : "";
|
|
2234
|
+
logger_default.info(
|
|
2235
|
+
`Registered route: ${method.toUpperCase()} ${fullPath} -> ${moduleClass.name}.${methodName}${description}`
|
|
2236
|
+
);
|
|
2237
|
+
} else {
|
|
2238
|
+
logger_default.warn(
|
|
2239
|
+
`Unsupported HTTP method: ${method}, skipping route ${fullPath}`
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2000
2242
|
}
|
|
2001
2243
|
}
|
|
2002
2244
|
}
|
|
2003
|
-
this.__wrapperChains__ = wrapperChains;
|
|
2004
|
-
this.__originalMethods__ = originalMethods;
|
|
2005
|
-
return handlers;
|
|
2006
2245
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
const wrapperChains = this.__wrapperChains__;
|
|
2013
|
-
const originalMethods = this.__originalMethods__;
|
|
2014
|
-
if (!wrapperChains || !originalMethods) return;
|
|
2015
|
-
for (const [methodKey, chain] of wrapperChains.entries()) {
|
|
2016
|
-
if (chain.length === 0) continue;
|
|
2017
|
-
const [moduleName, methodName] = methodKey.split(".");
|
|
2018
|
-
const module = this.modules.find((m) => m.clazz.name === moduleName);
|
|
2019
|
-
if (!module) continue;
|
|
2020
|
-
const originalMethod = originalMethods.get(methodKey);
|
|
2021
|
-
const prototype = module.clazz.prototype;
|
|
2022
|
-
let wrappedMethod = originalMethod;
|
|
2023
|
-
for (let i = chain.length - 1; i >= 0; i--) {
|
|
2024
|
-
const wrapper = chain[i];
|
|
2025
|
-
const next = wrappedMethod;
|
|
2026
|
-
wrappedMethod = async function(...args) {
|
|
2027
|
-
return wrapper(() => next.apply(this, args), this, ...args);
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
prototype[methodName] = wrappedMethod;
|
|
2031
|
-
}
|
|
2246
|
+
};
|
|
2247
|
+
function buildParamsSchema(schemas) {
|
|
2248
|
+
const shape = {};
|
|
2249
|
+
for (let i = 0; i < schemas.length; i++) {
|
|
2250
|
+
shape[String(i)] = schemas[i];
|
|
2032
2251
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
const server = net__namespace.createServer();
|
|
2039
|
-
server.unref();
|
|
2040
|
-
server.listen(port, hostname, () => {
|
|
2041
|
-
server.once("close", () => resolve(true));
|
|
2042
|
-
server.close();
|
|
2043
|
-
});
|
|
2044
|
-
server.on("error", () => resolve(false));
|
|
2045
|
-
});
|
|
2252
|
+
return zod.z.object(shape);
|
|
2253
|
+
}
|
|
2254
|
+
function parseAndValidateParams(body, schemas) {
|
|
2255
|
+
if (!body || typeof body !== "object") {
|
|
2256
|
+
body = {};
|
|
2046
2257
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
*/
|
|
2050
|
-
getRandomPort(hostname) {
|
|
2051
|
-
return new Promise((resolve, reject) => {
|
|
2052
|
-
const server = net__namespace.createServer();
|
|
2053
|
-
server.unref();
|
|
2054
|
-
server.on("error", reject);
|
|
2055
|
-
server.listen(0, hostname, () => {
|
|
2056
|
-
const port = server.address().port;
|
|
2057
|
-
server.close((err) => {
|
|
2058
|
-
if (err) {
|
|
2059
|
-
return reject(err);
|
|
2060
|
-
}
|
|
2061
|
-
resolve(port);
|
|
2062
|
-
});
|
|
2063
|
-
});
|
|
2064
|
-
});
|
|
2258
|
+
if (schemas.length === 0) {
|
|
2259
|
+
return { success: true, data: [] };
|
|
2065
2260
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
this.initializeModulesAndPlugins();
|
|
2078
|
-
this.processHandlers();
|
|
2079
|
-
await this.executePluginStartHooks();
|
|
2080
|
-
this.startHttpServer(port, hostname);
|
|
2081
|
-
this.started = true;
|
|
2082
|
-
logger_default.info(
|
|
2083
|
-
`${this.options.name} v${this.options.version} started successfully on port ${this.actualPort}`
|
|
2084
|
-
);
|
|
2085
|
-
return this.actualPort;
|
|
2086
|
-
} catch (error) {
|
|
2087
|
-
logger_default.error("Failed to start engine", error);
|
|
2088
|
-
throw error;
|
|
2089
|
-
}
|
|
2261
|
+
const paramsSchema = buildParamsSchema(schemas);
|
|
2262
|
+
const validation = paramsSchema.safeParse(body);
|
|
2263
|
+
if (!validation.success) {
|
|
2264
|
+
return {
|
|
2265
|
+
success: false,
|
|
2266
|
+
error: validation.error
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
const validatedData = [];
|
|
2270
|
+
for (let i = 0; i < schemas.length; i++) {
|
|
2271
|
+
validatedData[i] = validation.data[String(i)];
|
|
2090
2272
|
}
|
|
2273
|
+
return {
|
|
2274
|
+
success: true,
|
|
2275
|
+
data: validatedData
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function buildActionPath(enginePrefix, moduleName, handlerName) {
|
|
2279
|
+
const normalizedPrefix = enginePrefix.replace(/\/+$/, "");
|
|
2280
|
+
return `/${normalizedPrefix}/${moduleName}/${handlerName}`.replace(/\/+/g, "/").replace(/^\/\//, "/");
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/plugins/action/plugin.ts
|
|
2284
|
+
function isAsyncIterable(obj) {
|
|
2285
|
+
return obj != null && typeof obj[Symbol.asyncIterator] === "function";
|
|
2286
|
+
}
|
|
2287
|
+
var ActionPlugin = class {
|
|
2288
|
+
name = "action-plugin";
|
|
2289
|
+
priority = 1e3 /* ROUTE */;
|
|
2290
|
+
// 路由插件优先级最低,必须最后执行
|
|
2291
|
+
engine;
|
|
2091
2292
|
/**
|
|
2092
|
-
*
|
|
2293
|
+
* 声明Module配置Schema(用于类型推导+运行时校验)
|
|
2093
2294
|
*/
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2295
|
+
getModuleOptionsSchema() {
|
|
2296
|
+
return {
|
|
2297
|
+
_type: {},
|
|
2298
|
+
validate: (options) => {
|
|
2299
|
+
if (options.actionMiddlewares !== void 0 && !Array.isArray(options.actionMiddlewares)) {
|
|
2300
|
+
return "actionMiddlewares must be an array";
|
|
2301
|
+
}
|
|
2302
|
+
return true;
|
|
2101
2303
|
}
|
|
2102
|
-
|
|
2103
|
-
`Port ${requestedPort} is in use, finding a random available port...`
|
|
2104
|
-
);
|
|
2105
|
-
}
|
|
2106
|
-
this.actualPort = await this.getRandomPort(hostname);
|
|
2107
|
-
return { port: this.actualPort, hostname };
|
|
2304
|
+
};
|
|
2108
2305
|
}
|
|
2109
2306
|
/**
|
|
2110
|
-
*
|
|
2307
|
+
* 引擎初始化钩子:获取Hono实例
|
|
2111
2308
|
*/
|
|
2112
|
-
|
|
2113
|
-
this.
|
|
2114
|
-
|
|
2115
|
-
plugin.onInit?.(this);
|
|
2116
|
-
});
|
|
2117
|
-
this.executePluginHook("onModuleLoad", (plugin) => {
|
|
2118
|
-
plugin.onModuleLoad?.(
|
|
2119
|
-
this.modules
|
|
2120
|
-
);
|
|
2121
|
-
});
|
|
2309
|
+
onInit(engine) {
|
|
2310
|
+
this.engine = engine;
|
|
2311
|
+
logger_default.info("ActionPlugin initialized");
|
|
2122
2312
|
}
|
|
2123
2313
|
/**
|
|
2124
|
-
*
|
|
2314
|
+
* Handler加载钩子:解析type="action"的Handler元数据,注册HTTP路由
|
|
2125
2315
|
*/
|
|
2126
|
-
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
const { wrapperPlugins, routePlugins } = this.separateWrapperAndRoutePlugins(sortedPlugins);
|
|
2130
|
-
this.executePluginHook(
|
|
2131
|
-
"onHandlerLoad",
|
|
2132
|
-
(plugin) => {
|
|
2133
|
-
plugin.onHandlerLoad?.(handlers);
|
|
2134
|
-
},
|
|
2135
|
-
wrapperPlugins
|
|
2136
|
-
);
|
|
2137
|
-
this.applyWrapperChains();
|
|
2138
|
-
this.executePluginHook(
|
|
2139
|
-
"onHandlerLoad",
|
|
2140
|
-
(plugin) => {
|
|
2141
|
-
plugin.onHandlerLoad?.(handlers);
|
|
2142
|
-
},
|
|
2143
|
-
routePlugins
|
|
2316
|
+
onHandlerLoad(handlers) {
|
|
2317
|
+
const actionHandlers = handlers.filter(
|
|
2318
|
+
(handler) => handler.type === "action"
|
|
2144
2319
|
);
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
try {
|
|
2155
|
-
await plugin.onAfterStart?.(this);
|
|
2156
|
-
} catch (error) {
|
|
2157
|
-
throw new Error(
|
|
2158
|
-
`Plugin ${plugin.name} failed in onAfterStart: ${error instanceof Error ? error.message : String(error)}`
|
|
2320
|
+
logger_default.info(`Found ${actionHandlers.length} action handler(s)`);
|
|
2321
|
+
for (const handler of actionHandlers) {
|
|
2322
|
+
const actionOptions = handler.options;
|
|
2323
|
+
const methodName = handler.methodName;
|
|
2324
|
+
const moduleClass = handler.module;
|
|
2325
|
+
const moduleInstance = this.engine.get(moduleClass);
|
|
2326
|
+
if (!moduleInstance) {
|
|
2327
|
+
logger_default.warn(
|
|
2328
|
+
`Module instance not found for ${moduleClass.name}, skipping action registration`
|
|
2159
2329
|
);
|
|
2330
|
+
continue;
|
|
2160
2331
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
}
|
|
2164
|
-
/**
|
|
2165
|
-
* 注册版本路由(/prefix/version)
|
|
2166
|
-
* 用于健康检查和探针
|
|
2167
|
-
*/
|
|
2168
|
-
registerVersionRoute() {
|
|
2169
|
-
const prefix = this.options.prefix || "";
|
|
2170
|
-
const versionPath = prefix ? `${prefix}` : "/";
|
|
2171
|
-
const existingRoutes = this.hono.routes;
|
|
2172
|
-
const routeExists = existingRoutes.some(
|
|
2173
|
-
(route) => route.path === versionPath && route.method === "GET"
|
|
2174
|
-
);
|
|
2175
|
-
if (routeExists) {
|
|
2176
|
-
logger_default.info(
|
|
2177
|
-
`Version route ${versionPath} already exists, skipping registration`
|
|
2178
|
-
);
|
|
2179
|
-
return;
|
|
2180
|
-
}
|
|
2181
|
-
try {
|
|
2182
|
-
this.hono.get(versionPath, async (ctx) => {
|
|
2183
|
-
return ctx.json({
|
|
2184
|
-
name: this.options.name,
|
|
2185
|
-
version: this.options.version,
|
|
2186
|
-
status: "running"
|
|
2187
|
-
});
|
|
2188
|
-
});
|
|
2189
|
-
logger_default.info(`Registered version route: GET ${versionPath}`);
|
|
2190
|
-
} catch (error) {
|
|
2191
|
-
if (error instanceof Error && error.message.includes("matcher is already built")) {
|
|
2332
|
+
const moduleMetadata = this.engine.getModules().find((m) => m.clazz === moduleClass);
|
|
2333
|
+
if (!moduleMetadata) {
|
|
2192
2334
|
logger_default.warn(
|
|
2193
|
-
`
|
|
2335
|
+
`Module metadata not found for ${moduleClass.name}, skipping action registration`
|
|
2194
2336
|
);
|
|
2195
|
-
|
|
2196
|
-
throw error;
|
|
2337
|
+
continue;
|
|
2197
2338
|
}
|
|
2339
|
+
const moduleOptions = moduleMetadata.options || {};
|
|
2340
|
+
const enginePrefix = this.engine.options.prefix || "";
|
|
2341
|
+
const actionPath = buildActionPath(
|
|
2342
|
+
enginePrefix,
|
|
2343
|
+
moduleMetadata.name,
|
|
2344
|
+
methodName
|
|
2345
|
+
);
|
|
2346
|
+
const paramSchemas = actionOptions.params || [];
|
|
2347
|
+
const returnSchema = actionOptions.returns;
|
|
2348
|
+
const actionHandler = async (ctx) => {
|
|
2349
|
+
const method = moduleInstance[methodName];
|
|
2350
|
+
if (typeof method !== "function") {
|
|
2351
|
+
const errorResponse = ejson__namespace.stringify({
|
|
2352
|
+
success: false,
|
|
2353
|
+
error: "Action method not found"
|
|
2354
|
+
});
|
|
2355
|
+
return ctx.text(errorResponse, 500, {
|
|
2356
|
+
"Content-Type": "application/json"
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
try {
|
|
2360
|
+
let body = {};
|
|
2361
|
+
if (ctx.req.method === "GET") {
|
|
2362
|
+
const url = new URL(ctx.req.url);
|
|
2363
|
+
url.searchParams.forEach((value, key) => {
|
|
2364
|
+
body[key] = value;
|
|
2365
|
+
});
|
|
2366
|
+
} else {
|
|
2367
|
+
try {
|
|
2368
|
+
const rawBody = await ctx.req.text();
|
|
2369
|
+
if (rawBody) {
|
|
2370
|
+
body = ejson__namespace.parse(rawBody);
|
|
2371
|
+
}
|
|
2372
|
+
} catch (parseError) {
|
|
2373
|
+
try {
|
|
2374
|
+
body = await ctx.req.json().catch(() => ({}));
|
|
2375
|
+
} catch {
|
|
2376
|
+
body = {};
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
const validation = parseAndValidateParams(body, paramSchemas);
|
|
2381
|
+
if (!validation.success) {
|
|
2382
|
+
const errors = validation.error.issues || [];
|
|
2383
|
+
const errorMessage = `Validation failed: ${errors.map((e) => {
|
|
2384
|
+
const path = e.path || [];
|
|
2385
|
+
const pathStr = path.length > 0 ? path.map((p, i) => {
|
|
2386
|
+
if (i === 0 && typeof p === "string" && /^\d+$/.test(p)) {
|
|
2387
|
+
return `\u53C2\u6570[${p}]`;
|
|
2388
|
+
}
|
|
2389
|
+
return String(p);
|
|
2390
|
+
}).join(".") : "unknown";
|
|
2391
|
+
return `${pathStr}: ${e.message}`;
|
|
2392
|
+
}).join(", ")}`;
|
|
2393
|
+
const errorResponse = ejson__namespace.stringify({
|
|
2394
|
+
success: false,
|
|
2395
|
+
error: errorMessage
|
|
2396
|
+
});
|
|
2397
|
+
return ctx.text(errorResponse, 400, {
|
|
2398
|
+
"Content-Type": "application/json"
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
const methodLength = method.length;
|
|
2402
|
+
const paramsLength = paramSchemas.length;
|
|
2403
|
+
const args = [...validation.data];
|
|
2404
|
+
if (methodLength > paramsLength) {
|
|
2405
|
+
args.unshift(ctx);
|
|
2406
|
+
}
|
|
2407
|
+
const result = await method.apply(moduleInstance, args);
|
|
2408
|
+
if (actionOptions.stream) {
|
|
2409
|
+
if (!isAsyncIterable(result)) {
|
|
2410
|
+
const errorResponse = ejson__namespace.stringify({
|
|
2411
|
+
success: false,
|
|
2412
|
+
error: "Stream action must return AsyncIterable"
|
|
2413
|
+
});
|
|
2414
|
+
return ctx.text(errorResponse, 500, {
|
|
2415
|
+
"Content-Type": "application/json"
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
const encoder = new TextEncoder();
|
|
2419
|
+
const iterator = result[Symbol.asyncIterator]();
|
|
2420
|
+
const stream = new ReadableStream({
|
|
2421
|
+
async start(controller) {
|
|
2422
|
+
try {
|
|
2423
|
+
while (true) {
|
|
2424
|
+
const { value, done } = await iterator.next();
|
|
2425
|
+
if (done) {
|
|
2426
|
+
controller.enqueue(
|
|
2427
|
+
encoder.encode(ejson__namespace.stringify({ done: true }))
|
|
2428
|
+
);
|
|
2429
|
+
controller.close();
|
|
2430
|
+
break;
|
|
2431
|
+
}
|
|
2432
|
+
controller.enqueue(
|
|
2433
|
+
encoder.encode(ejson__namespace.stringify({ value, done: false }))
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
controller.enqueue(
|
|
2438
|
+
encoder.encode(
|
|
2439
|
+
ejson__namespace.stringify({
|
|
2440
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2441
|
+
done: true
|
|
2442
|
+
})
|
|
2443
|
+
)
|
|
2444
|
+
);
|
|
2445
|
+
controller.close();
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
return new Response(stream, {
|
|
2450
|
+
headers: {
|
|
2451
|
+
"Content-Type": "text/event-stream",
|
|
2452
|
+
"Cache-Control": "no-cache",
|
|
2453
|
+
Connection: "keep-alive"
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
if (returnSchema) {
|
|
2458
|
+
const returnValidation = returnSchema.safeParse(result);
|
|
2459
|
+
if (!returnValidation.success) {
|
|
2460
|
+
logger_default.error(
|
|
2461
|
+
`Return value validation failed for ${moduleClass.name}.${methodName}`,
|
|
2462
|
+
returnValidation.error
|
|
2463
|
+
);
|
|
2464
|
+
const returnErrors = returnValidation.error.issues || [];
|
|
2465
|
+
const errorMessage = `Return value validation failed: ${returnErrors.map((e) => {
|
|
2466
|
+
const path = e.path || [];
|
|
2467
|
+
const pathStr = path.length > 0 ? path.map((p) => String(p)).join(".") : "root";
|
|
2468
|
+
return `${pathStr}: ${e.message}`;
|
|
2469
|
+
}).join(", ")}`;
|
|
2470
|
+
const errorResponse = ejson__namespace.stringify({
|
|
2471
|
+
success: false,
|
|
2472
|
+
error: errorMessage
|
|
2473
|
+
});
|
|
2474
|
+
return ctx.text(errorResponse, 400, {
|
|
2475
|
+
"Content-Type": "application/json"
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
const successResponse2 = ejson__namespace.stringify({
|
|
2479
|
+
success: true,
|
|
2480
|
+
data: returnValidation.data
|
|
2481
|
+
});
|
|
2482
|
+
return ctx.text(successResponse2, 200, {
|
|
2483
|
+
"Content-Type": "application/json"
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
const successResponse = ejson__namespace.stringify({
|
|
2487
|
+
success: true,
|
|
2488
|
+
data: result
|
|
2489
|
+
});
|
|
2490
|
+
return ctx.text(successResponse, 200, {
|
|
2491
|
+
"Content-Type": "application/json"
|
|
2492
|
+
});
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
logger_default.error(
|
|
2495
|
+
`Error in action handler ${moduleClass.name}.${methodName}`,
|
|
2496
|
+
error
|
|
2497
|
+
);
|
|
2498
|
+
const errorResponse = ejson__namespace.stringify({
|
|
2499
|
+
success: false,
|
|
2500
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2501
|
+
});
|
|
2502
|
+
return ctx.text(errorResponse, 500, {
|
|
2503
|
+
"Content-Type": "application/json"
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
const hono = this.engine.getHono();
|
|
2508
|
+
let route = hono;
|
|
2509
|
+
if (moduleOptions.actionMiddlewares && moduleOptions.actionMiddlewares.length > 0) {
|
|
2510
|
+
route = route.use(actionPath, ...moduleOptions.actionMiddlewares);
|
|
2511
|
+
}
|
|
2512
|
+
if (actionOptions.middlewares && actionOptions.middlewares.length > 0) {
|
|
2513
|
+
route = route.use(actionPath, ...actionOptions.middlewares);
|
|
2514
|
+
}
|
|
2515
|
+
route.get(actionPath, actionHandler);
|
|
2516
|
+
route.post(actionPath, actionHandler);
|
|
2517
|
+
logger_default.info(
|
|
2518
|
+
`[\u6CE8\u518C\u52A8\u4F5C] ${moduleClass.name}.${methodName} ${actionOptions.description}`
|
|
2519
|
+
);
|
|
2198
2520
|
}
|
|
2199
2521
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2522
|
+
};
|
|
2523
|
+
|
|
2524
|
+
// src/plugins/action/decorator.ts
|
|
2525
|
+
function Action(options) {
|
|
2526
|
+
return Handler({
|
|
2527
|
+
type: "action",
|
|
2528
|
+
options
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/plugins/cache/adapter.ts
|
|
2533
|
+
var MemoryCacheAdapter = class {
|
|
2534
|
+
cache = /* @__PURE__ */ new Map();
|
|
2535
|
+
async get(key) {
|
|
2536
|
+
const item = this.cache.get(key);
|
|
2537
|
+
if (!item) {
|
|
2538
|
+
return null;
|
|
2539
|
+
}
|
|
2540
|
+
if (item.expiresAt <= Date.now()) {
|
|
2541
|
+
this.cache.delete(key);
|
|
2542
|
+
return null;
|
|
2543
|
+
}
|
|
2544
|
+
return item;
|
|
2210
2545
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
return
|
|
2216
|
-
const priorityA = this.getPluginPriority(a);
|
|
2217
|
-
const priorityB = this.getPluginPriority(b);
|
|
2218
|
-
if (priorityA !== priorityB) {
|
|
2219
|
-
return priorityA - priorityB;
|
|
2220
|
-
}
|
|
2221
|
-
const indexA = this.plugins.indexOf(a);
|
|
2222
|
-
const indexB = this.plugins.indexOf(b);
|
|
2223
|
-
return indexA - indexB;
|
|
2224
|
-
});
|
|
2546
|
+
async set(key, item) {
|
|
2547
|
+
this.cache.set(key, item);
|
|
2548
|
+
}
|
|
2549
|
+
async delete(key) {
|
|
2550
|
+
return this.cache.delete(key);
|
|
2225
2551
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
*/
|
|
2229
|
-
getPluginPriority(plugin) {
|
|
2230
|
-
return plugin.priority ?? 300 /* BUSINESS */;
|
|
2552
|
+
async clear() {
|
|
2553
|
+
this.cache.clear();
|
|
2231
2554
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
separateWrapperAndRoutePlugins(sortedPlugins) {
|
|
2236
|
-
const wrapperPlugins = [];
|
|
2237
|
-
const routePlugins = [];
|
|
2238
|
-
for (const plugin of sortedPlugins) {
|
|
2239
|
-
const priority = this.getPluginPriority(plugin);
|
|
2240
|
-
if (priority === 1e3 /* ROUTE */) {
|
|
2241
|
-
routePlugins.push(plugin);
|
|
2242
|
-
} else {
|
|
2243
|
-
wrapperPlugins.push(plugin);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
return { wrapperPlugins, routePlugins };
|
|
2555
|
+
async keys() {
|
|
2556
|
+
const now = Date.now();
|
|
2557
|
+
return Array.from(this.cache.entries()).filter(([_, item]) => item.expiresAt > now).map(([key]) => key);
|
|
2247
2558
|
}
|
|
2248
2559
|
/**
|
|
2249
|
-
*
|
|
2560
|
+
* 清理所有过期项(高效批量清理)
|
|
2561
|
+
* 这个方法比通过 keys() + get() + delete() 更高效
|
|
2250
2562
|
*/
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
);
|
|
2563
|
+
async cleanupExpired() {
|
|
2564
|
+
const now = Date.now();
|
|
2565
|
+
let cleaned = 0;
|
|
2566
|
+
for (const [key, item] of this.cache.entries()) {
|
|
2567
|
+
if (item.expiresAt <= now) {
|
|
2568
|
+
this.cache.delete(key);
|
|
2569
|
+
cleaned++;
|
|
2259
2570
|
}
|
|
2260
2571
|
}
|
|
2572
|
+
return cleaned;
|
|
2261
2573
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2574
|
+
async getStats() {
|
|
2575
|
+
const now = Date.now();
|
|
2576
|
+
const validEntries = Array.from(this.cache.entries()).filter(([_, item]) => item.expiresAt > now).map(([key, item]) => ({
|
|
2577
|
+
key,
|
|
2578
|
+
expiresAt: item.expiresAt,
|
|
2579
|
+
createdAt: item.createdAt
|
|
2580
|
+
}));
|
|
2581
|
+
return {
|
|
2582
|
+
size: validEntries.length,
|
|
2583
|
+
entries: validEntries
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
var RedisCacheAdapter = class {
|
|
2588
|
+
client;
|
|
2589
|
+
keyPrefix;
|
|
2590
|
+
constructor(options) {
|
|
2591
|
+
this.client = options.client;
|
|
2592
|
+
this.keyPrefix = options.keyPrefix || "cache:";
|
|
2593
|
+
}
|
|
2594
|
+
getKey(key) {
|
|
2595
|
+
return `${this.keyPrefix}${key}`;
|
|
2596
|
+
}
|
|
2597
|
+
async get(key) {
|
|
2598
|
+
const redisKey = this.getKey(key);
|
|
2599
|
+
const data = await this.client.get(redisKey);
|
|
2600
|
+
if (!data) {
|
|
2601
|
+
return null;
|
|
2268
2602
|
}
|
|
2269
2603
|
try {
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
this.server.close((err) => {
|
|
2275
|
-
if (err) {
|
|
2276
|
-
reject(err);
|
|
2277
|
-
} else {
|
|
2278
|
-
resolve();
|
|
2279
|
-
}
|
|
2280
|
-
});
|
|
2281
|
-
});
|
|
2282
|
-
} else {
|
|
2283
|
-
this.server.close();
|
|
2284
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
this.server = null;
|
|
2288
|
-
logger_default.info("HTTP server stopped");
|
|
2289
|
-
}
|
|
2290
|
-
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
2291
|
-
const plugin = this.plugins[i];
|
|
2292
|
-
try {
|
|
2293
|
-
await plugin.onDestroy?.();
|
|
2294
|
-
} catch (error) {
|
|
2295
|
-
logger_default.error(`Plugin ${plugin.name} failed in onDestroy`, error);
|
|
2296
|
-
}
|
|
2604
|
+
const item = JSON.parse(data);
|
|
2605
|
+
if (item.expiresAt <= Date.now()) {
|
|
2606
|
+
await this.delete(key);
|
|
2607
|
+
return null;
|
|
2297
2608
|
}
|
|
2298
|
-
|
|
2299
|
-
this.moduleInstances.clear();
|
|
2300
|
-
this.started = false;
|
|
2301
|
-
this.actualPort = null;
|
|
2302
|
-
logger_default.info(`${this.options.name} stopped`);
|
|
2609
|
+
return item;
|
|
2303
2610
|
} catch (error) {
|
|
2304
|
-
|
|
2305
|
-
|
|
2611
|
+
await this.delete(key);
|
|
2612
|
+
return null;
|
|
2306
2613
|
}
|
|
2307
2614
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
get(moduleClass) {
|
|
2314
|
-
if (!this.moduleInstances.has(moduleClass)) {
|
|
2315
|
-
this.moduleInstances.set(moduleClass, new moduleClass());
|
|
2316
|
-
}
|
|
2317
|
-
return this.moduleInstances.get(moduleClass);
|
|
2615
|
+
async set(key, item) {
|
|
2616
|
+
const redisKey = this.getKey(key);
|
|
2617
|
+
const data = JSON.stringify(item);
|
|
2618
|
+
const ttl = Math.max(0, Math.floor((item.expiresAt - Date.now()) / 1e3));
|
|
2619
|
+
await this.client.set(redisKey, data, "EX", ttl);
|
|
2318
2620
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2621
|
+
async delete(key) {
|
|
2622
|
+
const redisKey = this.getKey(key);
|
|
2623
|
+
const result = await this.client.del(redisKey);
|
|
2624
|
+
return result > 0;
|
|
2625
|
+
}
|
|
2626
|
+
async clear() {
|
|
2627
|
+
const pattern = `${this.keyPrefix}*`;
|
|
2628
|
+
const keys = await this.client.keys(pattern);
|
|
2629
|
+
if (keys.length > 0) {
|
|
2630
|
+
await Promise.all(keys.map((key) => this.client.del(key)));
|
|
2325
2631
|
}
|
|
2326
|
-
|
|
2632
|
+
}
|
|
2633
|
+
async keys() {
|
|
2634
|
+
const pattern = `${this.keyPrefix}*`;
|
|
2635
|
+
const redisKeys = await this.client.keys(pattern);
|
|
2636
|
+
return redisKeys.map((key) => key.replace(this.keyPrefix, ""));
|
|
2327
2637
|
}
|
|
2328
2638
|
/**
|
|
2329
|
-
*
|
|
2639
|
+
* 清理所有过期项
|
|
2640
|
+
*
|
|
2641
|
+
* 注意:Redis 本身支持自动过期(通过 SET key value EX seconds),
|
|
2642
|
+
* 过期键会被 Redis 自动删除。但在某些情况下(如测试环境、某些 Redis 客户端),
|
|
2643
|
+
* 可能需要手动清理。此方法会检查并清理:
|
|
2644
|
+
* 1. 已过期但尚未被 Redis 删除的键(双重保险)
|
|
2645
|
+
* 2. JSON 解析失败的数据
|
|
2330
2646
|
*/
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
PluginPriority2[PluginPriority2["SYSTEM"] = 50] = "SYSTEM";
|
|
2339
|
-
PluginPriority2[PluginPriority2["SECURITY"] = 100] = "SECURITY";
|
|
2340
|
-
PluginPriority2[PluginPriority2["LOGGING"] = 200] = "LOGGING";
|
|
2341
|
-
PluginPriority2[PluginPriority2["BUSINESS"] = 300] = "BUSINESS";
|
|
2342
|
-
PluginPriority2[PluginPriority2["PERFORMANCE"] = 400] = "PERFORMANCE";
|
|
2343
|
-
PluginPriority2[PluginPriority2["ROUTE"] = 1e3] = "ROUTE";
|
|
2344
|
-
return PluginPriority2;
|
|
2345
|
-
})(PluginPriority || {});
|
|
2346
|
-
|
|
2347
|
-
// src/core/checker.ts
|
|
2348
|
-
async function startCheck(checkers, pass) {
|
|
2349
|
-
logger_default.info("[ \u9884\u68C0\u5F00\u59CB ]");
|
|
2350
|
-
for (const [index, checker] of checkers.entries()) {
|
|
2351
|
-
const seq = index + 1;
|
|
2352
|
-
logger_default.info(`${seq}. ${checker.name}`);
|
|
2353
|
-
try {
|
|
2354
|
-
if (checker.skip) {
|
|
2355
|
-
logger_default.warn(`${seq}. ${checker.name} [\u8DF3\u8FC7]`);
|
|
2647
|
+
async cleanupExpired() {
|
|
2648
|
+
const pattern = `${this.keyPrefix}*`;
|
|
2649
|
+
const redisKeys = await this.client.keys(pattern);
|
|
2650
|
+
let cleaned = 0;
|
|
2651
|
+
for (const redisKey of redisKeys) {
|
|
2652
|
+
const data = await this.client.get(redisKey);
|
|
2653
|
+
if (!data) {
|
|
2356
2654
|
continue;
|
|
2357
2655
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2656
|
+
try {
|
|
2657
|
+
const item = JSON.parse(data);
|
|
2658
|
+
if (item.expiresAt <= Date.now()) {
|
|
2659
|
+
await this.client.del(redisKey);
|
|
2660
|
+
cleaned++;
|
|
2661
|
+
}
|
|
2662
|
+
} catch {
|
|
2663
|
+
await this.client.del(redisKey);
|
|
2664
|
+
cleaned++;
|
|
2665
|
+
}
|
|
2363
2666
|
}
|
|
2667
|
+
return cleaned;
|
|
2364
2668
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2669
|
+
async getStats() {
|
|
2670
|
+
const allKeys = await this.keys();
|
|
2671
|
+
const entries = [];
|
|
2672
|
+
for (const key of allKeys) {
|
|
2673
|
+
const item = await this.get(key);
|
|
2674
|
+
if (item) {
|
|
2675
|
+
entries.push({
|
|
2676
|
+
key,
|
|
2677
|
+
expiresAt: item.expiresAt,
|
|
2678
|
+
createdAt: item.createdAt
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return {
|
|
2683
|
+
size: entries.length,
|
|
2684
|
+
entries
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2368
2688
|
|
|
2369
|
-
// src/plugins/
|
|
2370
|
-
var
|
|
2371
|
-
name = "
|
|
2372
|
-
priority =
|
|
2373
|
-
|
|
2374
|
-
|
|
2689
|
+
// src/plugins/cache/plugin.ts
|
|
2690
|
+
var CachePlugin = class {
|
|
2691
|
+
name = "cache-plugin";
|
|
2692
|
+
priority = 400 /* PERFORMANCE */;
|
|
2693
|
+
// 性能优化插件,优先级较低
|
|
2694
|
+
// 缓存适配器
|
|
2695
|
+
adapter;
|
|
2696
|
+
// 清理定时器
|
|
2697
|
+
cleanupTimer = null;
|
|
2698
|
+
// 引擎引用
|
|
2699
|
+
engine = null;
|
|
2700
|
+
// 模块配置
|
|
2701
|
+
defaultTtl = 6e4;
|
|
2702
|
+
// 默认1分钟
|
|
2703
|
+
cacheEnabled = true;
|
|
2704
|
+
cleanupInterval = 6e4;
|
|
2705
|
+
// 默认1分钟清理一次
|
|
2375
2706
|
/**
|
|
2376
2707
|
* 构造函数
|
|
2377
|
-
* @param
|
|
2708
|
+
* @param adapter 缓存适配器,如果不提供则使用默认的内存缓存适配器
|
|
2378
2709
|
*/
|
|
2379
|
-
constructor(
|
|
2380
|
-
this.
|
|
2710
|
+
constructor(adapter) {
|
|
2711
|
+
this.adapter = adapter || new MemoryCacheAdapter();
|
|
2381
2712
|
}
|
|
2382
2713
|
/**
|
|
2383
|
-
* 声明Module配置Schema
|
|
2714
|
+
* 声明Module配置Schema
|
|
2384
2715
|
*/
|
|
2385
2716
|
getModuleOptionsSchema() {
|
|
2386
2717
|
return {
|
|
2387
2718
|
_type: {},
|
|
2388
2719
|
validate: (options) => {
|
|
2389
|
-
if (options.
|
|
2390
|
-
return
|
|
2391
|
-
}
|
|
2392
|
-
if (options.routePrefix && !options.routePrefix.startsWith("/")) {
|
|
2393
|
-
return `routePrefix must start with '/'`;
|
|
2720
|
+
if (options.cacheDefaultTtl !== void 0 && options.cacheDefaultTtl < 0) {
|
|
2721
|
+
return `cacheDefaultTtl must be >= 0`;
|
|
2394
2722
|
}
|
|
2395
|
-
if (options.
|
|
2396
|
-
return
|
|
2723
|
+
if (options.cacheCleanupInterval !== void 0 && options.cacheCleanupInterval < 0) {
|
|
2724
|
+
return `cacheCleanupInterval must be >= 0`;
|
|
2397
2725
|
}
|
|
2398
2726
|
return true;
|
|
2399
2727
|
}
|
|
2400
2728
|
};
|
|
2401
2729
|
}
|
|
2402
2730
|
/**
|
|
2403
|
-
*
|
|
2731
|
+
* 引擎初始化前钩子
|
|
2404
2732
|
*/
|
|
2405
2733
|
onInit(engine) {
|
|
2406
2734
|
this.engine = engine;
|
|
2407
|
-
logger_default.info("
|
|
2735
|
+
logger_default.info("CachePlugin initialized cache storage");
|
|
2408
2736
|
}
|
|
2409
2737
|
/**
|
|
2410
|
-
* Handler
|
|
2738
|
+
* Handler加载后钩子
|
|
2739
|
+
* 拦截带有 type="cache" 的 Handler,包装原始方法实现缓存逻辑
|
|
2411
2740
|
*/
|
|
2412
2741
|
onHandlerLoad(handlers) {
|
|
2413
|
-
const
|
|
2414
|
-
(handler) => handler.type === "
|
|
2742
|
+
const cacheHandlers = handlers.filter(
|
|
2743
|
+
(handler) => handler.type === "cache"
|
|
2415
2744
|
);
|
|
2416
|
-
logger_default.info(`Found ${
|
|
2417
|
-
for (const handler of
|
|
2418
|
-
const routeOptions = handler.options;
|
|
2745
|
+
logger_default.info(`Found ${cacheHandlers.length} cache handler(s)`);
|
|
2746
|
+
for (const handler of cacheHandlers) {
|
|
2419
2747
|
const methodName = handler.methodName;
|
|
2420
2748
|
const moduleClass = handler.module;
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
);
|
|
2749
|
+
const cacheOptions = handler.options || {};
|
|
2750
|
+
const moduleMetadata = this.engine?.getModules().find((m) => m.clazz === moduleClass);
|
|
2751
|
+
if (!moduleMetadata) {
|
|
2752
|
+
logger_default.warn(`Module metadata not found for ${moduleClass.name}`);
|
|
2426
2753
|
continue;
|
|
2427
2754
|
}
|
|
2428
|
-
const
|
|
2429
|
-
const
|
|
2430
|
-
const
|
|
2431
|
-
const
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
return ctx.json({ error: "Handler method not found" }, 500);
|
|
2437
|
-
}
|
|
2438
|
-
try {
|
|
2439
|
-
const result = await method.call(moduleInstance, ctx);
|
|
2440
|
-
if (result instanceof Response) {
|
|
2441
|
-
return result;
|
|
2442
|
-
}
|
|
2443
|
-
if (typeof result === "string" || typeof result === "object" && result !== null && "type" in result) {
|
|
2444
|
-
return result;
|
|
2445
|
-
}
|
|
2446
|
-
return ctx.json(result);
|
|
2447
|
-
} catch (error) {
|
|
2448
|
-
logger_default.error(
|
|
2449
|
-
`Error in route handler ${moduleClass.name}.${methodName}`,
|
|
2450
|
-
error
|
|
2451
|
-
);
|
|
2452
|
-
return ctx.json(
|
|
2453
|
-
{
|
|
2454
|
-
error: "Internal server error",
|
|
2455
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2456
|
-
},
|
|
2457
|
-
500
|
|
2458
|
-
);
|
|
2459
|
-
}
|
|
2460
|
-
};
|
|
2461
|
-
const hono = this.engine.getHono();
|
|
2462
|
-
const methods = routeOptions.method ? Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method] : ["GET"];
|
|
2463
|
-
for (const fullPath of fullPaths) {
|
|
2464
|
-
let route = hono;
|
|
2465
|
-
if (this.globalMiddlewares.length > 0) {
|
|
2466
|
-
route = route.use(fullPath, ...this.globalMiddlewares);
|
|
2467
|
-
}
|
|
2468
|
-
if (moduleOptions.routeMiddlewares && moduleOptions.routeMiddlewares.length > 0) {
|
|
2469
|
-
route = route.use(fullPath, ...moduleOptions.routeMiddlewares);
|
|
2470
|
-
}
|
|
2471
|
-
if (routeOptions.middlewares && routeOptions.middlewares.length > 0) {
|
|
2472
|
-
route = route.use(fullPath, ...routeOptions.middlewares);
|
|
2473
|
-
}
|
|
2474
|
-
for (const method of methods) {
|
|
2475
|
-
const methodLower = method.toLowerCase();
|
|
2476
|
-
if (methodLower === "get" || methodLower === "post" || methodLower === "put" || methodLower === "delete" || methodLower === "patch" || methodLower === "head" || methodLower === "options") {
|
|
2477
|
-
route[methodLower](fullPath, routeHandler);
|
|
2478
|
-
const description = routeOptions.description ? ` (${routeOptions.description})` : "";
|
|
2479
|
-
logger_default.info(
|
|
2480
|
-
`Registered route: ${method.toUpperCase()} ${fullPath} -> ${moduleClass.name}.${methodName}${description}`
|
|
2481
|
-
);
|
|
2482
|
-
} else {
|
|
2483
|
-
logger_default.warn(
|
|
2484
|
-
`Unsupported HTTP method: ${method}, skipping route ${fullPath}`
|
|
2485
|
-
);
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2755
|
+
const moduleOptions = moduleMetadata.options;
|
|
2756
|
+
const moduleDefaultTtl = moduleOptions?.cacheDefaultTtl ?? this.defaultTtl;
|
|
2757
|
+
const moduleCacheEnabled = moduleOptions?.cacheEnabled ?? this.cacheEnabled;
|
|
2758
|
+
const ttl = cacheOptions.ttl ?? moduleDefaultTtl;
|
|
2759
|
+
const enabled = cacheOptions.enabled ?? moduleCacheEnabled;
|
|
2760
|
+
if (!enabled) {
|
|
2761
|
+
logger_default.info(`Cache disabled for ${moduleClass.name}.${methodName}`);
|
|
2762
|
+
continue;
|
|
2488
2763
|
}
|
|
2764
|
+
const moduleName = moduleMetadata.name;
|
|
2765
|
+
handler.wrap(async (next, instance, ...args) => {
|
|
2766
|
+
const cacheKey = this.generateCacheKey(
|
|
2767
|
+
moduleName,
|
|
2768
|
+
methodName,
|
|
2769
|
+
cacheOptions.key,
|
|
2770
|
+
args
|
|
2771
|
+
);
|
|
2772
|
+
const cached = await this.adapter.get(cacheKey);
|
|
2773
|
+
if (cached) {
|
|
2774
|
+
logger_default.debug(`Cache hit for ${cacheKey}`);
|
|
2775
|
+
return cached.value;
|
|
2776
|
+
}
|
|
2777
|
+
logger_default.debug(`Cache miss for ${cacheKey}`);
|
|
2778
|
+
const result = await next();
|
|
2779
|
+
await this.adapter.set(cacheKey, {
|
|
2780
|
+
value: result,
|
|
2781
|
+
expiresAt: Date.now() + ttl,
|
|
2782
|
+
createdAt: Date.now()
|
|
2783
|
+
});
|
|
2784
|
+
return result;
|
|
2785
|
+
});
|
|
2786
|
+
logger_default.info(
|
|
2787
|
+
`Wrapped ${moduleClass.name}.${methodName} with cache (TTL: ${ttl}ms)`
|
|
2788
|
+
);
|
|
2489
2789
|
}
|
|
2490
2790
|
}
|
|
2491
|
-
};
|
|
2492
|
-
|
|
2493
|
-
// src/plugins/route/decorator.ts
|
|
2494
|
-
function Route(options) {
|
|
2495
|
-
return Handler({
|
|
2496
|
-
type: "route",
|
|
2497
|
-
options
|
|
2498
|
-
});
|
|
2499
|
-
}
|
|
2500
|
-
function Page(options) {
|
|
2501
|
-
const pageOptions = {
|
|
2502
|
-
method: options.method || "GET",
|
|
2503
|
-
...options
|
|
2504
|
-
};
|
|
2505
|
-
return Route(pageOptions);
|
|
2506
|
-
}
|
|
2507
|
-
var DEFAULT_FAVICON = /* @__PURE__ */ jsxRuntime.jsx(
|
|
2508
|
-
"link",
|
|
2509
|
-
{
|
|
2510
|
-
rel: "icon",
|
|
2511
|
-
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>",
|
|
2512
|
-
type: "image/svg+xml"
|
|
2513
|
-
}
|
|
2514
|
-
);
|
|
2515
|
-
var BaseLayout = (props = {
|
|
2516
|
-
title: "Microservice Template"
|
|
2517
|
-
}) => html.html`<!DOCTYPE html>
|
|
2518
|
-
<html>
|
|
2519
|
-
<head>
|
|
2520
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2521
|
-
<title>${props.title}</title>
|
|
2522
|
-
${props.heads}
|
|
2523
|
-
</head>
|
|
2524
|
-
<body>
|
|
2525
|
-
${props.children}
|
|
2526
|
-
</body>
|
|
2527
|
-
</html>`;
|
|
2528
|
-
var HtmxLayout = (props = {
|
|
2529
|
-
title: "Microservice Template"
|
|
2530
|
-
}) => BaseLayout({
|
|
2531
|
-
title: props.title,
|
|
2532
|
-
heads: html.html`
|
|
2533
|
-
<script src="https://unpkg.com/htmx.org@latest"></script>
|
|
2534
|
-
<script src="https://unpkg.com/hyperscript.org@latest"></script>
|
|
2535
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
2536
|
-
${props.favicon || DEFAULT_FAVICON}
|
|
2537
|
-
`,
|
|
2538
|
-
children: props.children
|
|
2539
|
-
});
|
|
2540
|
-
function buildParamsSchema(schemas) {
|
|
2541
|
-
const shape = {};
|
|
2542
|
-
for (let i = 0; i < schemas.length; i++) {
|
|
2543
|
-
shape[String(i)] = schemas[i];
|
|
2544
|
-
}
|
|
2545
|
-
return zod.z.object(shape);
|
|
2546
|
-
}
|
|
2547
|
-
function parseAndValidateParams(body, schemas) {
|
|
2548
|
-
if (!body || typeof body !== "object") {
|
|
2549
|
-
body = {};
|
|
2550
|
-
}
|
|
2551
|
-
if (schemas.length === 0) {
|
|
2552
|
-
return { success: true, data: [] };
|
|
2553
|
-
}
|
|
2554
|
-
const paramsSchema = buildParamsSchema(schemas);
|
|
2555
|
-
const validation = paramsSchema.safeParse(body);
|
|
2556
|
-
if (!validation.success) {
|
|
2557
|
-
return {
|
|
2558
|
-
success: false,
|
|
2559
|
-
error: validation.error
|
|
2560
|
-
};
|
|
2561
|
-
}
|
|
2562
|
-
const validatedData = [];
|
|
2563
|
-
for (let i = 0; i < schemas.length; i++) {
|
|
2564
|
-
validatedData[i] = validation.data[String(i)];
|
|
2565
|
-
}
|
|
2566
|
-
return {
|
|
2567
|
-
success: true,
|
|
2568
|
-
data: validatedData
|
|
2569
|
-
};
|
|
2570
|
-
}
|
|
2571
|
-
function buildActionPath(enginePrefix, moduleName, handlerName) {
|
|
2572
|
-
const normalizedPrefix = enginePrefix.replace(/\/+$/, "");
|
|
2573
|
-
return `/${normalizedPrefix}/${moduleName}/${handlerName}`.replace(/\/+/g, "/").replace(/^\/\//, "/");
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
// src/plugins/action/plugin.ts
|
|
2577
|
-
function isAsyncIterable(obj) {
|
|
2578
|
-
return obj != null && typeof obj[Symbol.asyncIterator] === "function";
|
|
2579
|
-
}
|
|
2580
|
-
var ActionPlugin = class {
|
|
2581
|
-
name = "action-plugin";
|
|
2582
|
-
priority = 1e3 /* ROUTE */;
|
|
2583
|
-
// 路由插件优先级最低,必须最后执行
|
|
2584
|
-
engine;
|
|
2585
2791
|
/**
|
|
2586
|
-
*
|
|
2792
|
+
* 引擎启动前钩子
|
|
2587
2793
|
*/
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2794
|
+
onBeforeStart(engine) {
|
|
2795
|
+
const moduleOptions = engine.options;
|
|
2796
|
+
const cleanupInterval = moduleOptions?.cacheCleanupInterval ?? this.cleanupInterval;
|
|
2797
|
+
if (cleanupInterval > 0) {
|
|
2798
|
+
this.cleanupTimer = setInterval(() => {
|
|
2799
|
+
this.cleanup().catch((error) => {
|
|
2800
|
+
logger_default.error("Cache cleanup failed", error);
|
|
2801
|
+
});
|
|
2802
|
+
}, cleanupInterval);
|
|
2803
|
+
logger_default.info(
|
|
2804
|
+
`Started cache cleanup timer (interval: ${cleanupInterval}ms)`
|
|
2805
|
+
);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* 引擎停止时钩子
|
|
2810
|
+
*/
|
|
2811
|
+
async onDestroy() {
|
|
2812
|
+
if (this.cleanupTimer) {
|
|
2813
|
+
clearInterval(this.cleanupTimer);
|
|
2814
|
+
this.cleanupTimer = null;
|
|
2815
|
+
}
|
|
2816
|
+
await this.adapter.clear();
|
|
2817
|
+
logger_default.info("Cache storage cleared");
|
|
2598
2818
|
}
|
|
2599
2819
|
/**
|
|
2600
|
-
*
|
|
2820
|
+
* 生成缓存键
|
|
2821
|
+
* @param moduleName 模块名
|
|
2822
|
+
* @param methodName 方法名
|
|
2823
|
+
* @param keyFunction 可选的 key 函数
|
|
2824
|
+
* @param args 方法参数
|
|
2825
|
+
* @returns 缓存键(格式:模块名:方法名:hash)
|
|
2601
2826
|
*/
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2827
|
+
generateCacheKey(moduleName, methodName, keyFunction, args) {
|
|
2828
|
+
const keyData = keyFunction ? keyFunction(...args) : args;
|
|
2829
|
+
const serialized = ejson__namespace.stringify(keyData);
|
|
2830
|
+
const hash = crypto.createHash("sha256").update(serialized).digest("hex");
|
|
2831
|
+
return `${moduleName}:${methodName}:${hash}`;
|
|
2605
2832
|
}
|
|
2606
2833
|
/**
|
|
2607
|
-
*
|
|
2834
|
+
* 清理过期缓存
|
|
2608
2835
|
*/
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
for (const handler of actionHandlers) {
|
|
2615
|
-
const actionOptions = handler.options;
|
|
2616
|
-
const methodName = handler.methodName;
|
|
2617
|
-
const moduleClass = handler.module;
|
|
2618
|
-
const moduleInstance = this.engine.get(moduleClass);
|
|
2619
|
-
if (!moduleInstance) {
|
|
2620
|
-
logger_default.warn(
|
|
2621
|
-
`Module instance not found for ${moduleClass.name}, skipping action registration`
|
|
2622
|
-
);
|
|
2623
|
-
continue;
|
|
2624
|
-
}
|
|
2625
|
-
const moduleMetadata = this.engine.getModules().find((m) => m.clazz === moduleClass);
|
|
2626
|
-
if (!moduleMetadata) {
|
|
2627
|
-
logger_default.warn(
|
|
2628
|
-
`Module metadata not found for ${moduleClass.name}, skipping action registration`
|
|
2629
|
-
);
|
|
2630
|
-
continue;
|
|
2836
|
+
async cleanup() {
|
|
2837
|
+
if (typeof this.adapter.cleanupExpired === "function") {
|
|
2838
|
+
const cleaned = await this.adapter.cleanupExpired();
|
|
2839
|
+
if (cleaned > 0) {
|
|
2840
|
+
logger_default.debug(`Cleaned up ${cleaned} expired cache entry(ies)`);
|
|
2631
2841
|
}
|
|
2632
|
-
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
const actionHandler = async (ctx) => {
|
|
2642
|
-
const method = moduleInstance[methodName];
|
|
2643
|
-
if (typeof method !== "function") {
|
|
2644
|
-
const errorResponse = ejson__namespace.stringify({
|
|
2645
|
-
success: false,
|
|
2646
|
-
error: "Action method not found"
|
|
2647
|
-
});
|
|
2648
|
-
return ctx.text(errorResponse, 500, {
|
|
2649
|
-
"Content-Type": "application/json"
|
|
2650
|
-
});
|
|
2651
|
-
}
|
|
2652
|
-
try {
|
|
2653
|
-
let body = {};
|
|
2654
|
-
if (ctx.req.method === "GET") {
|
|
2655
|
-
const url = new URL(ctx.req.url);
|
|
2656
|
-
url.searchParams.forEach((value, key) => {
|
|
2657
|
-
body[key] = value;
|
|
2658
|
-
});
|
|
2659
|
-
} else {
|
|
2660
|
-
try {
|
|
2661
|
-
const rawBody = await ctx.req.text();
|
|
2662
|
-
if (rawBody) {
|
|
2663
|
-
body = ejson__namespace.parse(rawBody);
|
|
2664
|
-
}
|
|
2665
|
-
} catch (parseError) {
|
|
2666
|
-
try {
|
|
2667
|
-
body = await ctx.req.json().catch(() => ({}));
|
|
2668
|
-
} catch {
|
|
2669
|
-
body = {};
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
const validation = parseAndValidateParams(body, paramSchemas);
|
|
2674
|
-
if (!validation.success) {
|
|
2675
|
-
const errors = validation.error.issues || [];
|
|
2676
|
-
const errorMessage = `Validation failed: ${errors.map((e) => {
|
|
2677
|
-
const path = e.path || [];
|
|
2678
|
-
const pathStr = path.length > 0 ? path.map((p, i) => {
|
|
2679
|
-
if (i === 0 && typeof p === "string" && /^\d+$/.test(p)) {
|
|
2680
|
-
return `\u53C2\u6570[${p}]`;
|
|
2681
|
-
}
|
|
2682
|
-
return String(p);
|
|
2683
|
-
}).join(".") : "unknown";
|
|
2684
|
-
return `${pathStr}: ${e.message}`;
|
|
2685
|
-
}).join(", ")}`;
|
|
2686
|
-
const errorResponse = ejson__namespace.stringify({
|
|
2687
|
-
success: false,
|
|
2688
|
-
error: errorMessage
|
|
2689
|
-
});
|
|
2690
|
-
return ctx.text(errorResponse, 400, {
|
|
2691
|
-
"Content-Type": "application/json"
|
|
2692
|
-
});
|
|
2693
|
-
}
|
|
2694
|
-
const methodLength = method.length;
|
|
2695
|
-
const paramsLength = paramSchemas.length;
|
|
2696
|
-
const args = [...validation.data];
|
|
2697
|
-
if (methodLength > paramsLength) {
|
|
2698
|
-
args.unshift(ctx);
|
|
2699
|
-
}
|
|
2700
|
-
const result = await method.apply(moduleInstance, args);
|
|
2701
|
-
if (actionOptions.stream) {
|
|
2702
|
-
if (!isAsyncIterable(result)) {
|
|
2703
|
-
const errorResponse = ejson__namespace.stringify({
|
|
2704
|
-
success: false,
|
|
2705
|
-
error: "Stream action must return AsyncIterable"
|
|
2706
|
-
});
|
|
2707
|
-
return ctx.text(errorResponse, 500, {
|
|
2708
|
-
"Content-Type": "application/json"
|
|
2709
|
-
});
|
|
2710
|
-
}
|
|
2711
|
-
const encoder = new TextEncoder();
|
|
2712
|
-
const iterator = result[Symbol.asyncIterator]();
|
|
2713
|
-
const stream = new ReadableStream({
|
|
2714
|
-
async start(controller) {
|
|
2715
|
-
try {
|
|
2716
|
-
while (true) {
|
|
2717
|
-
const { value, done } = await iterator.next();
|
|
2718
|
-
if (done) {
|
|
2719
|
-
controller.enqueue(
|
|
2720
|
-
encoder.encode(ejson__namespace.stringify({ done: true }))
|
|
2721
|
-
);
|
|
2722
|
-
controller.close();
|
|
2723
|
-
break;
|
|
2724
|
-
}
|
|
2725
|
-
controller.enqueue(
|
|
2726
|
-
encoder.encode(ejson__namespace.stringify({ value, done: false }))
|
|
2727
|
-
);
|
|
2728
|
-
}
|
|
2729
|
-
} catch (error) {
|
|
2730
|
-
controller.enqueue(
|
|
2731
|
-
encoder.encode(
|
|
2732
|
-
ejson__namespace.stringify({
|
|
2733
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2734
|
-
done: true
|
|
2735
|
-
})
|
|
2736
|
-
)
|
|
2737
|
-
);
|
|
2738
|
-
controller.close();
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
});
|
|
2742
|
-
return new Response(stream, {
|
|
2743
|
-
headers: {
|
|
2744
|
-
"Content-Type": "text/event-stream",
|
|
2745
|
-
"Cache-Control": "no-cache",
|
|
2746
|
-
Connection: "keep-alive"
|
|
2747
|
-
}
|
|
2748
|
-
});
|
|
2749
|
-
}
|
|
2750
|
-
if (returnSchema) {
|
|
2751
|
-
const returnValidation = returnSchema.safeParse(result);
|
|
2752
|
-
if (!returnValidation.success) {
|
|
2753
|
-
logger_default.error(
|
|
2754
|
-
`Return value validation failed for ${moduleClass.name}.${methodName}`,
|
|
2755
|
-
returnValidation.error
|
|
2756
|
-
);
|
|
2757
|
-
const returnErrors = returnValidation.error.issues || [];
|
|
2758
|
-
const errorMessage = `Return value validation failed: ${returnErrors.map((e) => {
|
|
2759
|
-
const path = e.path || [];
|
|
2760
|
-
const pathStr = path.length > 0 ? path.map((p) => String(p)).join(".") : "root";
|
|
2761
|
-
return `${pathStr}: ${e.message}`;
|
|
2762
|
-
}).join(", ")}`;
|
|
2763
|
-
const errorResponse = ejson__namespace.stringify({
|
|
2764
|
-
success: false,
|
|
2765
|
-
error: errorMessage
|
|
2766
|
-
});
|
|
2767
|
-
return ctx.text(errorResponse, 400, {
|
|
2768
|
-
"Content-Type": "application/json"
|
|
2769
|
-
});
|
|
2770
|
-
}
|
|
2771
|
-
const successResponse2 = ejson__namespace.stringify({
|
|
2772
|
-
success: true,
|
|
2773
|
-
data: returnValidation.data
|
|
2774
|
-
});
|
|
2775
|
-
return ctx.text(successResponse2, 200, {
|
|
2776
|
-
"Content-Type": "application/json"
|
|
2777
|
-
});
|
|
2778
|
-
}
|
|
2779
|
-
const successResponse = ejson__namespace.stringify({
|
|
2780
|
-
success: true,
|
|
2781
|
-
data: result
|
|
2782
|
-
});
|
|
2783
|
-
return ctx.text(successResponse, 200, {
|
|
2784
|
-
"Content-Type": "application/json"
|
|
2785
|
-
});
|
|
2786
|
-
} catch (error) {
|
|
2787
|
-
logger_default.error(
|
|
2788
|
-
`Error in action handler ${moduleClass.name}.${methodName}`,
|
|
2789
|
-
error
|
|
2790
|
-
);
|
|
2791
|
-
const errorResponse = ejson__namespace.stringify({
|
|
2792
|
-
success: false,
|
|
2793
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2794
|
-
});
|
|
2795
|
-
return ctx.text(errorResponse, 500, {
|
|
2796
|
-
"Content-Type": "application/json"
|
|
2797
|
-
});
|
|
2842
|
+
} else {
|
|
2843
|
+
const now = Date.now();
|
|
2844
|
+
let cleaned = 0;
|
|
2845
|
+
const keys = await this.adapter.keys();
|
|
2846
|
+
for (const key of keys) {
|
|
2847
|
+
const item = await this.adapter.get(key);
|
|
2848
|
+
if (!item || item.expiresAt <= now) {
|
|
2849
|
+
await this.adapter.delete(key);
|
|
2850
|
+
cleaned++;
|
|
2798
2851
|
}
|
|
2799
|
-
};
|
|
2800
|
-
const hono = this.engine.getHono();
|
|
2801
|
-
let route = hono;
|
|
2802
|
-
if (moduleOptions.actionMiddlewares && moduleOptions.actionMiddlewares.length > 0) {
|
|
2803
|
-
route = route.use(actionPath, ...moduleOptions.actionMiddlewares);
|
|
2804
2852
|
}
|
|
2805
|
-
if (
|
|
2806
|
-
|
|
2853
|
+
if (cleaned > 0) {
|
|
2854
|
+
logger_default.debug(`Cleaned up ${cleaned} expired cache entry(ies)`);
|
|
2807
2855
|
}
|
|
2808
|
-
route.get(actionPath, actionHandler);
|
|
2809
|
-
route.post(actionPath, actionHandler);
|
|
2810
|
-
logger_default.info(
|
|
2811
|
-
`[\u6CE8\u518C\u52A8\u4F5C] ${moduleClass.name}.${methodName} ${actionOptions.description}`
|
|
2812
|
-
);
|
|
2813
2856
|
}
|
|
2814
2857
|
}
|
|
2858
|
+
/**
|
|
2859
|
+
* 获取缓存统计信息
|
|
2860
|
+
*/
|
|
2861
|
+
async getStats() {
|
|
2862
|
+
return await this.adapter.getStats();
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* 清空所有缓存
|
|
2866
|
+
*/
|
|
2867
|
+
async clear() {
|
|
2868
|
+
await this.adapter.clear();
|
|
2869
|
+
logger_default.info("All cache cleared");
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* 删除指定的缓存项
|
|
2873
|
+
*/
|
|
2874
|
+
async delete(key) {
|
|
2875
|
+
return await this.adapter.delete(key);
|
|
2876
|
+
}
|
|
2815
2877
|
};
|
|
2816
2878
|
|
|
2817
|
-
// src/plugins/
|
|
2818
|
-
function
|
|
2879
|
+
// src/plugins/cache/decorator.ts
|
|
2880
|
+
function Cache(options = {}) {
|
|
2819
2881
|
return Handler({
|
|
2820
|
-
type: "
|
|
2821
|
-
options
|
|
2882
|
+
type: "cache",
|
|
2883
|
+
options: {
|
|
2884
|
+
ttl: options.ttl ?? 6e4,
|
|
2885
|
+
// 默认1分钟
|
|
2886
|
+
key: options.key,
|
|
2887
|
+
// key 函数
|
|
2888
|
+
enabled: options.enabled ?? true
|
|
2889
|
+
}
|
|
2822
2890
|
});
|
|
2823
2891
|
}
|
|
2824
2892
|
|
|
2825
|
-
// src/plugins/
|
|
2826
|
-
var
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
this.cache.clear();
|
|
2893
|
+
// src/plugins/graceful-shutdown/plugin.ts
|
|
2894
|
+
var GracefulShutdownPlugin = class {
|
|
2895
|
+
name = "graceful-shutdown-plugin";
|
|
2896
|
+
priority = 50 /* SYSTEM */;
|
|
2897
|
+
// 系统级插件,最高优先级
|
|
2898
|
+
engine = null;
|
|
2899
|
+
options;
|
|
2900
|
+
// 正在执行的处理器计数
|
|
2901
|
+
activeHandlers = 0;
|
|
2902
|
+
// 是否正在停机
|
|
2903
|
+
isShuttingDown = false;
|
|
2904
|
+
// 停机超时定时器
|
|
2905
|
+
shutdownTimer = null;
|
|
2906
|
+
// 信号监听器(用于清理)
|
|
2907
|
+
signalListeners = /* @__PURE__ */ new Map();
|
|
2908
|
+
constructor(options) {
|
|
2909
|
+
this.options = {
|
|
2910
|
+
shutdownTimeout: options?.shutdownTimeout ?? 10 * 60 * 1e3,
|
|
2911
|
+
// 默认10分钟
|
|
2912
|
+
enabled: options?.enabled ?? true
|
|
2913
|
+
};
|
|
2847
2914
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2915
|
+
/**
|
|
2916
|
+
* 引擎初始化钩子
|
|
2917
|
+
*/
|
|
2918
|
+
onInit(engine) {
|
|
2919
|
+
this.engine = engine;
|
|
2920
|
+
logger_default.info("GracefulShutdownPlugin initialized");
|
|
2851
2921
|
}
|
|
2852
2922
|
/**
|
|
2853
|
-
*
|
|
2854
|
-
* 这个方法比通过 keys() + get() + delete() 更高效
|
|
2923
|
+
* Handler加载钩子:拦截所有处理器,追踪执行状态
|
|
2855
2924
|
*/
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
for (const [key, item] of this.cache.entries()) {
|
|
2860
|
-
if (item.expiresAt <= now) {
|
|
2861
|
-
this.cache.delete(key);
|
|
2862
|
-
cleaned++;
|
|
2863
|
-
}
|
|
2925
|
+
onHandlerLoad(handlers) {
|
|
2926
|
+
if (!this.options.enabled) {
|
|
2927
|
+
return;
|
|
2864
2928
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
};
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
this.client = options.client;
|
|
2885
|
-
this.keyPrefix = options.keyPrefix || "cache:";
|
|
2886
|
-
}
|
|
2887
|
-
getKey(key) {
|
|
2888
|
-
return `${this.keyPrefix}${key}`;
|
|
2929
|
+
for (const handler of handlers) {
|
|
2930
|
+
handler.wrap(async (next, instance, ...args) => {
|
|
2931
|
+
if (this.isShuttingDown) {
|
|
2932
|
+
throw new Error("Service is shutting down, new requests are not accepted");
|
|
2933
|
+
}
|
|
2934
|
+
this.incrementActiveHandlers();
|
|
2935
|
+
try {
|
|
2936
|
+
const result = await next();
|
|
2937
|
+
return result;
|
|
2938
|
+
} catch (error) {
|
|
2939
|
+
throw error;
|
|
2940
|
+
} finally {
|
|
2941
|
+
this.decrementActiveHandlers();
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
logger_default.info(
|
|
2946
|
+
`GracefulShutdownPlugin: Tracking ${handlers.length} handler(s)`
|
|
2947
|
+
);
|
|
2889
2948
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2949
|
+
/**
|
|
2950
|
+
* 引擎启动后钩子:注册系统信号监听器
|
|
2951
|
+
*/
|
|
2952
|
+
async onAfterStart(engine) {
|
|
2953
|
+
if (!this.options.enabled) {
|
|
2954
|
+
return;
|
|
2895
2955
|
}
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2956
|
+
this.engine = engine;
|
|
2957
|
+
this.registerSignalHandlers();
|
|
2958
|
+
logger_default.info(
|
|
2959
|
+
`GracefulShutdownPlugin: Signal handlers registered, shutdown timeout: ${this.options.shutdownTimeout}ms`
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* 注册系统信号监听器
|
|
2964
|
+
*/
|
|
2965
|
+
registerSignalHandlers() {
|
|
2966
|
+
const signals = [
|
|
2967
|
+
"SIGINT",
|
|
2968
|
+
// Ctrl+C (Unix/Linux/Mac)
|
|
2969
|
+
"SIGTERM",
|
|
2970
|
+
// 终止信号 (Unix/Linux/Mac)
|
|
2971
|
+
"SIGBREAK"
|
|
2972
|
+
// Ctrl+Break (Windows)
|
|
2973
|
+
];
|
|
2974
|
+
for (const signal of signals) {
|
|
2975
|
+
if (process.platform === "win32" && signal === "SIGTERM") {
|
|
2976
|
+
continue;
|
|
2901
2977
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2978
|
+
const handler = () => {
|
|
2979
|
+
logger_default.info(`GracefulShutdownPlugin: Received ${signal} signal`);
|
|
2980
|
+
this.initiateShutdown();
|
|
2981
|
+
};
|
|
2982
|
+
process.on(signal, handler);
|
|
2983
|
+
this.signalListeners.set(signal, handler);
|
|
2906
2984
|
}
|
|
2907
2985
|
}
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
const result = await this.client.del(redisKey);
|
|
2917
|
-
return result > 0;
|
|
2986
|
+
/**
|
|
2987
|
+
* 增加活跃处理器计数
|
|
2988
|
+
*/
|
|
2989
|
+
incrementActiveHandlers() {
|
|
2990
|
+
this.activeHandlers++;
|
|
2991
|
+
logger_default.debug(
|
|
2992
|
+
`GracefulShutdownPlugin: Active handlers: ${this.activeHandlers}`
|
|
2993
|
+
);
|
|
2918
2994
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2995
|
+
/**
|
|
2996
|
+
* 减少活跃处理器计数
|
|
2997
|
+
*/
|
|
2998
|
+
decrementActiveHandlers() {
|
|
2999
|
+
this.activeHandlers = Math.max(0, this.activeHandlers - 1);
|
|
3000
|
+
logger_default.debug(
|
|
3001
|
+
`GracefulShutdownPlugin: Active handlers: ${this.activeHandlers}`
|
|
3002
|
+
);
|
|
3003
|
+
if (this.isShuttingDown && this.activeHandlers === 0) {
|
|
3004
|
+
logger_default.info(
|
|
3005
|
+
"GracefulShutdownPlugin: All handlers completed, proceeding with shutdown"
|
|
3006
|
+
);
|
|
3007
|
+
this.completeShutdown();
|
|
2924
3008
|
}
|
|
2925
3009
|
}
|
|
2926
|
-
async keys() {
|
|
2927
|
-
const pattern = `${this.keyPrefix}*`;
|
|
2928
|
-
const redisKeys = await this.client.keys(pattern);
|
|
2929
|
-
return redisKeys.map((key) => key.replace(this.keyPrefix, ""));
|
|
2930
|
-
}
|
|
2931
3010
|
/**
|
|
2932
|
-
*
|
|
2933
|
-
*
|
|
2934
|
-
* 注意:Redis 本身支持自动过期(通过 SET key value EX seconds),
|
|
2935
|
-
* 过期键会被 Redis 自动删除。但在某些情况下(如测试环境、某些 Redis 客户端),
|
|
2936
|
-
* 可能需要手动清理。此方法会检查并清理:
|
|
2937
|
-
* 1. 已过期但尚未被 Redis 删除的键(双重保险)
|
|
2938
|
-
* 2. JSON 解析失败的数据
|
|
3011
|
+
* 启动优雅停机流程
|
|
2939
3012
|
*/
|
|
2940
|
-
async
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
}
|
|
2955
|
-
} catch {
|
|
2956
|
-
await this.client.del(redisKey);
|
|
2957
|
-
cleaned++;
|
|
2958
|
-
}
|
|
3013
|
+
async initiateShutdown() {
|
|
3014
|
+
if (this.isShuttingDown) {
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
this.isShuttingDown = true;
|
|
3018
|
+
logger_default.info(
|
|
3019
|
+
`GracefulShutdownPlugin: Initiating graceful shutdown, waiting for ${this.activeHandlers} active handler(s) to complete`
|
|
3020
|
+
);
|
|
3021
|
+
if (this.activeHandlers === 0) {
|
|
3022
|
+
logger_default.info(
|
|
3023
|
+
"GracefulShutdownPlugin: No active handlers, proceeding with shutdown immediately"
|
|
3024
|
+
);
|
|
3025
|
+
await this.completeShutdown();
|
|
3026
|
+
return;
|
|
2959
3027
|
}
|
|
2960
|
-
|
|
3028
|
+
this.shutdownTimer = setTimeout(() => {
|
|
3029
|
+
logger_default.warn(
|
|
3030
|
+
`GracefulShutdownPlugin: Shutdown timeout (${this.options.shutdownTimeout}ms) reached, forcing shutdown`
|
|
3031
|
+
);
|
|
3032
|
+
this.completeShutdown();
|
|
3033
|
+
}, this.options.shutdownTimeout);
|
|
2961
3034
|
}
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3035
|
+
/**
|
|
3036
|
+
* 完成停机流程
|
|
3037
|
+
*/
|
|
3038
|
+
async completeShutdown() {
|
|
3039
|
+
if (this.shutdownTimer) {
|
|
3040
|
+
clearTimeout(this.shutdownTimer);
|
|
3041
|
+
this.shutdownTimer = null;
|
|
3042
|
+
}
|
|
3043
|
+
this.cleanupSignalHandlers();
|
|
3044
|
+
if (this.engine) {
|
|
3045
|
+
try {
|
|
3046
|
+
await this.engine.stop();
|
|
3047
|
+
logger_default.info("GracefulShutdownPlugin: Engine stopped successfully");
|
|
3048
|
+
} catch (error) {
|
|
3049
|
+
logger_default.error("GracefulShutdownPlugin: Failed to stop engine", error);
|
|
2973
3050
|
}
|
|
2974
3051
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
entries
|
|
2978
|
-
};
|
|
3052
|
+
logger_default.info("GracefulShutdownPlugin: Process exiting");
|
|
3053
|
+
process.exit(0);
|
|
2979
3054
|
}
|
|
2980
|
-
};
|
|
2981
|
-
|
|
2982
|
-
// src/plugins/cache/plugin.ts
|
|
2983
|
-
var CachePlugin = class {
|
|
2984
|
-
name = "cache-plugin";
|
|
2985
|
-
priority = 400 /* PERFORMANCE */;
|
|
2986
|
-
// 性能优化插件,优先级较低
|
|
2987
|
-
// 缓存适配器
|
|
2988
|
-
adapter;
|
|
2989
|
-
// 清理定时器
|
|
2990
|
-
cleanupTimer = null;
|
|
2991
|
-
// 引擎引用
|
|
2992
|
-
engine = null;
|
|
2993
|
-
// 模块配置
|
|
2994
|
-
defaultTtl = 6e4;
|
|
2995
|
-
// 默认1分钟
|
|
2996
|
-
cacheEnabled = true;
|
|
2997
|
-
cleanupInterval = 6e4;
|
|
2998
|
-
// 默认1分钟清理一次
|
|
2999
3055
|
/**
|
|
3000
|
-
*
|
|
3001
|
-
* @param adapter 缓存适配器,如果不提供则使用默认的内存缓存适配器
|
|
3056
|
+
* 清理信号监听器
|
|
3002
3057
|
*/
|
|
3003
|
-
|
|
3004
|
-
|
|
3058
|
+
cleanupSignalHandlers() {
|
|
3059
|
+
for (const [signal, handler] of this.signalListeners.entries()) {
|
|
3060
|
+
process.removeListener(signal, handler);
|
|
3061
|
+
}
|
|
3062
|
+
this.signalListeners.clear();
|
|
3005
3063
|
}
|
|
3006
3064
|
/**
|
|
3007
|
-
*
|
|
3065
|
+
* 引擎销毁钩子:清理资源
|
|
3008
3066
|
*/
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
if (options.cacheCleanupInterval !== void 0 && options.cacheCleanupInterval < 0) {
|
|
3017
|
-
return `cacheCleanupInterval must be >= 0`;
|
|
3018
|
-
}
|
|
3019
|
-
return true;
|
|
3020
|
-
}
|
|
3021
|
-
};
|
|
3067
|
+
async onDestroy() {
|
|
3068
|
+
this.cleanupSignalHandlers();
|
|
3069
|
+
if (this.shutdownTimer) {
|
|
3070
|
+
clearTimeout(this.shutdownTimer);
|
|
3071
|
+
this.shutdownTimer = null;
|
|
3072
|
+
}
|
|
3073
|
+
logger_default.info("GracefulShutdownPlugin: Cleaned up");
|
|
3022
3074
|
}
|
|
3023
3075
|
/**
|
|
3024
|
-
*
|
|
3076
|
+
* 获取当前活跃处理器数量(用于测试和监控)
|
|
3025
3077
|
*/
|
|
3026
|
-
|
|
3027
|
-
this.
|
|
3028
|
-
logger_default.info("CachePlugin initialized cache storage");
|
|
3078
|
+
getActiveHandlersCount() {
|
|
3079
|
+
return this.activeHandlers;
|
|
3029
3080
|
}
|
|
3030
3081
|
/**
|
|
3031
|
-
*
|
|
3032
|
-
* 拦截带有 type="cache" 的 Handler,包装原始方法实现缓存逻辑
|
|
3082
|
+
* 检查是否正在停机(用于测试和监控)
|
|
3033
3083
|
*/
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3084
|
+
isShuttingDownNow() {
|
|
3085
|
+
return this.isShuttingDown;
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
|
|
3089
|
+
// src/plugins/schedule/types.ts
|
|
3090
|
+
var ScheduleMode = /* @__PURE__ */ ((ScheduleMode2) => {
|
|
3091
|
+
ScheduleMode2["FIXED_RATE"] = "FIXED_RATE";
|
|
3092
|
+
ScheduleMode2["FIXED_DELAY"] = "FIXED_DELAY";
|
|
3093
|
+
return ScheduleMode2;
|
|
3094
|
+
})(ScheduleMode || {});
|
|
3095
|
+
|
|
3096
|
+
// src/plugins/schedule/decorator.ts
|
|
3097
|
+
function Schedule(options) {
|
|
3098
|
+
return Handler({
|
|
3099
|
+
type: "schedule",
|
|
3100
|
+
options: {
|
|
3101
|
+
interval: options.interval,
|
|
3102
|
+
mode: options.mode || "FIXED_RATE" /* FIXED_RATE */
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/plugins/schedule/mock-etcd.ts
|
|
3108
|
+
var MockEtcd3 = class {
|
|
3109
|
+
elections = /* @__PURE__ */ new Map();
|
|
3110
|
+
election(key, ttl) {
|
|
3111
|
+
if (!this.elections.has(key)) {
|
|
3112
|
+
this.elections.set(key, new MockElection(key));
|
|
3113
|
+
}
|
|
3114
|
+
return this.elections.get(key);
|
|
3115
|
+
}
|
|
3116
|
+
getElection(key) {
|
|
3117
|
+
return this.elections.get(key);
|
|
3118
|
+
}
|
|
3119
|
+
clearElections() {
|
|
3120
|
+
this.elections.clear();
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
var MockElection = class {
|
|
3124
|
+
constructor(key) {
|
|
3125
|
+
this.key = key;
|
|
3126
|
+
}
|
|
3127
|
+
observers = [];
|
|
3128
|
+
campaigns = [];
|
|
3129
|
+
currentLeader = null;
|
|
3130
|
+
async observe() {
|
|
3131
|
+
const observer = new MockObserver(this);
|
|
3132
|
+
this.observers.push(observer);
|
|
3133
|
+
if (this.currentLeader) {
|
|
3134
|
+
setTimeout(() => {
|
|
3135
|
+
observer.notifyChange(this.currentLeader);
|
|
3136
|
+
}, 0);
|
|
3137
|
+
}
|
|
3138
|
+
return observer;
|
|
3139
|
+
}
|
|
3140
|
+
campaign(candidate) {
|
|
3141
|
+
const campaign = new MockCampaign(candidate, this);
|
|
3142
|
+
this.campaigns.push(campaign);
|
|
3143
|
+
if (!this.currentLeader) {
|
|
3144
|
+
setTimeout(() => {
|
|
3145
|
+
this.setLeader(candidate);
|
|
3146
|
+
}, 50);
|
|
3147
|
+
}
|
|
3148
|
+
return campaign;
|
|
3149
|
+
}
|
|
3150
|
+
setLeader(leader) {
|
|
3151
|
+
this.currentLeader = leader;
|
|
3152
|
+
for (const observer of this.observers) {
|
|
3153
|
+
observer.notifyChange(leader);
|
|
3154
|
+
}
|
|
3155
|
+
setTimeout(() => {
|
|
3156
|
+
for (const campaign of this.campaigns) {
|
|
3157
|
+
if (campaign.candidate === leader && !campaign.resigned) {
|
|
3158
|
+
campaign.notifyElected();
|
|
3073
3159
|
}
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3160
|
+
}
|
|
3161
|
+
}, 50);
|
|
3162
|
+
}
|
|
3163
|
+
getCurrentLeader() {
|
|
3164
|
+
return this.currentLeader;
|
|
3165
|
+
}
|
|
3166
|
+
};
|
|
3167
|
+
var MockObserver = class {
|
|
3168
|
+
constructor(election) {
|
|
3169
|
+
this.election = election;
|
|
3170
|
+
}
|
|
3171
|
+
changeCallbacks = [];
|
|
3172
|
+
on(event, callback) {
|
|
3173
|
+
if (event === "change") {
|
|
3174
|
+
this.changeCallbacks.push(callback);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
notifyChange(leader) {
|
|
3178
|
+
for (const callback of this.changeCallbacks) {
|
|
3179
|
+
callback(leader);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
var MockCampaign = class {
|
|
3184
|
+
constructor(candidate, election) {
|
|
3185
|
+
this.candidate = candidate;
|
|
3186
|
+
this.election = election;
|
|
3187
|
+
}
|
|
3188
|
+
errorCallbacks = [];
|
|
3189
|
+
electedCallbacks = [];
|
|
3190
|
+
resigned = false;
|
|
3191
|
+
on(event, callback) {
|
|
3192
|
+
if (event === "error") {
|
|
3193
|
+
this.errorCallbacks.push(callback);
|
|
3194
|
+
} else if (event === "elected") {
|
|
3195
|
+
this.electedCallbacks.push(callback);
|
|
3086
3196
|
}
|
|
3087
3197
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
const cleanupInterval = moduleOptions?.cacheCleanupInterval ?? this.cleanupInterval;
|
|
3094
|
-
if (cleanupInterval > 0) {
|
|
3095
|
-
this.cleanupTimer = setInterval(() => {
|
|
3096
|
-
this.cleanup().catch((error) => {
|
|
3097
|
-
logger_default.error("Cache cleanup failed", error);
|
|
3098
|
-
});
|
|
3099
|
-
}, cleanupInterval);
|
|
3100
|
-
logger_default.info(
|
|
3101
|
-
`Started cache cleanup timer (interval: ${cleanupInterval}ms)`
|
|
3102
|
-
);
|
|
3198
|
+
notifyElected() {
|
|
3199
|
+
if (!this.resigned) {
|
|
3200
|
+
for (const callback of this.electedCallbacks) {
|
|
3201
|
+
callback();
|
|
3202
|
+
}
|
|
3103
3203
|
}
|
|
3104
3204
|
}
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
async onDestroy() {
|
|
3109
|
-
if (this.cleanupTimer) {
|
|
3110
|
-
clearInterval(this.cleanupTimer);
|
|
3111
|
-
this.cleanupTimer = null;
|
|
3205
|
+
notifyError(error) {
|
|
3206
|
+
for (const callback of this.errorCallbacks) {
|
|
3207
|
+
callback(error);
|
|
3112
3208
|
}
|
|
3113
|
-
await this.adapter.clear();
|
|
3114
|
-
logger_default.info("Cache storage cleared");
|
|
3115
3209
|
}
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
const hash = crypto.createHash("sha256").update(serialized).digest("hex");
|
|
3128
|
-
return `${moduleName}:${methodName}:${hash}`;
|
|
3210
|
+
async resign() {
|
|
3211
|
+
this.resigned = true;
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
|
|
3215
|
+
// src/plugins/schedule/scheduler.ts
|
|
3216
|
+
var import_api = __toESM(require_src());
|
|
3217
|
+
var tracer = import_api.trace.getTracer("scheduler");
|
|
3218
|
+
var Scheduler = class {
|
|
3219
|
+
constructor(etcdClient) {
|
|
3220
|
+
this.etcdClient = etcdClient;
|
|
3129
3221
|
}
|
|
3222
|
+
campaigns = /* @__PURE__ */ new Map();
|
|
3223
|
+
timers = /* @__PURE__ */ new Map();
|
|
3224
|
+
isLeader = /* @__PURE__ */ new Map();
|
|
3130
3225
|
/**
|
|
3131
|
-
*
|
|
3226
|
+
* 启动调度任务
|
|
3132
3227
|
*/
|
|
3133
|
-
async
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
let cleaned = 0;
|
|
3142
|
-
const keys = await this.adapter.keys();
|
|
3143
|
-
for (const key of keys) {
|
|
3144
|
-
const item = await this.adapter.get(key);
|
|
3145
|
-
if (!item || item.expiresAt <= now) {
|
|
3146
|
-
await this.adapter.delete(key);
|
|
3147
|
-
cleaned++;
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
if (cleaned > 0) {
|
|
3151
|
-
logger_default.debug(`Cleaned up ${cleaned} expired cache entry(ies)`);
|
|
3228
|
+
async startSchedule(serviceId, moduleName, methodName, electionKey, metadata, method) {
|
|
3229
|
+
const election = this.etcdClient.election(electionKey, 10);
|
|
3230
|
+
const observe = await election.observe();
|
|
3231
|
+
observe.on("change", (leader) => {
|
|
3232
|
+
const isLeader = leader === serviceId;
|
|
3233
|
+
this.isLeader.set(serviceId, isLeader);
|
|
3234
|
+
if (!isLeader) {
|
|
3235
|
+
this.stopTimer(serviceId);
|
|
3152
3236
|
}
|
|
3153
|
-
}
|
|
3237
|
+
});
|
|
3238
|
+
const campaign = election.campaign(serviceId);
|
|
3239
|
+
this.campaigns.set(serviceId, campaign);
|
|
3240
|
+
campaign.on("error", (error) => {
|
|
3241
|
+
logger_default.error(`Error in campaign for ${moduleName}.${methodName}:`, error);
|
|
3242
|
+
});
|
|
3243
|
+
campaign.on("elected", () => {
|
|
3244
|
+
this.isLeader.set(serviceId, true);
|
|
3245
|
+
this.startTimer(serviceId, metadata, moduleName, method);
|
|
3246
|
+
logger_default.info(`become leader for ${moduleName}.${methodName}`);
|
|
3247
|
+
});
|
|
3154
3248
|
}
|
|
3155
3249
|
/**
|
|
3156
|
-
*
|
|
3250
|
+
* 启动定时器
|
|
3157
3251
|
*/
|
|
3158
|
-
|
|
3159
|
-
|
|
3252
|
+
startTimer(serviceId, metadata, moduleName, method) {
|
|
3253
|
+
this.stopTimer(serviceId);
|
|
3254
|
+
const wrappedMethod = async () => {
|
|
3255
|
+
tracer.startActiveSpan(
|
|
3256
|
+
`ScheduleTask ${moduleName}.${metadata.name}`,
|
|
3257
|
+
{ root: true },
|
|
3258
|
+
async (span) => {
|
|
3259
|
+
span.setAttribute("serviceId", serviceId);
|
|
3260
|
+
span.setAttribute("methodName", metadata.name);
|
|
3261
|
+
span.setAttribute("moduleName", moduleName);
|
|
3262
|
+
span.setAttribute("interval", metadata.interval);
|
|
3263
|
+
span.setAttribute("mode", metadata.mode);
|
|
3264
|
+
try {
|
|
3265
|
+
await method();
|
|
3266
|
+
} catch (error) {
|
|
3267
|
+
span.setStatus({
|
|
3268
|
+
code: import_api.SpanStatusCode.ERROR,
|
|
3269
|
+
message: error.message
|
|
3270
|
+
});
|
|
3271
|
+
logger_default.error(
|
|
3272
|
+
`Error executing schedule task ${moduleName}.${metadata.name}:`,
|
|
3273
|
+
error
|
|
3274
|
+
);
|
|
3275
|
+
} finally {
|
|
3276
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
3277
|
+
span.end();
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
);
|
|
3281
|
+
};
|
|
3282
|
+
if (metadata.mode === "FIXED_DELAY" /* FIXED_DELAY */) {
|
|
3283
|
+
const runTask = async () => {
|
|
3284
|
+
if (!this.isLeader.get(serviceId)) return;
|
|
3285
|
+
try {
|
|
3286
|
+
await wrappedMethod();
|
|
3287
|
+
} finally {
|
|
3288
|
+
this.timers.set(serviceId, setTimeout(runTask, metadata.interval));
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
runTask();
|
|
3292
|
+
} else {
|
|
3293
|
+
this.timers.set(
|
|
3294
|
+
serviceId,
|
|
3295
|
+
setInterval(async () => {
|
|
3296
|
+
if (!this.isLeader.get(serviceId)) return;
|
|
3297
|
+
await wrappedMethod();
|
|
3298
|
+
}, metadata.interval)
|
|
3299
|
+
);
|
|
3300
|
+
}
|
|
3160
3301
|
}
|
|
3161
3302
|
/**
|
|
3162
|
-
*
|
|
3303
|
+
* 停止定时器
|
|
3163
3304
|
*/
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3305
|
+
stopTimer(serviceId) {
|
|
3306
|
+
const timer = this.timers.get(serviceId);
|
|
3307
|
+
if (timer) {
|
|
3308
|
+
clearTimeout(timer);
|
|
3309
|
+
clearInterval(timer);
|
|
3310
|
+
this.timers.delete(serviceId);
|
|
3311
|
+
}
|
|
3167
3312
|
}
|
|
3168
3313
|
/**
|
|
3169
|
-
*
|
|
3314
|
+
* 停止所有调度任务
|
|
3170
3315
|
*/
|
|
3171
|
-
async
|
|
3172
|
-
|
|
3316
|
+
async stop() {
|
|
3317
|
+
for (const serviceId of this.timers.keys()) {
|
|
3318
|
+
this.stopTimer(serviceId);
|
|
3319
|
+
}
|
|
3320
|
+
for (const [serviceId, campaign] of this.campaigns.entries()) {
|
|
3321
|
+
try {
|
|
3322
|
+
await campaign.resign().catch(() => {
|
|
3323
|
+
});
|
|
3324
|
+
} catch (error) {
|
|
3325
|
+
logger_default.error(`Error stopping schedule ${serviceId}:`, error);
|
|
3326
|
+
} finally {
|
|
3327
|
+
this.campaigns.delete(serviceId);
|
|
3328
|
+
this.isLeader.delete(serviceId);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3173
3331
|
}
|
|
3174
3332
|
};
|
|
3175
3333
|
|
|
3176
|
-
// src/plugins/
|
|
3177
|
-
function
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3334
|
+
// src/plugins/schedule/utils.ts
|
|
3335
|
+
function extractScheduleMetadata(handlers) {
|
|
3336
|
+
const result = /* @__PURE__ */ new Map();
|
|
3337
|
+
const scheduleHandlers = handlers.filter(
|
|
3338
|
+
(handler) => handler.type === "schedule"
|
|
3339
|
+
);
|
|
3340
|
+
for (const handler of scheduleHandlers) {
|
|
3341
|
+
const moduleClass = handler.module;
|
|
3342
|
+
const methodName = handler.methodName;
|
|
3343
|
+
const options = handler.options || {};
|
|
3344
|
+
if (!result.has(moduleClass)) {
|
|
3345
|
+
result.set(moduleClass, /* @__PURE__ */ new Map());
|
|
3186
3346
|
}
|
|
3187
|
-
|
|
3347
|
+
const moduleMetadata = result.get(moduleClass);
|
|
3348
|
+
moduleMetadata.set(methodName, {
|
|
3349
|
+
name: methodName,
|
|
3350
|
+
interval: options.interval,
|
|
3351
|
+
mode: options.mode || "FIXED_RATE" /* FIXED_RATE */
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
return result;
|
|
3188
3355
|
}
|
|
3189
3356
|
|
|
3190
|
-
// src/plugins/
|
|
3191
|
-
var
|
|
3192
|
-
name = "
|
|
3193
|
-
priority =
|
|
3194
|
-
//
|
|
3195
|
-
engine
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
isShuttingDown = false;
|
|
3201
|
-
// 停机超时定时器
|
|
3202
|
-
shutdownTimer = null;
|
|
3203
|
-
// 信号监听器(用于清理)
|
|
3204
|
-
signalListeners = /* @__PURE__ */ new Map();
|
|
3357
|
+
// src/plugins/schedule/plugin.ts
|
|
3358
|
+
var SchedulePlugin = class {
|
|
3359
|
+
name = "schedule-plugin";
|
|
3360
|
+
priority = 300 /* BUSINESS */;
|
|
3361
|
+
// 业务逻辑优先级
|
|
3362
|
+
engine;
|
|
3363
|
+
scheduler = null;
|
|
3364
|
+
etcdClient = null;
|
|
3365
|
+
scheduleHandlers = [];
|
|
3366
|
+
useMockEtcd = false;
|
|
3205
3367
|
constructor(options) {
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
}
|
|
3368
|
+
if (options?.useMockEtcd) {
|
|
3369
|
+
this.useMockEtcd = true;
|
|
3370
|
+
this.etcdClient = new MockEtcd3();
|
|
3371
|
+
logger_default.info("SchedulePlugin: Using MockEtcd3 for local development/testing");
|
|
3372
|
+
} else if (options?.etcdClient) {
|
|
3373
|
+
this.etcdClient = options.etcdClient;
|
|
3374
|
+
}
|
|
3211
3375
|
}
|
|
3212
3376
|
/**
|
|
3213
3377
|
* 引擎初始化钩子
|
|
3214
3378
|
*/
|
|
3215
3379
|
onInit(engine) {
|
|
3216
3380
|
this.engine = engine;
|
|
3217
|
-
logger_default.info("
|
|
3381
|
+
logger_default.info("SchedulePlugin initialized");
|
|
3218
3382
|
}
|
|
3219
3383
|
/**
|
|
3220
|
-
* Handler
|
|
3384
|
+
* Handler加载钩子:收集所有调度任务
|
|
3221
3385
|
*/
|
|
3222
3386
|
onHandlerLoad(handlers) {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
for (const handler of handlers) {
|
|
3227
|
-
handler.wrap(async (next, instance, ...args) => {
|
|
3228
|
-
if (this.isShuttingDown) {
|
|
3229
|
-
throw new Error("Service is shutting down, new requests are not accepted");
|
|
3230
|
-
}
|
|
3231
|
-
this.incrementActiveHandlers();
|
|
3232
|
-
try {
|
|
3233
|
-
const result = await next();
|
|
3234
|
-
return result;
|
|
3235
|
-
} catch (error) {
|
|
3236
|
-
throw error;
|
|
3237
|
-
} finally {
|
|
3238
|
-
this.decrementActiveHandlers();
|
|
3239
|
-
}
|
|
3240
|
-
});
|
|
3241
|
-
}
|
|
3387
|
+
this.scheduleHandlers = handlers.filter(
|
|
3388
|
+
(handler) => handler.type === "schedule"
|
|
3389
|
+
);
|
|
3242
3390
|
logger_default.info(
|
|
3243
|
-
`
|
|
3391
|
+
`SchedulePlugin: Found ${this.scheduleHandlers.length} schedule handler(s)`
|
|
3244
3392
|
);
|
|
3245
3393
|
}
|
|
3246
3394
|
/**
|
|
3247
|
-
*
|
|
3395
|
+
* 引擎启动后钩子:启动所有调度任务
|
|
3248
3396
|
*/
|
|
3249
3397
|
async onAfterStart(engine) {
|
|
3250
|
-
if (!this.
|
|
3398
|
+
if (!this.etcdClient) {
|
|
3399
|
+
logger_default.warn(
|
|
3400
|
+
"SchedulePlugin: etcdClient not configured and useMockEtcd is false, schedule tasks will not start"
|
|
3401
|
+
);
|
|
3251
3402
|
return;
|
|
3252
3403
|
}
|
|
3253
|
-
this.
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
// Ctrl+Break (Windows)
|
|
3270
|
-
];
|
|
3271
|
-
for (const signal of signals) {
|
|
3272
|
-
if (process.platform === "win32" && signal === "SIGTERM") {
|
|
3404
|
+
this.scheduler = new Scheduler(this.etcdClient);
|
|
3405
|
+
const modules = engine.getModules();
|
|
3406
|
+
if (!this.scheduleHandlers) {
|
|
3407
|
+
logger_default.warn(
|
|
3408
|
+
"SchedulePlugin: No schedule handlers found, schedule tasks will not start"
|
|
3409
|
+
);
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
const scheduleMetadataMap = extractScheduleMetadata(this.scheduleHandlers);
|
|
3413
|
+
const serviceId = `${engine.options.name}-${Date.now()}-${Math.random()}`;
|
|
3414
|
+
for (const moduleMetadata of modules) {
|
|
3415
|
+
const moduleClass = moduleMetadata.clazz;
|
|
3416
|
+
const moduleName = moduleMetadata.name;
|
|
3417
|
+
const moduleInstance = engine.get(moduleClass);
|
|
3418
|
+
const moduleScheduleMetadata = scheduleMetadataMap.get(moduleClass);
|
|
3419
|
+
if (!moduleScheduleMetadata || moduleScheduleMetadata.size === 0) {
|
|
3273
3420
|
continue;
|
|
3274
3421
|
}
|
|
3275
|
-
const
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3422
|
+
for (const [methodName, metadata] of moduleScheduleMetadata.entries()) {
|
|
3423
|
+
const method = moduleInstance[methodName];
|
|
3424
|
+
if (typeof method !== "function") {
|
|
3425
|
+
logger_default.warn(
|
|
3426
|
+
`SchedulePlugin: Method ${moduleName}.${methodName} is not a function, skipping`
|
|
3427
|
+
);
|
|
3428
|
+
continue;
|
|
3429
|
+
}
|
|
3430
|
+
const electionKey = `/schedule/${engine.options.name}/${moduleName}/${methodName}`;
|
|
3431
|
+
const taskServiceId = `${serviceId}-${moduleName}-${methodName}`;
|
|
3432
|
+
try {
|
|
3433
|
+
await this.scheduler.startSchedule(
|
|
3434
|
+
taskServiceId,
|
|
3435
|
+
moduleName,
|
|
3436
|
+
methodName,
|
|
3437
|
+
electionKey,
|
|
3438
|
+
metadata,
|
|
3439
|
+
method.bind(moduleInstance)
|
|
3440
|
+
);
|
|
3441
|
+
logger_default.info(
|
|
3442
|
+
`SchedulePlugin: Started schedule task ${moduleName}.${methodName} (interval: ${metadata.interval}ms, mode: ${metadata.mode})`
|
|
3443
|
+
);
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
logger_default.error(
|
|
3446
|
+
`SchedulePlugin: Failed to start schedule task ${moduleName}.${methodName}:`,
|
|
3447
|
+
error
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3281
3451
|
}
|
|
3282
3452
|
}
|
|
3283
3453
|
/**
|
|
3284
|
-
*
|
|
3285
|
-
*/
|
|
3286
|
-
incrementActiveHandlers() {
|
|
3287
|
-
this.activeHandlers++;
|
|
3288
|
-
logger_default.debug(
|
|
3289
|
-
`GracefulShutdownPlugin: Active handlers: ${this.activeHandlers}`
|
|
3290
|
-
);
|
|
3291
|
-
}
|
|
3292
|
-
/**
|
|
3293
|
-
* 减少活跃处理器计数
|
|
3454
|
+
* 引擎销毁钩子:停止所有调度任务
|
|
3294
3455
|
*/
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
);
|
|
3304
|
-
this.completeShutdown();
|
|
3456
|
+
async onDestroy() {
|
|
3457
|
+
if (this.scheduler) {
|
|
3458
|
+
try {
|
|
3459
|
+
await this.scheduler.stop();
|
|
3460
|
+
logger_default.info("SchedulePlugin: All schedule tasks stopped");
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
logger_default.error("SchedulePlugin: Error stopping schedule tasks:", error);
|
|
3463
|
+
}
|
|
3305
3464
|
}
|
|
3306
3465
|
}
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3466
|
+
};
|
|
3467
|
+
async function formatCode(code) {
|
|
3468
|
+
try {
|
|
3469
|
+
return prettier__default.default.format(code, { parser: "typescript" });
|
|
3470
|
+
} catch {
|
|
3471
|
+
return code;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// src/plugins/client-code/generator.ts
|
|
3476
|
+
function getZodTypeString(schema, defaultOptional = false) {
|
|
3477
|
+
function processType(type) {
|
|
3478
|
+
if (!type) {
|
|
3479
|
+
return "unknown";
|
|
3313
3480
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
if (this.activeHandlers === 0) {
|
|
3319
|
-
logger_default.info(
|
|
3320
|
-
"GracefulShutdownPlugin: No active handlers, proceeding with shutdown immediately"
|
|
3321
|
-
);
|
|
3322
|
-
await this.completeShutdown();
|
|
3323
|
-
return;
|
|
3481
|
+
const def = type._def;
|
|
3482
|
+
const typeName = def.type;
|
|
3483
|
+
if (typeName === "nullable") {
|
|
3484
|
+
return `${processType(def.innerType)} | null`;
|
|
3324
3485
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
`GracefulShutdownPlugin: Shutdown timeout (${this.options.shutdownTimeout}ms) reached, forcing shutdown`
|
|
3328
|
-
);
|
|
3329
|
-
this.completeShutdown();
|
|
3330
|
-
}, this.options.shutdownTimeout);
|
|
3331
|
-
}
|
|
3332
|
-
/**
|
|
3333
|
-
* 完成停机流程
|
|
3334
|
-
*/
|
|
3335
|
-
async completeShutdown() {
|
|
3336
|
-
if (this.shutdownTimer) {
|
|
3337
|
-
clearTimeout(this.shutdownTimer);
|
|
3338
|
-
this.shutdownTimer = null;
|
|
3486
|
+
if (typeName === "optional") {
|
|
3487
|
+
return processType(def.innerType);
|
|
3339
3488
|
}
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3489
|
+
if (typeName === "pipe" && def.in) {
|
|
3490
|
+
return processType(def.in);
|
|
3491
|
+
}
|
|
3492
|
+
if (typeName === "effects" && def.schema) {
|
|
3493
|
+
return processType(def.schema);
|
|
3494
|
+
}
|
|
3495
|
+
switch (typeName) {
|
|
3496
|
+
case "string": {
|
|
3497
|
+
return "string";
|
|
3498
|
+
}
|
|
3499
|
+
case "number": {
|
|
3500
|
+
return "number";
|
|
3501
|
+
}
|
|
3502
|
+
case "bigint": {
|
|
3503
|
+
return "bigint";
|
|
3504
|
+
}
|
|
3505
|
+
case "boolean": {
|
|
3506
|
+
return "boolean";
|
|
3507
|
+
}
|
|
3508
|
+
case "array": {
|
|
3509
|
+
const elementType = processType(def.element);
|
|
3510
|
+
return `${elementType}[]`;
|
|
3511
|
+
}
|
|
3512
|
+
case "date": {
|
|
3513
|
+
return "Date";
|
|
3514
|
+
}
|
|
3515
|
+
case "object": {
|
|
3516
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
3517
|
+
const props = Object.entries(shape).map(([key, value]) => {
|
|
3518
|
+
if (key.includes("-")) {
|
|
3519
|
+
key = `'${key}'`;
|
|
3520
|
+
}
|
|
3521
|
+
const fieldDef = value._def;
|
|
3522
|
+
const fieldTypeName = fieldDef.type;
|
|
3523
|
+
const isOptional = fieldTypeName === "optional";
|
|
3524
|
+
const isDefault = defaultOptional && fieldTypeName === "default";
|
|
3525
|
+
const fieldType = processType(
|
|
3526
|
+
isOptional ? fieldDef.innerType : value
|
|
3527
|
+
);
|
|
3528
|
+
return `${key}${isOptional || isDefault ? "?" : ""}: ${fieldType}`;
|
|
3529
|
+
}).join("; ");
|
|
3530
|
+
return `{ ${props} }`;
|
|
3531
|
+
}
|
|
3532
|
+
case "union": {
|
|
3533
|
+
return def.options.map((opt) => processType(opt)).join(" | ");
|
|
3534
|
+
}
|
|
3535
|
+
case "null": {
|
|
3536
|
+
return "null";
|
|
3537
|
+
}
|
|
3538
|
+
case "promise": {
|
|
3539
|
+
return `Promise<${processType(def.type)}>`;
|
|
3540
|
+
}
|
|
3541
|
+
case "void": {
|
|
3542
|
+
return "void";
|
|
3543
|
+
}
|
|
3544
|
+
case "record": {
|
|
3545
|
+
if (def.valueType) {
|
|
3546
|
+
const keyType = def.keyType ? processType(def.keyType) : "string";
|
|
3547
|
+
return `Record<${keyType}, ${processType(def.valueType)}>`;
|
|
3548
|
+
} else if (def.keyType) {
|
|
3549
|
+
return `Record<string, ${processType(def.keyType)}>`;
|
|
3550
|
+
}
|
|
3551
|
+
return "Record<string, any>";
|
|
3552
|
+
}
|
|
3553
|
+
case "map": {
|
|
3554
|
+
return `Map<${processType(def.keyType)}, ${processType(
|
|
3555
|
+
def.valueType
|
|
3556
|
+
)}>`;
|
|
3557
|
+
}
|
|
3558
|
+
case "any": {
|
|
3559
|
+
return "any";
|
|
3560
|
+
}
|
|
3561
|
+
case "unknown": {
|
|
3562
|
+
return "unknown";
|
|
3563
|
+
}
|
|
3564
|
+
case "enum": {
|
|
3565
|
+
const values = def.entries ? Object.values(def.entries) : [];
|
|
3566
|
+
return "(" + values.map((opt) => `"${String(opt)}"`).join(" | ") + ")";
|
|
3567
|
+
}
|
|
3568
|
+
case "default": {
|
|
3569
|
+
return processType(def.innerType);
|
|
3570
|
+
}
|
|
3571
|
+
default: {
|
|
3572
|
+
if (type.safeParse(new Uint8Array()).success) {
|
|
3573
|
+
return "Uint8Array";
|
|
3574
|
+
}
|
|
3575
|
+
return "unknown";
|
|
3347
3576
|
}
|
|
3348
3577
|
}
|
|
3349
|
-
logger_default.info("GracefulShutdownPlugin: Process exiting");
|
|
3350
|
-
process.exit(0);
|
|
3351
|
-
}
|
|
3352
|
-
/**
|
|
3353
|
-
* 清理信号监听器
|
|
3354
|
-
*/
|
|
3355
|
-
cleanupSignalHandlers() {
|
|
3356
|
-
for (const [signal, handler] of this.signalListeners.entries()) {
|
|
3357
|
-
process.removeListener(signal, handler);
|
|
3358
|
-
}
|
|
3359
|
-
this.signalListeners.clear();
|
|
3360
3578
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3579
|
+
return processType(schema);
|
|
3580
|
+
}
|
|
3581
|
+
async function generateClientCode(modules) {
|
|
3582
|
+
const imports = [
|
|
3583
|
+
"// \u8FD9\u4E2A\u6587\u4EF6\u662F\u81EA\u52A8\u751F\u6210\u7684\uFF0C\u8BF7\u4E0D\u8981\u624B\u52A8\u4FEE\u6539",
|
|
3584
|
+
"",
|
|
3585
|
+
'import { MicroserviceClient as BaseMicroserviceClient } from "imean-service-client";',
|
|
3586
|
+
'export * from "imean-service-client";',
|
|
3587
|
+
""
|
|
3588
|
+
].join("\n");
|
|
3589
|
+
function toPascalCase(moduleName) {
|
|
3590
|
+
return moduleName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
3371
3591
|
}
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
getActiveHandlersCount() {
|
|
3376
|
-
return this.activeHandlers;
|
|
3592
|
+
function toCamelCase(moduleName) {
|
|
3593
|
+
const parts = moduleName.split("-");
|
|
3594
|
+
return parts[0] + parts.slice(1).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
3377
3595
|
}
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3596
|
+
const interfaces = Object.entries(modules).map(([name, module]) => {
|
|
3597
|
+
const methods = Object.entries(module.actions).map(([actionName, action]) => {
|
|
3598
|
+
if (!action.params) {
|
|
3599
|
+
throw new Error(`Missing params for action ${actionName}`);
|
|
3600
|
+
}
|
|
3601
|
+
const paramNames = action.paramNames || [];
|
|
3602
|
+
const params = action.params.map((param, index) => {
|
|
3603
|
+
const paramName = paramNames[index] || param.description || `arg${index}`;
|
|
3604
|
+
const paramDef = param._def;
|
|
3605
|
+
const isOptional = param.isOptional();
|
|
3606
|
+
const hasDefault = paramDef?.type === "default";
|
|
3607
|
+
return `${paramName}${isOptional || hasDefault ? "?" : ""}: ${getZodTypeString(
|
|
3608
|
+
param,
|
|
3609
|
+
true
|
|
3610
|
+
)}`;
|
|
3611
|
+
}).join(", ");
|
|
3612
|
+
const returnType = action.returns ? getZodTypeString(action.returns) : "void";
|
|
3613
|
+
return `
|
|
3614
|
+
/**
|
|
3615
|
+
* ${action.description || ""}
|
|
3616
|
+
*/
|
|
3617
|
+
${actionName}: (${params}) => Promise<${action.stream ? `AsyncIterable<${returnType}>` : returnType}>;`;
|
|
3618
|
+
}).join("\n ");
|
|
3619
|
+
const interfaceName = `${toPascalCase(name)}Module`;
|
|
3620
|
+
return `export interface ${interfaceName} {
|
|
3621
|
+
${methods}
|
|
3622
|
+
}`;
|
|
3623
|
+
}).join("\n\n");
|
|
3624
|
+
const clientClass = `export class MicroserviceClient extends BaseMicroserviceClient {
|
|
3625
|
+
constructor(options: any) {
|
|
3626
|
+
super(options);
|
|
3383
3627
|
}
|
|
3384
|
-
};
|
|
3385
|
-
|
|
3386
|
-
// src/plugins/schedule/types.ts
|
|
3387
|
-
var ScheduleMode = /* @__PURE__ */ ((ScheduleMode2) => {
|
|
3388
|
-
ScheduleMode2["FIXED_RATE"] = "FIXED_RATE";
|
|
3389
|
-
ScheduleMode2["FIXED_DELAY"] = "FIXED_DELAY";
|
|
3390
|
-
return ScheduleMode2;
|
|
3391
|
-
})(ScheduleMode || {});
|
|
3392
3628
|
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3629
|
+
${Object.entries(modules).map(([name, module]) => {
|
|
3630
|
+
const methods = Object.entries(module.actions).map(([actionName, action]) => {
|
|
3631
|
+
return `${actionName}: { idempotent: ${!!action.idempotence}, stream: ${!!action.stream} }`;
|
|
3632
|
+
}).join(",\n ");
|
|
3633
|
+
const propertyName = toCamelCase(name);
|
|
3634
|
+
const interfaceName = `${toPascalCase(name)}Module`;
|
|
3635
|
+
return `public readonly ${propertyName} = this.registerModule<${interfaceName}>("${name}", {
|
|
3636
|
+
${methods}
|
|
3637
|
+
});`;
|
|
3638
|
+
}).join("\n\n ")}
|
|
3639
|
+
}`;
|
|
3640
|
+
return await formatCode([imports, interfaces, clientClass].join("\n\n"));
|
|
3402
3641
|
}
|
|
3403
3642
|
|
|
3404
|
-
// src/plugins/
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
if (!this.elections.has(key)) {
|
|
3409
|
-
this.elections.set(key, new MockElection(key));
|
|
3410
|
-
}
|
|
3411
|
-
return this.elections.get(key);
|
|
3412
|
-
}
|
|
3413
|
-
getElection(key) {
|
|
3414
|
-
return this.elections.get(key);
|
|
3415
|
-
}
|
|
3416
|
-
clearElections() {
|
|
3417
|
-
this.elections.clear();
|
|
3418
|
-
}
|
|
3419
|
-
};
|
|
3420
|
-
var MockElection = class {
|
|
3421
|
-
constructor(key) {
|
|
3422
|
-
this.key = key;
|
|
3643
|
+
// src/plugins/client-code/utils.ts
|
|
3644
|
+
function extractParamNames(func) {
|
|
3645
|
+
if (!func || typeof func !== "function") {
|
|
3646
|
+
return [];
|
|
3423
3647
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
setTimeout(() => {
|
|
3432
|
-
observer.notifyChange(this.currentLeader);
|
|
3433
|
-
}, 0);
|
|
3648
|
+
try {
|
|
3649
|
+
const funcStr = func.toString();
|
|
3650
|
+
const match = funcStr.match(
|
|
3651
|
+
/(?:async\s+)?(?:function\s+\w*\s*)?\(([^)]*)\)|(?:async\s+)?\(([^)]*)\)\s*=>/
|
|
3652
|
+
);
|
|
3653
|
+
if (!match) {
|
|
3654
|
+
return [];
|
|
3434
3655
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
const campaign = new MockCampaign(candidate, this);
|
|
3439
|
-
this.campaigns.push(campaign);
|
|
3440
|
-
if (!this.currentLeader) {
|
|
3441
|
-
setTimeout(() => {
|
|
3442
|
-
this.setLeader(candidate);
|
|
3443
|
-
}, 50);
|
|
3656
|
+
const paramsStr = match[1] || match[2] || "";
|
|
3657
|
+
if (!paramsStr.trim()) {
|
|
3658
|
+
return [];
|
|
3444
3659
|
}
|
|
3445
|
-
return
|
|
3660
|
+
return paramsStr.split(",").map((param) => {
|
|
3661
|
+
return param.replace(/\/\*.*?\*\//g, "").replace(/\/\/.*$/g, "").replace(/:\s*[^=,]+/g, "").replace(/\s*=\s*[^,]+/g, "").trim();
|
|
3662
|
+
}).filter((name) => name.length > 0);
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
return [];
|
|
3446
3665
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3666
|
+
}
|
|
3667
|
+
function convertHandlersToModuleInfo(handlers) {
|
|
3668
|
+
const modules = {};
|
|
3669
|
+
const actionHandlers = handlers.filter(
|
|
3670
|
+
(handler) => handler.type === "action"
|
|
3671
|
+
);
|
|
3672
|
+
for (const handler of actionHandlers) {
|
|
3673
|
+
const actionOptions = handler.options;
|
|
3674
|
+
const moduleClass = handler.module;
|
|
3675
|
+
const methodName = handler.methodName;
|
|
3676
|
+
const moduleName = moduleClass.name.toLowerCase().replace(/service$/, "");
|
|
3677
|
+
if (!modules[moduleName]) {
|
|
3678
|
+
modules[moduleName] = {
|
|
3679
|
+
name: moduleName,
|
|
3680
|
+
actions: {}
|
|
3681
|
+
};
|
|
3451
3682
|
}
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
return this.currentLeader;
|
|
3462
|
-
}
|
|
3463
|
-
};
|
|
3464
|
-
var MockObserver = class {
|
|
3465
|
-
constructor(election) {
|
|
3466
|
-
this.election = election;
|
|
3683
|
+
const paramNames = extractParamNames(handler.method);
|
|
3684
|
+
modules[moduleName].actions[methodName] = {
|
|
3685
|
+
description: actionOptions.description,
|
|
3686
|
+
params: actionOptions.params || [],
|
|
3687
|
+
returns: actionOptions.returns,
|
|
3688
|
+
stream: actionOptions.stream || false,
|
|
3689
|
+
idempotence: actionOptions.idempotence || false,
|
|
3690
|
+
paramNames
|
|
3691
|
+
};
|
|
3467
3692
|
}
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3693
|
+
return modules;
|
|
3694
|
+
}
|
|
3695
|
+
function convertHandlersToModuleInfoWithMetadata(handlers, getModuleMetadata) {
|
|
3696
|
+
const modules = {};
|
|
3697
|
+
const actionHandlers = handlers.filter(
|
|
3698
|
+
(handler) => handler.type === "action"
|
|
3699
|
+
);
|
|
3700
|
+
for (const handler of actionHandlers) {
|
|
3701
|
+
const actionOptions = handler.options;
|
|
3702
|
+
const moduleClass = handler.module;
|
|
3703
|
+
const methodName = handler.methodName;
|
|
3704
|
+
const moduleMetadata = getModuleMetadata(moduleClass);
|
|
3705
|
+
if (!moduleMetadata) {
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
const moduleName = moduleMetadata.name;
|
|
3709
|
+
if (!modules[moduleName]) {
|
|
3710
|
+
modules[moduleName] = {
|
|
3711
|
+
name: moduleName,
|
|
3712
|
+
actions: {}
|
|
3713
|
+
};
|
|
3472
3714
|
}
|
|
3715
|
+
const paramNames = extractParamNames(handler.method);
|
|
3716
|
+
modules[moduleName].actions[methodName] = {
|
|
3717
|
+
description: actionOptions.description,
|
|
3718
|
+
params: actionOptions.params || [],
|
|
3719
|
+
returns: actionOptions.returns,
|
|
3720
|
+
stream: actionOptions.stream || false,
|
|
3721
|
+
idempotence: actionOptions.idempotence || false,
|
|
3722
|
+
paramNames
|
|
3723
|
+
};
|
|
3473
3724
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3725
|
+
return modules;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// src/plugins/client-code/plugin.ts
|
|
3729
|
+
var ClientCodePlugin = class {
|
|
3730
|
+
name = "client-code-plugin";
|
|
3731
|
+
priority = 1e3 /* ROUTE */;
|
|
3732
|
+
// 路由插件优先级,在 ActionPlugin 之后
|
|
3733
|
+
engine;
|
|
3734
|
+
actionHandlers = [];
|
|
3735
|
+
generatedCode = null;
|
|
3736
|
+
clientSavePath;
|
|
3737
|
+
constructor(options) {
|
|
3738
|
+
this.clientSavePath = options?.clientSavePath;
|
|
3478
3739
|
}
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
this.
|
|
3740
|
+
/**
|
|
3741
|
+
* 引擎初始化钩子
|
|
3742
|
+
*/
|
|
3743
|
+
onInit(engine) {
|
|
3744
|
+
this.engine = engine;
|
|
3745
|
+
logger_default.info("ClientCodePlugin initialized");
|
|
3484
3746
|
}
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3747
|
+
/**
|
|
3748
|
+
* Handler加载钩子:收集所有 Action handlers
|
|
3749
|
+
*/
|
|
3750
|
+
onHandlerLoad(handlers) {
|
|
3751
|
+
const actionHandlers = handlers.filter(
|
|
3752
|
+
(handler) => handler.type === "action"
|
|
3753
|
+
);
|
|
3754
|
+
this.actionHandlers = actionHandlers;
|
|
3755
|
+
logger_default.info(
|
|
3756
|
+
`ClientCodePlugin collected ${actionHandlers.length} action handler(s)`
|
|
3757
|
+
);
|
|
3494
3758
|
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3759
|
+
/**
|
|
3760
|
+
* 引擎启动后钩子:注册客户端代码下载路由
|
|
3761
|
+
*/
|
|
3762
|
+
async onAfterStart(engine) {
|
|
3763
|
+
await this.generateCode();
|
|
3764
|
+
const prefix = engine.options.prefix || "";
|
|
3765
|
+
const clientPath = prefix ? `${prefix}/client.ts` : "/client.ts";
|
|
3766
|
+
const hono = engine.getHono();
|
|
3767
|
+
hono.get(clientPath, async (ctx) => {
|
|
3768
|
+
if (!this.generatedCode) {
|
|
3769
|
+
await this.generateCode();
|
|
3499
3770
|
}
|
|
3500
|
-
|
|
3771
|
+
return ctx.text(this.generatedCode || "", 200, {
|
|
3772
|
+
"Content-Type": "text/typescript; charset=utf-8",
|
|
3773
|
+
"Content-Disposition": `attachment; filename="client.ts"`
|
|
3774
|
+
});
|
|
3775
|
+
});
|
|
3776
|
+
logger_default.info(`Client code available at ${clientPath}`);
|
|
3501
3777
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3778
|
+
/**
|
|
3779
|
+
* 生成客户端代码
|
|
3780
|
+
*/
|
|
3781
|
+
async generateCode() {
|
|
3782
|
+
try {
|
|
3783
|
+
const getModuleMetadata = (moduleClass) => {
|
|
3784
|
+
const modules2 = this.engine.getModules();
|
|
3785
|
+
const moduleMetadata = modules2.find((m) => m.clazz === moduleClass);
|
|
3786
|
+
return moduleMetadata ? { name: moduleMetadata.name } : void 0;
|
|
3787
|
+
};
|
|
3788
|
+
const modules = convertHandlersToModuleInfoWithMetadata(
|
|
3789
|
+
this.actionHandlers,
|
|
3790
|
+
getModuleMetadata
|
|
3791
|
+
);
|
|
3792
|
+
this.generatedCode = await generateClientCode(modules);
|
|
3793
|
+
logger_default.debug(
|
|
3794
|
+
`Generated client code for ${Object.keys(modules).length} module(s)`
|
|
3795
|
+
);
|
|
3796
|
+
if (this.clientSavePath && this.generatedCode) {
|
|
3797
|
+
await this.saveCodeToFile(this.clientSavePath, this.generatedCode);
|
|
3798
|
+
}
|
|
3799
|
+
} catch (error) {
|
|
3800
|
+
logger_default.error("Failed to generate client code", error);
|
|
3801
|
+
this.generatedCode = "// Error: Failed to generate client code";
|
|
3505
3802
|
}
|
|
3506
3803
|
}
|
|
3507
|
-
|
|
3508
|
-
|
|
3804
|
+
/**
|
|
3805
|
+
* 保存代码到文件
|
|
3806
|
+
*/
|
|
3807
|
+
async saveCodeToFile(path$1, code) {
|
|
3808
|
+
try {
|
|
3809
|
+
const dir = path.dirname(path$1);
|
|
3810
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
3811
|
+
await fs.promises.writeFile(path$1, code, "utf-8");
|
|
3812
|
+
logger_default.info(`Client code saved to ${path$1}`);
|
|
3813
|
+
} catch (error) {
|
|
3814
|
+
logger_default.error(`Failed to save client code to ${path$1}`, error);
|
|
3815
|
+
}
|
|
3509
3816
|
}
|
|
3510
3817
|
};
|
|
3511
|
-
|
|
3512
|
-
//
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3818
|
+
var Microservice3 = class {
|
|
3819
|
+
// 存储已注册的模块
|
|
3820
|
+
modules = [];
|
|
3821
|
+
// 存储已注册的插件(按注册顺序)
|
|
3822
|
+
plugins = [];
|
|
3823
|
+
// 插件注册表(以name为键,用于覆盖逻辑)
|
|
3824
|
+
pluginRegistry = /* @__PURE__ */ new Map();
|
|
3825
|
+
// 模块实例缓存(单例管理)
|
|
3826
|
+
moduleInstances = /* @__PURE__ */ new Map();
|
|
3827
|
+
// 引擎配置(冻结,只读)
|
|
3828
|
+
options;
|
|
3829
|
+
// 是否已启动
|
|
3830
|
+
started = false;
|
|
3831
|
+
// 模块元数据键(用于通过双向访问查找模块)
|
|
3832
|
+
moduleMetadataKey;
|
|
3833
|
+
// 实际使用的端口(启动后设置)
|
|
3834
|
+
actualPort = null;
|
|
3835
|
+
// Hono 实例(由引擎统一管理)
|
|
3836
|
+
hono = new hono.Hono();
|
|
3837
|
+
// HTTP 服务器实例
|
|
3838
|
+
server = null;
|
|
3839
|
+
constructor(options, moduleMetadataKey) {
|
|
3840
|
+
this.options = Object.freeze({
|
|
3841
|
+
hostname: "0.0.0.0",
|
|
3842
|
+
...options
|
|
3843
|
+
});
|
|
3844
|
+
this.moduleMetadataKey = moduleMetadataKey;
|
|
3518
3845
|
}
|
|
3519
|
-
campaigns = /* @__PURE__ */ new Map();
|
|
3520
|
-
timers = /* @__PURE__ */ new Map();
|
|
3521
|
-
isLeader = /* @__PURE__ */ new Map();
|
|
3522
3846
|
/**
|
|
3523
|
-
*
|
|
3847
|
+
* 获取 Hono 实例(供插件使用)
|
|
3524
3848
|
*/
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3849
|
+
getHono() {
|
|
3850
|
+
return this.hono;
|
|
3851
|
+
}
|
|
3852
|
+
/**
|
|
3853
|
+
* 内部注册插件方法(供构造函数和子类使用)
|
|
3854
|
+
* @protected
|
|
3855
|
+
*/
|
|
3856
|
+
registerPlugin(plugin) {
|
|
3857
|
+
if (!plugin.name || plugin.name.trim() === "") {
|
|
3858
|
+
throw new PluginNameRequiredError();
|
|
3859
|
+
}
|
|
3860
|
+
const existingPlugin = this.pluginRegistry.get(plugin.name);
|
|
3861
|
+
if (existingPlugin) {
|
|
3862
|
+
const oldIndex = this.plugins.indexOf(existingPlugin);
|
|
3863
|
+
if (oldIndex >= 0) {
|
|
3864
|
+
this.plugins.splice(oldIndex, 1);
|
|
3533
3865
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
this.
|
|
3537
|
-
|
|
3538
|
-
logger_default.error(`Error in campaign for ${moduleName}.${methodName}:`, error);
|
|
3539
|
-
});
|
|
3540
|
-
campaign.on("elected", () => {
|
|
3541
|
-
this.isLeader.set(serviceId, true);
|
|
3542
|
-
this.startTimer(serviceId, metadata, moduleName, method);
|
|
3543
|
-
logger_default.info(`become leader for ${moduleName}.${methodName}`);
|
|
3544
|
-
});
|
|
3866
|
+
logger_default.info(`Override plugin: ${plugin.name}`);
|
|
3867
|
+
}
|
|
3868
|
+
this.pluginRegistry.set(plugin.name, plugin);
|
|
3869
|
+
this.plugins.push(plugin);
|
|
3545
3870
|
}
|
|
3546
3871
|
/**
|
|
3547
|
-
*
|
|
3872
|
+
* 加载并注册所有模块(通过唯一的 key 查找)
|
|
3873
|
+
* 在引擎启动时调用,使用双向访问机制查找所有被装饰的类
|
|
3874
|
+
*
|
|
3875
|
+
* 注意:
|
|
3876
|
+
* - 每个引擎实例使用唯一的 moduleMetadataKey,实现隔离
|
|
3877
|
+
* - 模块类是静态的,可以被多个引擎实例共享
|
|
3878
|
+
* - 每个引擎实例会创建独立的模块实例,互不影响
|
|
3548
3879
|
*/
|
|
3549
|
-
|
|
3550
|
-
this.
|
|
3551
|
-
const
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
} catch (error) {
|
|
3564
|
-
span.setStatus({
|
|
3565
|
-
code: import_api.SpanStatusCode.ERROR,
|
|
3566
|
-
message: error.message
|
|
3567
|
-
});
|
|
3568
|
-
logger_default.error(
|
|
3569
|
-
`Error executing schedule task ${moduleName}.${metadata.name}:`,
|
|
3570
|
-
error
|
|
3571
|
-
);
|
|
3572
|
-
} finally {
|
|
3573
|
-
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
3574
|
-
span.end();
|
|
3575
|
-
}
|
|
3576
|
-
}
|
|
3577
|
-
);
|
|
3578
|
-
};
|
|
3579
|
-
if (metadata.mode === "FIXED_DELAY" /* FIXED_DELAY */) {
|
|
3580
|
-
const runTask = async () => {
|
|
3581
|
-
if (!this.isLeader.get(serviceId)) return;
|
|
3582
|
-
try {
|
|
3583
|
-
await wrappedMethod();
|
|
3584
|
-
} finally {
|
|
3585
|
-
this.timers.set(serviceId, setTimeout(runTask, metadata.interval));
|
|
3586
|
-
}
|
|
3880
|
+
loadModules() {
|
|
3881
|
+
this.modules = [];
|
|
3882
|
+
const moduleClasses = getClassesByKey(this.moduleMetadataKey);
|
|
3883
|
+
for (const moduleClass of moduleClasses) {
|
|
3884
|
+
const metadata = getClassMetadata(moduleClass, this.moduleMetadataKey);
|
|
3885
|
+
const moduleName = metadata.name || moduleClass.name;
|
|
3886
|
+
const existingModule = this.modules.find((m) => m.name === moduleName);
|
|
3887
|
+
if (existingModule) {
|
|
3888
|
+
throw new DuplicateModuleError(moduleName);
|
|
3889
|
+
}
|
|
3890
|
+
const moduleMetadata = {
|
|
3891
|
+
name: moduleName,
|
|
3892
|
+
clazz: moduleClass,
|
|
3893
|
+
options: metadata.options || {}
|
|
3587
3894
|
};
|
|
3588
|
-
|
|
3589
|
-
} else {
|
|
3590
|
-
this.timers.set(
|
|
3591
|
-
serviceId,
|
|
3592
|
-
setInterval(async () => {
|
|
3593
|
-
if (!this.isLeader.get(serviceId)) return;
|
|
3594
|
-
await wrappedMethod();
|
|
3595
|
-
}, metadata.interval)
|
|
3596
|
-
);
|
|
3895
|
+
this.modules.push(moduleMetadata);
|
|
3597
3896
|
}
|
|
3598
3897
|
}
|
|
3599
3898
|
/**
|
|
3600
|
-
*
|
|
3899
|
+
* 加载Handler元数据(平铺结构)
|
|
3900
|
+
* 每个装饰器都是独立的HandlerMetadata条目
|
|
3901
|
+
* 为每个方法创建包装链管理器,提供简单的 wrap API
|
|
3601
3902
|
*/
|
|
3602
|
-
|
|
3603
|
-
const
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
this.
|
|
3903
|
+
loadHandlerMetadata() {
|
|
3904
|
+
const handlers = [];
|
|
3905
|
+
const wrapperChains = /* @__PURE__ */ new Map();
|
|
3906
|
+
const originalMethods = /* @__PURE__ */ new Map();
|
|
3907
|
+
for (const module of this.modules) {
|
|
3908
|
+
if (!this.moduleInstances.has(module.clazz)) {
|
|
3909
|
+
this.get(module.clazz);
|
|
3910
|
+
}
|
|
3911
|
+
const allMetadata = getAllHandlerMetadata(module.clazz);
|
|
3912
|
+
for (const [methodName, metadataList] of allMetadata.entries()) {
|
|
3913
|
+
const method = module.clazz.prototype[methodName];
|
|
3914
|
+
const methodNameStr = String(methodName);
|
|
3915
|
+
const methodKey = `${module.clazz.name}.${methodNameStr}`;
|
|
3916
|
+
if (!originalMethods.has(methodKey)) {
|
|
3917
|
+
originalMethods.set(methodKey, method);
|
|
3918
|
+
wrapperChains.set(methodKey, []);
|
|
3919
|
+
}
|
|
3920
|
+
for (const meta of metadataList) {
|
|
3921
|
+
const chain = wrapperChains.get(methodKey);
|
|
3922
|
+
handlers.push({
|
|
3923
|
+
...meta,
|
|
3924
|
+
method: originalMethods.get(methodKey),
|
|
3925
|
+
methodName: methodNameStr,
|
|
3926
|
+
module: module.clazz,
|
|
3927
|
+
// 提供简单的 wrap API:插件只需要调用这个方法
|
|
3928
|
+
wrap: (wrapper) => {
|
|
3929
|
+
chain.push(wrapper);
|
|
3930
|
+
}
|
|
3931
|
+
});
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3608
3934
|
}
|
|
3935
|
+
this.__wrapperChains__ = wrapperChains;
|
|
3936
|
+
this.__originalMethods__ = originalMethods;
|
|
3937
|
+
return handlers;
|
|
3609
3938
|
}
|
|
3610
3939
|
/**
|
|
3611
|
-
*
|
|
3940
|
+
* 应用所有包装链到原型
|
|
3941
|
+
* 在所有插件执行完 onHandlerLoad 后调用
|
|
3612
3942
|
*/
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
for (const [
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3943
|
+
applyWrapperChains() {
|
|
3944
|
+
const wrapperChains = this.__wrapperChains__;
|
|
3945
|
+
const originalMethods = this.__originalMethods__;
|
|
3946
|
+
if (!wrapperChains || !originalMethods) return;
|
|
3947
|
+
for (const [methodKey, chain] of wrapperChains.entries()) {
|
|
3948
|
+
if (chain.length === 0) continue;
|
|
3949
|
+
const [moduleName, methodName] = methodKey.split(".");
|
|
3950
|
+
const module = this.modules.find((m) => m.clazz.name === moduleName);
|
|
3951
|
+
if (!module) continue;
|
|
3952
|
+
const originalMethod = originalMethods.get(methodKey);
|
|
3953
|
+
const prototype = module.clazz.prototype;
|
|
3954
|
+
let wrappedMethod = originalMethod;
|
|
3955
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
3956
|
+
const wrapper = chain[i];
|
|
3957
|
+
const next = wrappedMethod;
|
|
3958
|
+
wrappedMethod = async function(...args) {
|
|
3959
|
+
return wrapper(() => next.apply(this, args), this, ...args);
|
|
3960
|
+
};
|
|
3626
3961
|
}
|
|
3962
|
+
prototype[methodName] = wrappedMethod;
|
|
3627
3963
|
}
|
|
3628
3964
|
}
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
name: methodName,
|
|
3647
|
-
interval: options.interval,
|
|
3648
|
-
mode: options.mode || "FIXED_RATE" /* FIXED_RATE */
|
|
3965
|
+
/**
|
|
3966
|
+
* 寻找一个随机的可用端口
|
|
3967
|
+
*/
|
|
3968
|
+
getRandomPort(hostname) {
|
|
3969
|
+
return new Promise((resolve, reject) => {
|
|
3970
|
+
const server = net__namespace.createServer();
|
|
3971
|
+
server.unref();
|
|
3972
|
+
server.on("error", reject);
|
|
3973
|
+
server.listen(0, hostname, () => {
|
|
3974
|
+
const port = server.address().port;
|
|
3975
|
+
server.close((err) => {
|
|
3976
|
+
if (err) {
|
|
3977
|
+
return reject(err);
|
|
3978
|
+
}
|
|
3979
|
+
resolve(port);
|
|
3980
|
+
});
|
|
3981
|
+
});
|
|
3649
3982
|
});
|
|
3650
3983
|
}
|
|
3651
|
-
return result;
|
|
3652
|
-
}
|
|
3653
|
-
|
|
3654
|
-
// src/plugins/schedule/plugin.ts
|
|
3655
|
-
var SchedulePlugin = class {
|
|
3656
|
-
name = "schedule-plugin";
|
|
3657
|
-
priority = 300 /* BUSINESS */;
|
|
3658
|
-
// 业务逻辑优先级
|
|
3659
|
-
engine;
|
|
3660
|
-
scheduler = null;
|
|
3661
|
-
etcdClient = null;
|
|
3662
|
-
scheduleHandlers = [];
|
|
3663
|
-
useMockEtcd = false;
|
|
3664
|
-
constructor(options) {
|
|
3665
|
-
if (options?.useMockEtcd) {
|
|
3666
|
-
this.useMockEtcd = true;
|
|
3667
|
-
this.etcdClient = new MockEtcd3();
|
|
3668
|
-
logger_default.info("SchedulePlugin: Using MockEtcd3 for local development/testing");
|
|
3669
|
-
} else if (options?.etcdClient) {
|
|
3670
|
-
this.etcdClient = options.etcdClient;
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
3984
|
/**
|
|
3674
|
-
*
|
|
3985
|
+
* 启动引擎
|
|
3986
|
+
* @param requestedPort 启动端口(可选,默认0,表示随机端口)
|
|
3987
|
+
* @returns 实际使用的端口号
|
|
3675
3988
|
*/
|
|
3676
|
-
|
|
3677
|
-
this.
|
|
3678
|
-
|
|
3989
|
+
async start(requestedPort = 0) {
|
|
3990
|
+
if (this.started) {
|
|
3991
|
+
throw new Error("Engine is already started");
|
|
3992
|
+
}
|
|
3993
|
+
try {
|
|
3994
|
+
const { port, hostname } = await this.determinePort(requestedPort);
|
|
3995
|
+
this.initializeModulesAndPlugins();
|
|
3996
|
+
this.processHandlers();
|
|
3997
|
+
await this.executePluginStartHooks();
|
|
3998
|
+
this.startHttpServer(port, hostname);
|
|
3999
|
+
this.started = true;
|
|
4000
|
+
logger_default.info(
|
|
4001
|
+
`${this.options.name} v${this.options.version} started successfully on port ${this.actualPort}`
|
|
4002
|
+
);
|
|
4003
|
+
return this.actualPort;
|
|
4004
|
+
} catch (error) {
|
|
4005
|
+
logger_default.error("Failed to start engine", error);
|
|
4006
|
+
throw error;
|
|
4007
|
+
}
|
|
3679
4008
|
}
|
|
3680
4009
|
/**
|
|
3681
|
-
*
|
|
4010
|
+
* 确定实际使用的端口
|
|
3682
4011
|
*/
|
|
3683
|
-
|
|
3684
|
-
this.
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
);
|
|
4012
|
+
async determinePort(requestedPort) {
|
|
4013
|
+
const hostname = this.options.hostname || "0.0.0.0";
|
|
4014
|
+
if (requestedPort !== 0) {
|
|
4015
|
+
this.actualPort = requestedPort;
|
|
4016
|
+
return { port: this.actualPort, hostname };
|
|
4017
|
+
}
|
|
4018
|
+
this.actualPort = await this.getRandomPort(hostname);
|
|
4019
|
+
return { port: this.actualPort, hostname };
|
|
3690
4020
|
}
|
|
3691
4021
|
/**
|
|
3692
|
-
*
|
|
4022
|
+
* 初始化模块和插件
|
|
3693
4023
|
*/
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
const modules = engine.getModules();
|
|
3703
|
-
if (!this.scheduleHandlers) {
|
|
3704
|
-
logger_default.warn(
|
|
3705
|
-
"SchedulePlugin: No schedule handlers found, schedule tasks will not start"
|
|
4024
|
+
initializeModulesAndPlugins() {
|
|
4025
|
+
this.loadModules();
|
|
4026
|
+
this.executePluginHook("onInit", (plugin) => {
|
|
4027
|
+
plugin.onInit?.(this);
|
|
4028
|
+
});
|
|
4029
|
+
this.executePluginHook("onModuleLoad", (plugin) => {
|
|
4030
|
+
plugin.onModuleLoad?.(
|
|
4031
|
+
this.modules
|
|
3706
4032
|
);
|
|
3707
|
-
|
|
3708
|
-
}
|
|
3709
|
-
const scheduleMetadataMap = extractScheduleMetadata(this.scheduleHandlers);
|
|
3710
|
-
const serviceId = `${engine.options.name}-${Date.now()}-${Math.random()}`;
|
|
3711
|
-
for (const moduleMetadata of modules) {
|
|
3712
|
-
const moduleClass = moduleMetadata.clazz;
|
|
3713
|
-
const moduleName = moduleMetadata.name;
|
|
3714
|
-
const moduleInstance = engine.get(moduleClass);
|
|
3715
|
-
const moduleScheduleMetadata = scheduleMetadataMap.get(moduleClass);
|
|
3716
|
-
if (!moduleScheduleMetadata || moduleScheduleMetadata.size === 0) {
|
|
3717
|
-
continue;
|
|
3718
|
-
}
|
|
3719
|
-
for (const [methodName, metadata] of moduleScheduleMetadata.entries()) {
|
|
3720
|
-
const method = moduleInstance[methodName];
|
|
3721
|
-
if (typeof method !== "function") {
|
|
3722
|
-
logger_default.warn(
|
|
3723
|
-
`SchedulePlugin: Method ${moduleName}.${methodName} is not a function, skipping`
|
|
3724
|
-
);
|
|
3725
|
-
continue;
|
|
3726
|
-
}
|
|
3727
|
-
const electionKey = `/schedule/${engine.options.name}/${moduleName}/${methodName}`;
|
|
3728
|
-
const taskServiceId = `${serviceId}-${moduleName}-${methodName}`;
|
|
3729
|
-
try {
|
|
3730
|
-
await this.scheduler.startSchedule(
|
|
3731
|
-
taskServiceId,
|
|
3732
|
-
moduleName,
|
|
3733
|
-
methodName,
|
|
3734
|
-
electionKey,
|
|
3735
|
-
metadata,
|
|
3736
|
-
method.bind(moduleInstance)
|
|
3737
|
-
);
|
|
3738
|
-
logger_default.info(
|
|
3739
|
-
`SchedulePlugin: Started schedule task ${moduleName}.${methodName} (interval: ${metadata.interval}ms, mode: ${metadata.mode})`
|
|
3740
|
-
);
|
|
3741
|
-
} catch (error) {
|
|
3742
|
-
logger_default.error(
|
|
3743
|
-
`SchedulePlugin: Failed to start schedule task ${moduleName}.${methodName}:`,
|
|
3744
|
-
error
|
|
3745
|
-
);
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
3748
|
-
}
|
|
4033
|
+
});
|
|
3749
4034
|
}
|
|
3750
4035
|
/**
|
|
3751
|
-
*
|
|
4036
|
+
* 处理 Handler 元数据和插件包装
|
|
3752
4037
|
*/
|
|
3753
|
-
|
|
3754
|
-
|
|
4038
|
+
processHandlers() {
|
|
4039
|
+
const handlers = this.loadHandlerMetadata();
|
|
4040
|
+
const sortedPlugins = this.sortPluginsByPriority();
|
|
4041
|
+
const { wrapperPlugins, routePlugins } = this.separateWrapperAndRoutePlugins(sortedPlugins);
|
|
4042
|
+
this.executePluginHook(
|
|
4043
|
+
"onHandlerLoad",
|
|
4044
|
+
(plugin) => {
|
|
4045
|
+
plugin.onHandlerLoad?.(handlers);
|
|
4046
|
+
},
|
|
4047
|
+
wrapperPlugins
|
|
4048
|
+
);
|
|
4049
|
+
this.applyWrapperChains();
|
|
4050
|
+
this.executePluginHook(
|
|
4051
|
+
"onHandlerLoad",
|
|
4052
|
+
(plugin) => {
|
|
4053
|
+
plugin.onHandlerLoad?.(handlers);
|
|
4054
|
+
},
|
|
4055
|
+
routePlugins
|
|
4056
|
+
);
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* 执行插件启动钩子
|
|
4060
|
+
*/
|
|
4061
|
+
async executePluginStartHooks() {
|
|
4062
|
+
this.executePluginHook("onBeforeStart", (plugin) => {
|
|
4063
|
+
plugin.onBeforeStart?.(this);
|
|
4064
|
+
});
|
|
4065
|
+
for (const plugin of this.plugins) {
|
|
3755
4066
|
try {
|
|
3756
|
-
await
|
|
3757
|
-
logger_default.info("SchedulePlugin: All schedule tasks stopped");
|
|
4067
|
+
await plugin.onAfterStart?.(this);
|
|
3758
4068
|
} catch (error) {
|
|
3759
|
-
|
|
4069
|
+
throw new Error(
|
|
4070
|
+
`Plugin ${plugin.name} failed in onAfterStart: ${error instanceof Error ? error.message : String(error)}`
|
|
4071
|
+
);
|
|
3760
4072
|
}
|
|
3761
4073
|
}
|
|
4074
|
+
this.registerVersionRoute();
|
|
3762
4075
|
}
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
const
|
|
3772
|
-
|
|
3773
|
-
/(?:async\s+)?(?:function\s+\w*\s*)?\(([^)]*)\)|(?:async\s+)?\(([^)]*)\)\s*=>/
|
|
4076
|
+
/**
|
|
4077
|
+
* 注册版本路由(/prefix/version)
|
|
4078
|
+
* 用于健康检查和探针
|
|
4079
|
+
*/
|
|
4080
|
+
registerVersionRoute() {
|
|
4081
|
+
const prefix = this.options.prefix || "";
|
|
4082
|
+
const versionPath = prefix ? `${prefix}` : "/";
|
|
4083
|
+
const existingRoutes = this.hono.routes;
|
|
4084
|
+
const routeExists = existingRoutes.some(
|
|
4085
|
+
(route) => route.path === versionPath && route.method === "GET"
|
|
3774
4086
|
);
|
|
3775
|
-
if (
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
return [];
|
|
3781
|
-
}
|
|
3782
|
-
return paramsStr.split(",").map((param) => {
|
|
3783
|
-
return param.replace(/\/\*.*?\*\//g, "").replace(/\/\/.*$/g, "").replace(/:\s*[^=,]+/g, "").replace(/\s*=\s*[^,]+/g, "").trim();
|
|
3784
|
-
}).filter((name) => name.length > 0);
|
|
3785
|
-
} catch (error) {
|
|
3786
|
-
return [];
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
function convertHandlersToModuleInfo(handlers) {
|
|
3790
|
-
const modules = {};
|
|
3791
|
-
const actionHandlers = handlers.filter(
|
|
3792
|
-
(handler) => handler.type === "action"
|
|
3793
|
-
);
|
|
3794
|
-
for (const handler of actionHandlers) {
|
|
3795
|
-
const actionOptions = handler.options;
|
|
3796
|
-
const moduleClass = handler.module;
|
|
3797
|
-
const methodName = handler.methodName;
|
|
3798
|
-
const moduleName = moduleClass.name.toLowerCase().replace(/service$/, "");
|
|
3799
|
-
if (!modules[moduleName]) {
|
|
3800
|
-
modules[moduleName] = {
|
|
3801
|
-
name: moduleName,
|
|
3802
|
-
actions: {}
|
|
3803
|
-
};
|
|
3804
|
-
}
|
|
3805
|
-
const paramNames = extractParamNames(handler.method);
|
|
3806
|
-
modules[moduleName].actions[methodName] = {
|
|
3807
|
-
description: actionOptions.description,
|
|
3808
|
-
params: actionOptions.params || [],
|
|
3809
|
-
returns: actionOptions.returns,
|
|
3810
|
-
stream: actionOptions.stream || false,
|
|
3811
|
-
idempotence: actionOptions.idempotence || false,
|
|
3812
|
-
paramNames
|
|
3813
|
-
};
|
|
3814
|
-
}
|
|
3815
|
-
return modules;
|
|
3816
|
-
}
|
|
3817
|
-
function convertHandlersToModuleInfoWithMetadata(handlers, getModuleMetadata) {
|
|
3818
|
-
const modules = {};
|
|
3819
|
-
const actionHandlers = handlers.filter(
|
|
3820
|
-
(handler) => handler.type === "action"
|
|
3821
|
-
);
|
|
3822
|
-
for (const handler of actionHandlers) {
|
|
3823
|
-
const actionOptions = handler.options;
|
|
3824
|
-
const moduleClass = handler.module;
|
|
3825
|
-
const methodName = handler.methodName;
|
|
3826
|
-
const moduleMetadata = getModuleMetadata(moduleClass);
|
|
3827
|
-
if (!moduleMetadata) {
|
|
3828
|
-
continue;
|
|
3829
|
-
}
|
|
3830
|
-
const moduleName = moduleMetadata.name;
|
|
3831
|
-
if (!modules[moduleName]) {
|
|
3832
|
-
modules[moduleName] = {
|
|
3833
|
-
name: moduleName,
|
|
3834
|
-
actions: {}
|
|
3835
|
-
};
|
|
3836
|
-
}
|
|
3837
|
-
const paramNames = extractParamNames(handler.method);
|
|
3838
|
-
modules[moduleName].actions[methodName] = {
|
|
3839
|
-
description: actionOptions.description,
|
|
3840
|
-
params: actionOptions.params || [],
|
|
3841
|
-
returns: actionOptions.returns,
|
|
3842
|
-
stream: actionOptions.stream || false,
|
|
3843
|
-
idempotence: actionOptions.idempotence || false,
|
|
3844
|
-
paramNames
|
|
3845
|
-
};
|
|
3846
|
-
}
|
|
3847
|
-
return modules;
|
|
3848
|
-
}
|
|
3849
|
-
async function formatCode(code) {
|
|
3850
|
-
try {
|
|
3851
|
-
return prettier__default.default.format(code, { parser: "typescript" });
|
|
3852
|
-
} catch {
|
|
3853
|
-
return code;
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
|
|
3857
|
-
// src/plugins/client-code/generator.ts
|
|
3858
|
-
function getZodTypeString(schema, defaultOptional = false) {
|
|
3859
|
-
function processType(type) {
|
|
3860
|
-
if (!type) {
|
|
3861
|
-
return "unknown";
|
|
3862
|
-
}
|
|
3863
|
-
const def = type._def;
|
|
3864
|
-
const typeName = def.type;
|
|
3865
|
-
if (typeName === "nullable") {
|
|
3866
|
-
return `${processType(def.innerType)} | null`;
|
|
3867
|
-
}
|
|
3868
|
-
if (typeName === "optional") {
|
|
3869
|
-
return processType(def.innerType);
|
|
3870
|
-
}
|
|
3871
|
-
if (typeName === "pipe" && def.in) {
|
|
3872
|
-
return processType(def.in);
|
|
3873
|
-
}
|
|
3874
|
-
if (typeName === "effects" && def.schema) {
|
|
3875
|
-
return processType(def.schema);
|
|
4087
|
+
if (routeExists) {
|
|
4088
|
+
logger_default.info(
|
|
4089
|
+
`Version route ${versionPath} already exists, skipping registration`
|
|
4090
|
+
);
|
|
4091
|
+
return;
|
|
3876
4092
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
return
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
}
|
|
3894
|
-
case "date": {
|
|
3895
|
-
return "Date";
|
|
3896
|
-
}
|
|
3897
|
-
case "object": {
|
|
3898
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
3899
|
-
const props = Object.entries(shape).map(([key, value]) => {
|
|
3900
|
-
if (key.includes("-")) {
|
|
3901
|
-
key = `'${key}'`;
|
|
3902
|
-
}
|
|
3903
|
-
const fieldDef = value._def;
|
|
3904
|
-
const fieldTypeName = fieldDef.type;
|
|
3905
|
-
const isOptional = fieldTypeName === "optional";
|
|
3906
|
-
const isDefault = defaultOptional && fieldTypeName === "default";
|
|
3907
|
-
const fieldType = processType(
|
|
3908
|
-
isOptional ? fieldDef.innerType : value
|
|
3909
|
-
);
|
|
3910
|
-
return `${key}${isOptional || isDefault ? "?" : ""}: ${fieldType}`;
|
|
3911
|
-
}).join("; ");
|
|
3912
|
-
return `{ ${props} }`;
|
|
3913
|
-
}
|
|
3914
|
-
case "union": {
|
|
3915
|
-
return def.options.map((opt) => processType(opt)).join(" | ");
|
|
3916
|
-
}
|
|
3917
|
-
case "null": {
|
|
3918
|
-
return "null";
|
|
3919
|
-
}
|
|
3920
|
-
case "promise": {
|
|
3921
|
-
return `Promise<${processType(def.type)}>`;
|
|
3922
|
-
}
|
|
3923
|
-
case "void": {
|
|
3924
|
-
return "void";
|
|
3925
|
-
}
|
|
3926
|
-
case "record": {
|
|
3927
|
-
if (def.valueType) {
|
|
3928
|
-
const keyType = def.keyType ? processType(def.keyType) : "string";
|
|
3929
|
-
return `Record<${keyType}, ${processType(def.valueType)}>`;
|
|
3930
|
-
} else if (def.keyType) {
|
|
3931
|
-
return `Record<string, ${processType(def.keyType)}>`;
|
|
3932
|
-
}
|
|
3933
|
-
return "Record<string, any>";
|
|
3934
|
-
}
|
|
3935
|
-
case "map": {
|
|
3936
|
-
return `Map<${processType(def.keyType)}, ${processType(
|
|
3937
|
-
def.valueType
|
|
3938
|
-
)}>`;
|
|
3939
|
-
}
|
|
3940
|
-
case "any": {
|
|
3941
|
-
return "any";
|
|
3942
|
-
}
|
|
3943
|
-
case "unknown": {
|
|
3944
|
-
return "unknown";
|
|
3945
|
-
}
|
|
3946
|
-
case "enum": {
|
|
3947
|
-
const values = def.entries ? Object.values(def.entries) : [];
|
|
3948
|
-
return "(" + values.map((opt) => `"${String(opt)}"`).join(" | ") + ")";
|
|
3949
|
-
}
|
|
3950
|
-
case "default": {
|
|
3951
|
-
return processType(def.innerType);
|
|
3952
|
-
}
|
|
3953
|
-
default: {
|
|
3954
|
-
if (type.safeParse(new Uint8Array()).success) {
|
|
3955
|
-
return "Uint8Array";
|
|
3956
|
-
}
|
|
3957
|
-
return "unknown";
|
|
4093
|
+
try {
|
|
4094
|
+
this.hono.get(versionPath, async (ctx) => {
|
|
4095
|
+
return ctx.json({
|
|
4096
|
+
name: this.options.name,
|
|
4097
|
+
version: this.options.version,
|
|
4098
|
+
status: "running"
|
|
4099
|
+
});
|
|
4100
|
+
});
|
|
4101
|
+
logger_default.info(`Registered version route: GET ${versionPath}`);
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
if (error instanceof Error && error.message.includes("matcher is already built")) {
|
|
4104
|
+
logger_default.warn(
|
|
4105
|
+
`Cannot register version route ${versionPath}: route matcher is already built. If you have already registered a route at ${versionPath}, it will be used instead.`
|
|
4106
|
+
);
|
|
4107
|
+
} else {
|
|
4108
|
+
throw error;
|
|
3958
4109
|
}
|
|
3959
4110
|
}
|
|
3960
4111
|
}
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
function toPascalCase(moduleName) {
|
|
3972
|
-
return moduleName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
3973
|
-
}
|
|
3974
|
-
function toCamelCase(moduleName) {
|
|
3975
|
-
const parts = moduleName.split("-");
|
|
3976
|
-
return parts[0] + parts.slice(1).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
4112
|
+
/**
|
|
4113
|
+
* 启动 HTTP 服务器
|
|
4114
|
+
*/
|
|
4115
|
+
startHttpServer(port, hostname) {
|
|
4116
|
+
this.server = nodeServer.serve({
|
|
4117
|
+
fetch: this.hono.fetch,
|
|
4118
|
+
port,
|
|
4119
|
+
hostname
|
|
4120
|
+
});
|
|
4121
|
+
logger_default.info(`HTTP server started on http://${hostname}:${port}`);
|
|
3977
4122
|
}
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4123
|
+
/**
|
|
4124
|
+
* 按优先级排序插件
|
|
4125
|
+
*/
|
|
4126
|
+
sortPluginsByPriority() {
|
|
4127
|
+
return [...this.plugins].sort((a, b) => {
|
|
4128
|
+
const priorityA = this.getPluginPriority(a);
|
|
4129
|
+
const priorityB = this.getPluginPriority(b);
|
|
4130
|
+
if (priorityA !== priorityB) {
|
|
4131
|
+
return priorityA - priorityB;
|
|
3982
4132
|
}
|
|
3983
|
-
const
|
|
3984
|
-
const
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
const isOptional = param.isOptional();
|
|
3988
|
-
const hasDefault = paramDef?.type === "default";
|
|
3989
|
-
return `${paramName}${isOptional || hasDefault ? "?" : ""}: ${getZodTypeString(
|
|
3990
|
-
param,
|
|
3991
|
-
true
|
|
3992
|
-
)}`;
|
|
3993
|
-
}).join(", ");
|
|
3994
|
-
const returnType = action.returns ? getZodTypeString(action.returns) : "void";
|
|
3995
|
-
return `
|
|
3996
|
-
/**
|
|
3997
|
-
* ${action.description || ""}
|
|
3998
|
-
*/
|
|
3999
|
-
${actionName}: (${params}) => Promise<${action.stream ? `AsyncIterable<${returnType}>` : returnType}>;`;
|
|
4000
|
-
}).join("\n ");
|
|
4001
|
-
const interfaceName = `${toPascalCase(name)}Module`;
|
|
4002
|
-
return `export interface ${interfaceName} {
|
|
4003
|
-
${methods}
|
|
4004
|
-
}`;
|
|
4005
|
-
}).join("\n\n");
|
|
4006
|
-
const clientClass = `export class MicroserviceClient extends BaseMicroserviceClient {
|
|
4007
|
-
constructor(options: any) {
|
|
4008
|
-
super(options);
|
|
4009
|
-
}
|
|
4010
|
-
|
|
4011
|
-
${Object.entries(modules).map(([name, module]) => {
|
|
4012
|
-
const methods = Object.entries(module.actions).map(([actionName, action]) => {
|
|
4013
|
-
return `${actionName}: { idempotent: ${!!action.idempotence}, stream: ${!!action.stream} }`;
|
|
4014
|
-
}).join(",\n ");
|
|
4015
|
-
const propertyName = toCamelCase(name);
|
|
4016
|
-
const interfaceName = `${toPascalCase(name)}Module`;
|
|
4017
|
-
return `public readonly ${propertyName} = this.registerModule<${interfaceName}>("${name}", {
|
|
4018
|
-
${methods}
|
|
4019
|
-
});`;
|
|
4020
|
-
}).join("\n\n ")}
|
|
4021
|
-
}`;
|
|
4022
|
-
return await formatCode([imports, interfaces, clientClass].join("\n\n"));
|
|
4023
|
-
}
|
|
4024
|
-
|
|
4025
|
-
// src/plugins/client-code/plugin.ts
|
|
4026
|
-
var ClientCodePlugin = class {
|
|
4027
|
-
name = "client-code-plugin";
|
|
4028
|
-
priority = 1e3 /* ROUTE */;
|
|
4029
|
-
// 路由插件优先级,在 ActionPlugin 之后
|
|
4030
|
-
engine;
|
|
4031
|
-
actionHandlers = [];
|
|
4032
|
-
generatedCode = null;
|
|
4033
|
-
clientSavePath;
|
|
4034
|
-
constructor(options) {
|
|
4035
|
-
this.clientSavePath = options?.clientSavePath;
|
|
4133
|
+
const indexA = this.plugins.indexOf(a);
|
|
4134
|
+
const indexB = this.plugins.indexOf(b);
|
|
4135
|
+
return indexA - indexB;
|
|
4136
|
+
});
|
|
4036
4137
|
}
|
|
4037
4138
|
/**
|
|
4038
|
-
*
|
|
4139
|
+
* 获取插件优先级
|
|
4039
4140
|
*/
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
logger_default.info("ClientCodePlugin initialized");
|
|
4141
|
+
getPluginPriority(plugin) {
|
|
4142
|
+
return plugin.priority ?? 300 /* BUSINESS */;
|
|
4043
4143
|
}
|
|
4044
4144
|
/**
|
|
4045
|
-
*
|
|
4145
|
+
* 分离包装插件和路由插件
|
|
4046
4146
|
*/
|
|
4047
|
-
|
|
4048
|
-
const
|
|
4049
|
-
|
|
4050
|
-
)
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4147
|
+
separateWrapperAndRoutePlugins(sortedPlugins) {
|
|
4148
|
+
const wrapperPlugins = [];
|
|
4149
|
+
const routePlugins = [];
|
|
4150
|
+
for (const plugin of sortedPlugins) {
|
|
4151
|
+
const priority = this.getPluginPriority(plugin);
|
|
4152
|
+
if (priority === 1e3 /* ROUTE */) {
|
|
4153
|
+
routePlugins.push(plugin);
|
|
4154
|
+
} else {
|
|
4155
|
+
wrapperPlugins.push(plugin);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
return { wrapperPlugins, routePlugins };
|
|
4055
4159
|
}
|
|
4056
4160
|
/**
|
|
4057
|
-
*
|
|
4161
|
+
* 执行插件钩子(通用方法)
|
|
4058
4162
|
*/
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4163
|
+
executePluginHook(hookName, callback, plugins = this.plugins) {
|
|
4164
|
+
for (const plugin of plugins) {
|
|
4165
|
+
try {
|
|
4166
|
+
callback(plugin);
|
|
4167
|
+
} catch (error) {
|
|
4168
|
+
throw new Error(
|
|
4169
|
+
`Plugin ${plugin.name} failed in ${hookName}: ${error instanceof Error ? error.message : String(error)}`
|
|
4170
|
+
);
|
|
4067
4171
|
}
|
|
4068
|
-
|
|
4069
|
-
"Content-Type": "text/typescript; charset=utf-8",
|
|
4070
|
-
"Content-Disposition": `attachment; filename="client.ts"`
|
|
4071
|
-
});
|
|
4072
|
-
});
|
|
4073
|
-
logger_default.info(`Client code available at ${clientPath}`);
|
|
4172
|
+
}
|
|
4074
4173
|
}
|
|
4075
4174
|
/**
|
|
4076
|
-
*
|
|
4175
|
+
* 停止引擎
|
|
4077
4176
|
*/
|
|
4078
|
-
async
|
|
4177
|
+
async stop() {
|
|
4178
|
+
if (!this.started) {
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
4079
4181
|
try {
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4182
|
+
if (this.server) {
|
|
4183
|
+
if (typeof this.server.close === "function") {
|
|
4184
|
+
if (this.server.close.length > 0) {
|
|
4185
|
+
await new Promise((resolve, reject) => {
|
|
4186
|
+
this.server.close((err) => {
|
|
4187
|
+
if (err) {
|
|
4188
|
+
reject(err);
|
|
4189
|
+
} else {
|
|
4190
|
+
resolve();
|
|
4191
|
+
}
|
|
4192
|
+
});
|
|
4193
|
+
});
|
|
4194
|
+
} else {
|
|
4195
|
+
this.server.close();
|
|
4196
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
this.server = null;
|
|
4200
|
+
logger_default.info("HTTP server stopped");
|
|
4201
|
+
}
|
|
4202
|
+
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
4203
|
+
const plugin = this.plugins[i];
|
|
4204
|
+
try {
|
|
4205
|
+
await plugin.onDestroy?.();
|
|
4206
|
+
} catch (error) {
|
|
4207
|
+
logger_default.error(`Plugin ${plugin.name} failed in onDestroy`, error);
|
|
4208
|
+
}
|
|
4095
4209
|
}
|
|
4210
|
+
this.modules = [];
|
|
4211
|
+
this.moduleInstances.clear();
|
|
4212
|
+
this.started = false;
|
|
4213
|
+
this.actualPort = null;
|
|
4214
|
+
logger_default.info(`${this.options.name} stopped`);
|
|
4096
4215
|
} catch (error) {
|
|
4097
|
-
logger_default.error("Failed to
|
|
4098
|
-
|
|
4216
|
+
logger_default.error("Failed to stop engine", error);
|
|
4217
|
+
throw error;
|
|
4099
4218
|
}
|
|
4100
4219
|
}
|
|
4101
4220
|
/**
|
|
4102
|
-
*
|
|
4221
|
+
* 获取模块实例(单例)
|
|
4222
|
+
* @param moduleClass 模块类
|
|
4223
|
+
* @returns 模块实例
|
|
4103
4224
|
*/
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4225
|
+
get(moduleClass) {
|
|
4226
|
+
if (!this.moduleInstances.has(moduleClass)) {
|
|
4227
|
+
this.moduleInstances.set(moduleClass, new moduleClass());
|
|
4228
|
+
}
|
|
4229
|
+
return this.moduleInstances.get(moduleClass);
|
|
4230
|
+
}
|
|
4231
|
+
/**
|
|
4232
|
+
* 获取已注册的模块列表
|
|
4233
|
+
*/
|
|
4234
|
+
getModules() {
|
|
4235
|
+
if (this.modules.length === 0 && !this.started) {
|
|
4236
|
+
this.loadModules();
|
|
4112
4237
|
}
|
|
4238
|
+
return [...this.modules];
|
|
4239
|
+
}
|
|
4240
|
+
/**
|
|
4241
|
+
* 获取实际使用的端口
|
|
4242
|
+
*/
|
|
4243
|
+
getPort() {
|
|
4244
|
+
return this.actualPort;
|
|
4113
4245
|
}
|
|
4114
4246
|
};
|
|
4115
4247
|
|
|
@@ -4125,7 +4257,7 @@ var Factory = class {
|
|
|
4125
4257
|
const moduleMetadataKey = /* @__PURE__ */ Symbol.for(
|
|
4126
4258
|
`imean:moduleMetadata:${Date.now()}:${Math.random()}`
|
|
4127
4259
|
);
|
|
4128
|
-
class TypedMicroservice extends
|
|
4260
|
+
class TypedMicroservice extends Microservice3 {
|
|
4129
4261
|
constructor(options) {
|
|
4130
4262
|
super(options, moduleMetadataKey);
|
|
4131
4263
|
for (const plugin of plugins) {
|
|
@@ -4155,8 +4287,8 @@ var DEFAULT_TEST_OPTIONS = {
|
|
|
4155
4287
|
};
|
|
4156
4288
|
function createTestEngine(config) {
|
|
4157
4289
|
const factory = Factory.create(...config.plugins);
|
|
4158
|
-
const
|
|
4159
|
-
const engine = new
|
|
4290
|
+
const Microservice4 = factory.Microservice;
|
|
4291
|
+
const engine = new Microservice4({
|
|
4160
4292
|
...DEFAULT_TEST_OPTIONS,
|
|
4161
4293
|
...config.options
|
|
4162
4294
|
});
|
|
@@ -4174,6 +4306,10 @@ var Testing = {
|
|
|
4174
4306
|
wait
|
|
4175
4307
|
};
|
|
4176
4308
|
|
|
4309
|
+
Object.defineProperty(exports, "z", {
|
|
4310
|
+
enumerable: true,
|
|
4311
|
+
get: function () { return zod.z; }
|
|
4312
|
+
});
|
|
4177
4313
|
Object.defineProperty(exports, "Context", {
|
|
4178
4314
|
enumerable: true,
|
|
4179
4315
|
get: function () { return hono.Context; }
|
|
@@ -4182,10 +4318,6 @@ Object.defineProperty(exports, "MiddlewareHandler", {
|
|
|
4182
4318
|
enumerable: true,
|
|
4183
4319
|
get: function () { return hono.MiddlewareHandler; }
|
|
4184
4320
|
});
|
|
4185
|
-
Object.defineProperty(exports, "z", {
|
|
4186
|
-
enumerable: true,
|
|
4187
|
-
get: function () { return zod.z; }
|
|
4188
|
-
});
|
|
4189
4321
|
exports.Action = Action;
|
|
4190
4322
|
exports.ActionPlugin = ActionPlugin;
|
|
4191
4323
|
exports.BaseLayout = BaseLayout;
|
|
@@ -4199,7 +4331,6 @@ exports.GracefulShutdownPlugin = GracefulShutdownPlugin;
|
|
|
4199
4331
|
exports.Handler = Handler;
|
|
4200
4332
|
exports.HtmxLayout = HtmxLayout;
|
|
4201
4333
|
exports.MemoryCacheAdapter = MemoryCacheAdapter;
|
|
4202
|
-
exports.Microservice = Microservice;
|
|
4203
4334
|
exports.MockEtcd3 = MockEtcd3;
|
|
4204
4335
|
exports.ModuleConfigValidationError = ModuleConfigValidationError;
|
|
4205
4336
|
exports.Page = Page;
|
|
@@ -4212,6 +4343,8 @@ exports.Schedule = Schedule;
|
|
|
4212
4343
|
exports.ScheduleMode = ScheduleMode;
|
|
4213
4344
|
exports.SchedulePlugin = SchedulePlugin;
|
|
4214
4345
|
exports.Scheduler = Scheduler;
|
|
4346
|
+
exports.ServiceInfoCards = ServiceInfoCards;
|
|
4347
|
+
exports.ServiceStatusPage = ServiceStatusPage;
|
|
4215
4348
|
exports.Testing = Testing;
|
|
4216
4349
|
exports.convertHandlersToModuleInfo = convertHandlersToModuleInfo;
|
|
4217
4350
|
exports.convertHandlersToModuleInfoWithMetadata = convertHandlersToModuleInfoWithMetadata;
|