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