@dangao/bun-server 1.0.0 → 1.0.3
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/package.json +4 -2
- package/readme.md +163 -2
- package/src/auth/controller.ts +148 -0
- package/src/auth/decorators.ts +81 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/jwt.ts +169 -0
- package/src/auth/oauth2.ts +244 -0
- package/src/auth/types.ts +248 -0
- package/src/cache/cache-module.ts +67 -0
- package/src/cache/decorators.ts +202 -0
- package/src/cache/index.ts +27 -0
- package/src/cache/service.ts +151 -0
- package/src/cache/types.ts +420 -0
- package/src/config/config-module.ts +76 -0
- package/src/config/index.ts +8 -0
- package/src/config/service.ts +93 -0
- package/src/config/types.ts +27 -0
- package/src/controller/controller.ts +251 -0
- package/src/controller/decorators.ts +84 -0
- package/src/controller/index.ts +7 -0
- package/src/controller/metadata.ts +27 -0
- package/src/controller/param-binder.ts +157 -0
- package/src/core/application.ts +233 -0
- package/src/core/context.ts +228 -0
- package/src/core/index.ts +4 -0
- package/src/core/server.ts +128 -0
- package/src/core/types.ts +2 -0
- package/src/database/connection-manager.ts +239 -0
- package/src/database/connection-pool.ts +322 -0
- package/src/database/database-extension.ts +62 -0
- package/src/database/database-module.ts +115 -0
- package/src/database/health-indicator.ts +51 -0
- package/src/database/index.ts +47 -0
- package/src/database/orm/decorators.ts +155 -0
- package/src/database/orm/drizzle-repository.ts +39 -0
- package/src/database/orm/index.ts +23 -0
- package/src/database/orm/repository-decorator.ts +39 -0
- package/src/database/orm/repository.ts +103 -0
- package/src/database/orm/service.ts +49 -0
- package/src/database/orm/transaction-decorator.ts +45 -0
- package/src/database/orm/transaction-interceptor.ts +243 -0
- package/src/database/orm/transaction-manager.ts +276 -0
- package/src/database/orm/transaction-types.ts +140 -0
- package/src/database/orm/types.ts +99 -0
- package/src/database/service.ts +221 -0
- package/src/database/types.ts +171 -0
- package/src/di/container.ts +398 -0
- package/src/di/decorators.ts +228 -0
- package/src/di/index.ts +4 -0
- package/src/di/module-registry.ts +188 -0
- package/src/di/module.ts +65 -0
- package/src/di/types.ts +67 -0
- package/src/error/error-codes.ts +222 -0
- package/src/error/filter.ts +43 -0
- package/src/error/handler.ts +66 -0
- package/src/error/http-exception.ts +115 -0
- package/src/error/i18n.ts +217 -0
- package/src/error/index.ts +16 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/logger-extension.ts +31 -0
- package/src/extensions/logger-module.ts +69 -0
- package/src/extensions/types.ts +14 -0
- package/src/files/index.ts +5 -0
- package/src/files/static-middleware.ts +53 -0
- package/src/files/storage.ts +67 -0
- package/src/files/types.ts +33 -0
- package/src/files/upload-middleware.ts +45 -0
- package/src/health/controller.ts +76 -0
- package/src/health/health-module.ts +51 -0
- package/src/health/index.ts +12 -0
- package/src/health/types.ts +28 -0
- package/src/index.ts +270 -0
- package/src/metrics/collector.ts +209 -0
- package/src/metrics/controller.ts +40 -0
- package/src/metrics/index.ts +15 -0
- package/src/metrics/metrics-module.ts +58 -0
- package/src/metrics/middleware.ts +46 -0
- package/src/metrics/prometheus.ts +79 -0
- package/src/metrics/types.ts +103 -0
- package/src/middleware/builtin/cors.ts +60 -0
- package/src/middleware/builtin/error-handler.ts +90 -0
- package/src/middleware/builtin/file-upload.ts +42 -0
- package/src/middleware/builtin/index.ts +14 -0
- package/src/middleware/builtin/logger.ts +91 -0
- package/src/middleware/builtin/rate-limit.ts +252 -0
- package/src/middleware/builtin/static-file.ts +88 -0
- package/src/middleware/decorators.ts +91 -0
- package/src/middleware/index.ts +11 -0
- package/src/middleware/middleware.ts +13 -0
- package/src/middleware/pipeline.ts +93 -0
- package/src/queue/decorators.ts +110 -0
- package/src/queue/index.ts +26 -0
- package/src/queue/queue-module.ts +64 -0
- package/src/queue/service.ts +302 -0
- package/src/queue/types.ts +341 -0
- package/src/request/body-parser.ts +133 -0
- package/src/request/file-handler.ts +46 -0
- package/src/request/index.ts +5 -0
- package/src/request/request.ts +107 -0
- package/src/request/response.ts +150 -0
- package/src/router/decorators.ts +122 -0
- package/src/router/index.ts +6 -0
- package/src/router/registry.ts +98 -0
- package/src/router/route.ts +140 -0
- package/src/router/router.ts +241 -0
- package/src/router/types.ts +27 -0
- package/src/security/access-decision-manager.ts +34 -0
- package/src/security/authentication-manager.ts +47 -0
- package/src/security/context.ts +92 -0
- package/src/security/filter.ts +162 -0
- package/src/security/index.ts +8 -0
- package/src/security/providers/index.ts +3 -0
- package/src/security/providers/jwt-provider.ts +60 -0
- package/src/security/providers/oauth2-provider.ts +70 -0
- package/src/security/security-module.ts +145 -0
- package/src/security/types.ts +165 -0
- package/src/session/decorators.ts +45 -0
- package/src/session/index.ts +19 -0
- package/src/session/middleware.ts +143 -0
- package/src/session/service.ts +218 -0
- package/src/session/session-module.ts +69 -0
- package/src/session/types.ts +373 -0
- package/src/swagger/decorators.ts +133 -0
- package/src/swagger/generator.ts +234 -0
- package/src/swagger/index.ts +7 -0
- package/src/swagger/swagger-extension.ts +41 -0
- package/src/swagger/swagger-module.ts +83 -0
- package/src/swagger/types.ts +188 -0
- package/src/swagger/ui.ts +98 -0
- package/src/testing/harness.ts +96 -0
- package/src/validation/decorators.ts +95 -0
- package/src/validation/errors.ts +28 -0
- package/src/validation/index.ts +14 -0
- package/src/validation/types.ts +35 -0
- package/src/validation/validator.ts +63 -0
- package/src/websocket/decorators.ts +51 -0
- package/src/websocket/index.ts +12 -0
- package/src/websocket/registry.ts +133 -0
- package/tests/cache/cache-module.test.ts +212 -0
- package/tests/config/config-module.test.ts +151 -0
- package/tests/controller/controller.test.ts +189 -0
- package/tests/core/application.test.ts +57 -0
- package/tests/core/context-body.test.ts +44 -0
- package/tests/core/context.test.ts +86 -0
- package/tests/core/edge-cases.test.ts +432 -0
- package/tests/database/database-module.test.ts +385 -0
- package/tests/database/orm.test.ts +164 -0
- package/tests/database/postgres-mysql-integration.test.ts +395 -0
- package/tests/database/transaction.test.ts +238 -0
- package/tests/di/container.test.ts +264 -0
- package/tests/di/module.test.ts +128 -0
- package/tests/error/error-codes.test.ts +121 -0
- package/tests/error/error-handler.test.ts +68 -0
- package/tests/error/error-handling.test.ts +254 -0
- package/tests/error/http-exception.test.ts +37 -0
- package/tests/error/i18n-integration.test.ts +175 -0
- package/tests/extensions/logger-extension.test.ts +40 -0
- package/tests/files/static-middleware.test.ts +67 -0
- package/tests/files/upload-middleware.test.ts +43 -0
- package/tests/health/health-module.test.ts +116 -0
- package/tests/integration/application-router.test.ts +85 -0
- package/tests/integration/body-parsing.test.ts +88 -0
- package/tests/integration/cache-e2e.test.ts +114 -0
- package/tests/integration/oauth2-e2e.test.ts +615 -0
- package/tests/integration/session-e2e.test.ts +207 -0
- package/tests/metrics/metrics-module.test.ts +178 -0
- package/tests/middleware/builtin.test.ts +206 -0
- package/tests/middleware/file-upload.test.ts +41 -0
- package/tests/middleware/middleware.test.ts +120 -0
- package/tests/middleware/pipeline.test.ts +72 -0
- package/tests/middleware/rate-limit.test.ts +314 -0
- package/tests/middleware/static-file.test.ts +62 -0
- package/tests/perf/harness.test.ts +48 -0
- package/tests/perf/optimization.test.ts +183 -0
- package/tests/perf/regression.test.ts +120 -0
- package/tests/queue/queue-module.test.ts +217 -0
- package/tests/request/body-parser.test.ts +96 -0
- package/tests/request/response.test.ts +99 -0
- package/tests/router/decorators.test.ts +48 -0
- package/tests/router/registry.test.ts +51 -0
- package/tests/router/route.test.ts +71 -0
- package/tests/router/router-normalization.test.ts +106 -0
- package/tests/router/router.test.ts +133 -0
- package/tests/security/access-decision-manager.test.ts +84 -0
- package/tests/security/authentication-manager.test.ts +81 -0
- package/tests/security/context.test.ts +302 -0
- package/tests/security/filter.test.ts +225 -0
- package/tests/security/jwt-provider.test.ts +106 -0
- package/tests/security/oauth2-provider.test.ts +269 -0
- package/tests/security/security-module.test.ts +143 -0
- package/tests/session/session-module.test.ts +307 -0
- package/tests/stress/di-stress.test.ts +30 -0
- package/tests/swagger/decorators.test.ts +153 -0
- package/tests/swagger/generator.test.ts +202 -0
- package/tests/swagger/swagger-extension.test.ts +72 -0
- package/tests/swagger/swagger-module.test.ts +79 -0
- package/tests/utils/test-port.ts +10 -0
- package/tests/validation/controller-validation.test.ts +64 -0
- package/tests/validation/validation.test.ts +42 -0
- package/tests/websocket/gateway.test.ts +68 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
|
|
3
|
+
export interface BenchmarkResult {
|
|
4
|
+
name: string;
|
|
5
|
+
iterations: number;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
opsPerSecond: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface StressResult {
|
|
11
|
+
name: string;
|
|
12
|
+
iterations: number;
|
|
13
|
+
concurrency: number;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
errors: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 简单性能压测工具,方便在 bun:test 中快速基准测试组件
|
|
20
|
+
*/
|
|
21
|
+
export class PerformanceHarness {
|
|
22
|
+
public static async benchmark(
|
|
23
|
+
name: string,
|
|
24
|
+
iterations: number,
|
|
25
|
+
runner: (iteration: number) => void | Promise<void>,
|
|
26
|
+
): Promise<BenchmarkResult> {
|
|
27
|
+
if (iterations <= 0) {
|
|
28
|
+
throw new Error('iterations must be greater than 0');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const start = performance.now();
|
|
32
|
+
for (let i = 0; i < iterations; i++) {
|
|
33
|
+
await runner(i);
|
|
34
|
+
}
|
|
35
|
+
const durationMs = performance.now() - start;
|
|
36
|
+
const opsPerSecond = iterations / Math.max(durationMs / 1000, 0.0001);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name,
|
|
40
|
+
iterations,
|
|
41
|
+
durationMs,
|
|
42
|
+
opsPerSecond,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 简单压力测试运行器,支持并发执行任务并收集错误
|
|
49
|
+
*/
|
|
50
|
+
export class StressTester {
|
|
51
|
+
public static async run(
|
|
52
|
+
name: string,
|
|
53
|
+
iterations: number,
|
|
54
|
+
concurrency: number,
|
|
55
|
+
task: (iteration: number) => Promise<void>,
|
|
56
|
+
): Promise<StressResult> {
|
|
57
|
+
if (iterations <= 0) {
|
|
58
|
+
throw new Error('iterations must be greater than 0');
|
|
59
|
+
}
|
|
60
|
+
if (concurrency <= 0) {
|
|
61
|
+
throw new Error('concurrency must be greater than 0');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let next = 0;
|
|
65
|
+
let errors = 0;
|
|
66
|
+
const start = performance.now();
|
|
67
|
+
|
|
68
|
+
const worker = async () => {
|
|
69
|
+
while (true) {
|
|
70
|
+
const current = next;
|
|
71
|
+
next += 1;
|
|
72
|
+
if (current >= iterations) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await task(current);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
errors += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const workers = Array.from({ length: Math.min(concurrency, iterations) }, () => worker());
|
|
84
|
+
await Promise.all(workers);
|
|
85
|
+
|
|
86
|
+
const durationMs = performance.now() - start;
|
|
87
|
+
return {
|
|
88
|
+
name,
|
|
89
|
+
iterations,
|
|
90
|
+
concurrency: Math.min(concurrency, iterations),
|
|
91
|
+
durationMs,
|
|
92
|
+
errors,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import type { ValidationRuleDefinition, ValidationMetadata } from './types';
|
|
3
|
+
|
|
4
|
+
const VALIDATION_METADATA_KEY = Symbol('validation:param');
|
|
5
|
+
|
|
6
|
+
function getOrCreateMetadata(target: object, propertyKey: string | symbol): ValidationMetadata[] {
|
|
7
|
+
const existing = Reflect.getMetadata(VALIDATION_METADATA_KEY, target, propertyKey) as
|
|
8
|
+
| ValidationMetadata[]
|
|
9
|
+
| undefined;
|
|
10
|
+
if (existing) {
|
|
11
|
+
return existing;
|
|
12
|
+
}
|
|
13
|
+
const metadata: ValidationMetadata[] = [];
|
|
14
|
+
Reflect.defineMetadata(VALIDATION_METADATA_KEY, metadata, target, propertyKey);
|
|
15
|
+
return metadata;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 通用校验装饰器
|
|
20
|
+
* @param rules - 校验规则
|
|
21
|
+
*/
|
|
22
|
+
export function Validate(...rules: ValidationRuleDefinition[]): ParameterDecorator {
|
|
23
|
+
return (target, propertyKey, parameterIndex) => {
|
|
24
|
+
if (!propertyKey) {
|
|
25
|
+
throw new Error('@Validate decorator can only be used on methods');
|
|
26
|
+
}
|
|
27
|
+
if (!rules.length) {
|
|
28
|
+
throw new Error('@Validate requires at least one validation rule');
|
|
29
|
+
}
|
|
30
|
+
const metadata = getOrCreateMetadata(target, propertyKey);
|
|
31
|
+
let entry = metadata.find((item) => item.index === parameterIndex);
|
|
32
|
+
if (!entry) {
|
|
33
|
+
entry = { index: parameterIndex, rules: [] };
|
|
34
|
+
metadata.push(entry);
|
|
35
|
+
}
|
|
36
|
+
entry.rules.push(...rules);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RuleOption {
|
|
41
|
+
message?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function IsString(options: RuleOption = {}): ValidationRuleDefinition {
|
|
45
|
+
return {
|
|
46
|
+
name: 'isString',
|
|
47
|
+
message: options.message ?? '必须是字符串',
|
|
48
|
+
validate: (value) => typeof value === 'string',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function IsNumber(options: RuleOption = {}): ValidationRuleDefinition {
|
|
53
|
+
return {
|
|
54
|
+
name: 'isNumber',
|
|
55
|
+
message: options.message ?? '必须是数字',
|
|
56
|
+
validate: (value) => typeof value === 'number' && !Number.isNaN(value),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function IsEmail(options: RuleOption = {}): ValidationRuleDefinition {
|
|
61
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
62
|
+
return {
|
|
63
|
+
name: 'isEmail',
|
|
64
|
+
message: options.message ?? '必须是合法的邮箱地址',
|
|
65
|
+
validate: (value) => typeof value === 'string' && emailRegex.test(value),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function IsOptional(): ValidationRuleDefinition {
|
|
70
|
+
return {
|
|
71
|
+
name: 'isOptional',
|
|
72
|
+
message: '',
|
|
73
|
+
optional: true,
|
|
74
|
+
validate: () => true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function MinLength(min: number, options: RuleOption = {}): ValidationRuleDefinition {
|
|
79
|
+
return {
|
|
80
|
+
name: 'minLength',
|
|
81
|
+
message: options.message ?? `长度不能小于 ${min}`,
|
|
82
|
+
validate: (value) => typeof value === 'string' && value.length >= min,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 获取方法参数的验证元数据
|
|
88
|
+
*/
|
|
89
|
+
export function getValidationMetadata(target: object, propertyKey: string | symbol): ValidationMetadata[] {
|
|
90
|
+
return (
|
|
91
|
+
(Reflect.getMetadata(VALIDATION_METADATA_KEY, target, propertyKey) as ValidationMetadata[]) || []
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ValidationIssue {
|
|
2
|
+
/**
|
|
3
|
+
* 参数索引
|
|
4
|
+
*/
|
|
5
|
+
index: number;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 失败的规则名称
|
|
9
|
+
*/
|
|
10
|
+
rule: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 错误信息
|
|
14
|
+
*/
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ValidationError extends Error {
|
|
19
|
+
public readonly issues: ValidationIssue[];
|
|
20
|
+
|
|
21
|
+
public constructor(message: string, issues: ValidationIssue[]) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'ValidationError';
|
|
24
|
+
this.issues = issues;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Validate,
|
|
3
|
+
IsString,
|
|
4
|
+
IsNumber,
|
|
5
|
+
IsEmail,
|
|
6
|
+
IsOptional,
|
|
7
|
+
MinLength,
|
|
8
|
+
getValidationMetadata,
|
|
9
|
+
} from './decorators';
|
|
10
|
+
export type { ValidationRuleDefinition, ValidationMetadata } from './types';
|
|
11
|
+
export { validateParameters } from './validator';
|
|
12
|
+
export { ValidationError, type ValidationIssue } from './errors';
|
|
13
|
+
|
|
14
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ValidationRuleDefinition {
|
|
2
|
+
/**
|
|
3
|
+
* 规则名称
|
|
4
|
+
*/
|
|
5
|
+
name: string;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 错误信息
|
|
9
|
+
*/
|
|
10
|
+
message: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 校验函数
|
|
14
|
+
*/
|
|
15
|
+
validate: (value: unknown) => boolean;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 是否为可选字段
|
|
19
|
+
*/
|
|
20
|
+
optional?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ValidationMetadata {
|
|
24
|
+
/**
|
|
25
|
+
* 参数索引
|
|
26
|
+
*/
|
|
27
|
+
index: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 规则列表
|
|
31
|
+
*/
|
|
32
|
+
rules: ValidationRuleDefinition[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ValidationMetadata, ValidationRuleDefinition } from './types';
|
|
2
|
+
import { ValidationError, type ValidationIssue } from './errors';
|
|
3
|
+
|
|
4
|
+
function shouldSkip(rule: ValidationRuleDefinition, value: unknown): boolean {
|
|
5
|
+
return rule.optional === true && (value === undefined || value === null);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function validateRule(
|
|
9
|
+
rule: ValidationRuleDefinition,
|
|
10
|
+
value: unknown,
|
|
11
|
+
index: number,
|
|
12
|
+
): ValidationIssue | null {
|
|
13
|
+
if (shouldSkip(rule, value)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const passed = rule.validate(value);
|
|
17
|
+
if (passed) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
index,
|
|
22
|
+
rule: rule.name,
|
|
23
|
+
message: rule.message,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 根据元数据验证参数
|
|
29
|
+
* @param params - 参数数组
|
|
30
|
+
* @param metadata - 验证元数据
|
|
31
|
+
*/
|
|
32
|
+
export function validateParameters(params: unknown[], metadata: ValidationMetadata[]): void {
|
|
33
|
+
if (!metadata.length) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const issues: ValidationIssue[] = [];
|
|
37
|
+
|
|
38
|
+
for (const item of metadata) {
|
|
39
|
+
const value = params[item.index];
|
|
40
|
+
let skipped = false;
|
|
41
|
+
|
|
42
|
+
for (const rule of item.rules) {
|
|
43
|
+
if (rule.optional && (value === undefined || value === null)) {
|
|
44
|
+
skipped = true;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const issue = validateRule(rule, value, item.index);
|
|
48
|
+
if (issue) {
|
|
49
|
+
issues.push(issue);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (skipped) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (issues.length > 0) {
|
|
59
|
+
throw new ValidationError('Validation failed', issues);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
const GATEWAY_METADATA_KEY = Symbol('websocket:gateway');
|
|
4
|
+
const HANDLER_METADATA_KEY = Symbol('websocket:handlers');
|
|
5
|
+
|
|
6
|
+
export interface WebSocketGatewayMetadata {
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WebSocketHandlerMetadata {
|
|
11
|
+
open?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
close?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function WebSocketGateway(path: string): ClassDecorator {
|
|
17
|
+
return (target: Function) => {
|
|
18
|
+
Reflect.defineMetadata(GATEWAY_METADATA_KEY, { path }, target);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createHandlerDecorator(type: keyof WebSocketHandlerMetadata): MethodDecorator {
|
|
23
|
+
return function (target, propertyKey) {
|
|
24
|
+
if (typeof propertyKey !== 'string') {
|
|
25
|
+
throw new Error('@OnOpen/@OnMessage/@OnClose only support string method names');
|
|
26
|
+
}
|
|
27
|
+
const existing =
|
|
28
|
+
(Reflect.getMetadata(HANDLER_METADATA_KEY, target) as WebSocketHandlerMetadata | undefined) ||
|
|
29
|
+
{};
|
|
30
|
+
existing[type] = propertyKey;
|
|
31
|
+
Reflect.defineMetadata(HANDLER_METADATA_KEY, existing, target);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const OnOpen = createHandlerDecorator('open');
|
|
36
|
+
export const OnMessage = createHandlerDecorator('message');
|
|
37
|
+
export const OnClose = createHandlerDecorator('close');
|
|
38
|
+
|
|
39
|
+
export function getGatewayMetadata(
|
|
40
|
+
constructor: new (...args: unknown[]) => unknown,
|
|
41
|
+
): WebSocketGatewayMetadata | undefined {
|
|
42
|
+
return Reflect.getMetadata(GATEWAY_METADATA_KEY, constructor);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getHandlerMetadata(target: object): WebSocketHandlerMetadata {
|
|
46
|
+
return (
|
|
47
|
+
(Reflect.getMetadata(HANDLER_METADATA_KEY, target) as WebSocketHandlerMetadata | undefined) || {}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ServerWebSocket } from 'bun';
|
|
2
|
+
|
|
3
|
+
import { Container } from '../di/container';
|
|
4
|
+
import { ControllerRegistry } from '../controller/controller';
|
|
5
|
+
import { getGatewayMetadata, getHandlerMetadata } from './decorators';
|
|
6
|
+
import type { Constructor } from '@/core/types'
|
|
7
|
+
|
|
8
|
+
interface GatewayDefinition {
|
|
9
|
+
path: string;
|
|
10
|
+
gatewayClass: Constructor<unknown>;
|
|
11
|
+
handlers: {
|
|
12
|
+
open?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
close?: string;
|
|
15
|
+
};
|
|
16
|
+
instance?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WebSocketConnectionData {
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class WebSocketGatewayRegistry {
|
|
24
|
+
private static instance: WebSocketGatewayRegistry;
|
|
25
|
+
private readonly container: Container;
|
|
26
|
+
private readonly gateways = new Map<string, GatewayDefinition>();
|
|
27
|
+
|
|
28
|
+
private constructor() {
|
|
29
|
+
this.container = ControllerRegistry.getInstance().getContainer();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static getInstance(): WebSocketGatewayRegistry {
|
|
33
|
+
if (!WebSocketGatewayRegistry.instance) {
|
|
34
|
+
WebSocketGatewayRegistry.instance = new WebSocketGatewayRegistry();
|
|
35
|
+
}
|
|
36
|
+
return WebSocketGatewayRegistry.instance;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public register(gatewayClass: Constructor<unknown>): void {
|
|
40
|
+
const metadata = getGatewayMetadata(gatewayClass);
|
|
41
|
+
if (!metadata) {
|
|
42
|
+
throw new Error(`WebSocket gateway ${gatewayClass.name} must use @WebSocketGateway()`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handlers = getHandlerMetadata(gatewayClass.prototype);
|
|
46
|
+
if (!handlers.open && !handlers.message && !handlers.close) {
|
|
47
|
+
throw new Error(`WebSocket gateway ${gatewayClass.name} must define at least one handler`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!this.container.isRegistered(gatewayClass)) {
|
|
51
|
+
this.container.register(gatewayClass);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.gateways.set(metadata.path, {
|
|
55
|
+
path: metadata.path,
|
|
56
|
+
gatewayClass,
|
|
57
|
+
handlers,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public hasGateway(path: string): boolean {
|
|
62
|
+
return this.gateways.has(path);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public clear(): void {
|
|
66
|
+
this.gateways.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private getGateway(path: string): GatewayDefinition | undefined {
|
|
70
|
+
return this.gateways.get(path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private getGatewayInstance(definition: GatewayDefinition): unknown {
|
|
74
|
+
if (!definition.instance) {
|
|
75
|
+
definition.instance = this.container.resolve(definition.gatewayClass);
|
|
76
|
+
}
|
|
77
|
+
return definition.instance;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private invokeHandler(
|
|
81
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
82
|
+
definition: GatewayDefinition,
|
|
83
|
+
handlerName: string | undefined,
|
|
84
|
+
...args: unknown[]
|
|
85
|
+
): void {
|
|
86
|
+
if (!handlerName) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const instance = this.getGatewayInstance(definition);
|
|
90
|
+
const handler = (instance as Record<string, unknown>)[handlerName];
|
|
91
|
+
if (typeof handler === 'function') {
|
|
92
|
+
handler.apply(instance, [ws, ...args]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
97
|
+
const path = ws.data?.path;
|
|
98
|
+
const definition = path ? this.getGateway(path) : undefined;
|
|
99
|
+
if (!definition) {
|
|
100
|
+
ws.close(1008, 'Gateway not found');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.invokeHandler(ws, definition, definition.handlers.open);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public handleMessage(
|
|
107
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
108
|
+
message: string | ArrayBuffer | ArrayBufferView,
|
|
109
|
+
): void {
|
|
110
|
+
const path = ws.data?.path;
|
|
111
|
+
const definition = path ? this.getGateway(path) : undefined;
|
|
112
|
+
if (!definition) {
|
|
113
|
+
ws.close(1008, 'Gateway not found');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.invokeHandler(ws, definition, definition.handlers.message, message);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public handleClose(
|
|
120
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
121
|
+
code: number,
|
|
122
|
+
reason: string,
|
|
123
|
+
): void {
|
|
124
|
+
const path = ws.data?.path;
|
|
125
|
+
const definition = path ? this.getGateway(path) : undefined;
|
|
126
|
+
if (!definition) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|