@dangao/bun-server 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -3
- package/dist/cache/cache-module.d.ts +18 -0
- package/dist/cache/cache-module.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/interceptors.d.ts +41 -0
- package/dist/cache/interceptors.d.ts.map +1 -0
- package/dist/cache/service-proxy.d.ts +62 -0
- package/dist/cache/service-proxy.d.ts.map +1 -0
- package/dist/controller/controller.d.ts +8 -0
- package/dist/controller/controller.d.ts.map +1 -1
- package/dist/core/application.d.ts +5 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/di/container.d.ts +18 -1
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/types.d.ts +22 -0
- package/dist/di/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3235 -2875
- package/dist/websocket/registry.d.ts +19 -0
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/symbol-interface-pattern.md +431 -0
- package/docs/zh/symbol-interface-pattern.md +431 -0
- package/package.json +1 -1
- package/src/cache/cache-module.ts +37 -0
- package/src/cache/index.ts +16 -1
- package/src/cache/interceptors.ts +295 -0
- package/src/cache/service-proxy.ts +219 -0
- package/src/controller/controller.ts +30 -6
- package/src/core/application.ts +25 -1
- package/src/core/server.ts +1 -0
- package/src/di/container.ts +57 -7
- package/src/di/index.ts +7 -1
- package/src/di/types.ts +29 -0
- package/src/index.ts +7 -0
- package/src/websocket/registry.ts +114 -14
- package/tests/cache/cache-decorators.test.ts +284 -0
- package/tests/controller/path-combination.test.ts +353 -0
- package/tests/websocket/gateway.test.ts +207 -1
package/src/di/container.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import {
|
|
3
3
|
type DependencyMetadata,
|
|
4
|
+
type InstancePostProcessor,
|
|
5
|
+
INSTANCE_POST_PROCESSOR_TOKEN,
|
|
4
6
|
Lifecycle,
|
|
5
7
|
type ProviderConfig,
|
|
6
8
|
} from "./types";
|
|
@@ -59,6 +61,11 @@ export class Container {
|
|
|
59
61
|
DependencyPlan
|
|
60
62
|
>();
|
|
61
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 实例后处理器列表
|
|
66
|
+
*/
|
|
67
|
+
private readonly postProcessors: InstancePostProcessor[] = [];
|
|
68
|
+
|
|
62
69
|
/**
|
|
63
70
|
* 注册提供者
|
|
64
71
|
* @param token - 提供者标识符(类构造函数或 token)
|
|
@@ -112,6 +119,45 @@ export class Container {
|
|
|
112
119
|
});
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
/**
|
|
123
|
+
* 注册实例后处理器
|
|
124
|
+
* @param processor - 后处理器实例
|
|
125
|
+
*/
|
|
126
|
+
public registerPostProcessor(processor: InstancePostProcessor): void {
|
|
127
|
+
this.postProcessors.push(processor);
|
|
128
|
+
// 按优先级排序(数字越小优先级越高)
|
|
129
|
+
this.postProcessors.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 应用所有后处理器(包括父容器的后处理器)
|
|
134
|
+
* @param instance - 原始实例
|
|
135
|
+
* @param constructor - 构造函数
|
|
136
|
+
* @param originContainer - 原始调用容器(用于后处理器解析依赖)
|
|
137
|
+
* @returns 处理后的实例
|
|
138
|
+
*/
|
|
139
|
+
private applyPostProcessors<T>(
|
|
140
|
+
instance: T,
|
|
141
|
+
constructor: Constructor<T>,
|
|
142
|
+
originContainer?: Container,
|
|
143
|
+
): T {
|
|
144
|
+
// 使用原始容器或当前容器
|
|
145
|
+
const containerForProcessors = originContainer ?? this;
|
|
146
|
+
let result = instance;
|
|
147
|
+
|
|
148
|
+
// 先应用父容器的后处理器
|
|
149
|
+
if (this.parent) {
|
|
150
|
+
result = (this.parent as Container).applyPostProcessors(result, constructor, containerForProcessors);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 再应用本容器的后处理器
|
|
154
|
+
// 传递原始调用容器,而不是当前容器
|
|
155
|
+
for (const processor of this.postProcessors) {
|
|
156
|
+
result = processor.postProcess(result, constructor, containerForProcessors);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
115
161
|
/**
|
|
116
162
|
* 解析依赖
|
|
117
163
|
* @param token - 提供者标识符
|
|
@@ -321,16 +367,19 @@ export class Container {
|
|
|
321
367
|
private instantiate<T>(constructor: Constructor<T>): T {
|
|
322
368
|
const plan = this.getDependencyPlan(constructor);
|
|
323
369
|
|
|
370
|
+
let instance: T;
|
|
324
371
|
if (plan.paramLength === 0) {
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
372
|
+
instance = new constructor();
|
|
373
|
+
} else {
|
|
374
|
+
const dependencies = new Array(plan.paramLength);
|
|
375
|
+
for (let index = 0; index < plan.paramLength; index++) {
|
|
376
|
+
dependencies[index] = this.resolveFromPlan(constructor, plan, index);
|
|
377
|
+
}
|
|
378
|
+
instance = new constructor(...dependencies);
|
|
331
379
|
}
|
|
332
380
|
|
|
333
|
-
|
|
381
|
+
// 应用后处理器
|
|
382
|
+
return this.applyPostProcessors(instance, constructor);
|
|
334
383
|
}
|
|
335
384
|
|
|
336
385
|
/**
|
|
@@ -356,6 +405,7 @@ export class Container {
|
|
|
356
405
|
this.singletons.clear();
|
|
357
406
|
this.typeToToken.clear();
|
|
358
407
|
this.dependencyPlans.clear();
|
|
408
|
+
this.postProcessors.length = 0;
|
|
359
409
|
// scopedInstances 使用 WeakMap,当 Context 对象被 GC 时会自动清理
|
|
360
410
|
}
|
|
361
411
|
|
package/src/di/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export { Container } from './container';
|
|
2
2
|
export { Injectable, Inject, getDependencyMetadata, isInjectable, getLifecycle } from './decorators';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
Lifecycle,
|
|
5
|
+
INSTANCE_POST_PROCESSOR_TOKEN,
|
|
6
|
+
type ProviderConfig,
|
|
7
|
+
type DependencyMetadata,
|
|
8
|
+
type InstancePostProcessor,
|
|
9
|
+
} from './types';
|
|
4
10
|
|
package/src/di/types.ts
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import type { Constructor } from '@/core/types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* 实例后处理器接口
|
|
5
|
+
* 用于在 DI 容器创建实例后进行处理(如创建代理、注入额外依赖等)
|
|
6
|
+
*/
|
|
7
|
+
export interface InstancePostProcessor {
|
|
8
|
+
/**
|
|
9
|
+
* 处理新创建的实例
|
|
10
|
+
* @param instance - 原始实例
|
|
11
|
+
* @param constructor - 构造函数
|
|
12
|
+
* @param container - DI 容器引用
|
|
13
|
+
* @returns 处理后的实例(可以是代理)
|
|
14
|
+
*/
|
|
15
|
+
postProcess<T>(
|
|
16
|
+
instance: T,
|
|
17
|
+
constructor: Constructor<T>,
|
|
18
|
+
container: unknown,
|
|
19
|
+
): T;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 优先级(数字越小优先级越高,默认 100)
|
|
23
|
+
*/
|
|
24
|
+
priority?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 实例后处理器注册表 Token
|
|
29
|
+
*/
|
|
30
|
+
export const INSTANCE_POST_PROCESSOR_TOKEN = Symbol('@dangao/bun-server:di:post-processor');
|
|
31
|
+
|
|
3
32
|
/**
|
|
4
33
|
* 依赖生命周期类型
|
|
5
34
|
*/
|
package/src/index.ts
CHANGED
|
@@ -254,10 +254,17 @@ export {
|
|
|
254
254
|
Cacheable,
|
|
255
255
|
CacheEvict,
|
|
256
256
|
CachePut,
|
|
257
|
+
EnableCacheProxy,
|
|
258
|
+
CacheServiceProxy,
|
|
259
|
+
CachePostProcessor,
|
|
260
|
+
CacheableInterceptor,
|
|
261
|
+
CacheEvictInterceptor,
|
|
262
|
+
CachePutInterceptor,
|
|
257
263
|
MemoryCacheStore,
|
|
258
264
|
RedisCacheStore,
|
|
259
265
|
CACHE_SERVICE_TOKEN,
|
|
260
266
|
CACHE_OPTIONS_TOKEN,
|
|
267
|
+
CACHE_POST_PROCESSOR_TOKEN,
|
|
261
268
|
} from './cache';
|
|
262
269
|
export type {
|
|
263
270
|
CacheModuleOptions,
|
|
@@ -16,18 +16,24 @@ interface GatewayDefinition {
|
|
|
16
16
|
message?: string;
|
|
17
17
|
close?: string;
|
|
18
18
|
};
|
|
19
|
+
pattern: RegExp;
|
|
20
|
+
paramNames: string[];
|
|
21
|
+
isStatic: boolean;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export interface WebSocketConnectionData {
|
|
22
25
|
path: string;
|
|
23
26
|
query?: URLSearchParams;
|
|
24
27
|
context?: Context;
|
|
28
|
+
params?: Record<string, string>;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export class WebSocketGatewayRegistry {
|
|
28
32
|
private static instance: WebSocketGatewayRegistry;
|
|
29
33
|
private readonly container: Container;
|
|
30
34
|
private readonly gateways = new Map<string, GatewayDefinition>();
|
|
35
|
+
private readonly staticGateways = new Map<string, GatewayDefinition>();
|
|
36
|
+
private readonly dynamicGateways: GatewayDefinition[] = [];
|
|
31
37
|
|
|
32
38
|
private constructor() {
|
|
33
39
|
this.container = ControllerRegistry.getInstance().getContainer();
|
|
@@ -40,6 +46,24 @@ export class WebSocketGatewayRegistry {
|
|
|
40
46
|
return WebSocketGatewayRegistry.instance;
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 解析路径,生成匹配模式和参数名列表
|
|
51
|
+
* @param path - 路由路径
|
|
52
|
+
* @returns 匹配模式和参数名列表
|
|
53
|
+
*/
|
|
54
|
+
private parsePath(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
55
|
+
const paramNames: string[] = [];
|
|
56
|
+
const patternString = path
|
|
57
|
+
.replace(/:([^/]+)/g, (_, paramName) => {
|
|
58
|
+
paramNames.push(paramName);
|
|
59
|
+
return '([^/]+)';
|
|
60
|
+
})
|
|
61
|
+
.replace(/\*/g, '.*');
|
|
62
|
+
|
|
63
|
+
const pattern = new RegExp(`^${patternString}$`);
|
|
64
|
+
return { pattern, paramNames };
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
public register(gatewayClass: Constructor<unknown>): void {
|
|
44
68
|
const metadata = getGatewayMetadata(gatewayClass);
|
|
45
69
|
if (!metadata) {
|
|
@@ -55,23 +79,82 @@ export class WebSocketGatewayRegistry {
|
|
|
55
79
|
this.container.register(gatewayClass);
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
// 解析路径
|
|
83
|
+
const { pattern, paramNames } = this.parsePath(metadata.path);
|
|
84
|
+
const isStatic = !metadata.path.includes(':') && !metadata.path.includes('*');
|
|
85
|
+
|
|
86
|
+
const definition: GatewayDefinition = {
|
|
59
87
|
path: metadata.path,
|
|
60
88
|
gatewayClass,
|
|
61
89
|
handlers,
|
|
62
|
-
|
|
90
|
+
pattern,
|
|
91
|
+
paramNames,
|
|
92
|
+
isStatic,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.gateways.set(metadata.path, definition);
|
|
96
|
+
|
|
97
|
+
// 分别存储静态和动态路由
|
|
98
|
+
if (isStatic) {
|
|
99
|
+
this.staticGateways.set(metadata.path, definition);
|
|
100
|
+
} else {
|
|
101
|
+
this.dynamicGateways.push(definition);
|
|
102
|
+
}
|
|
63
103
|
}
|
|
64
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 检查是否有匹配的网关(支持动态路径匹配)
|
|
107
|
+
* @param path - 请求路径
|
|
108
|
+
* @returns 是否有匹配的网关
|
|
109
|
+
*/
|
|
65
110
|
public hasGateway(path: string): boolean {
|
|
66
|
-
|
|
111
|
+
// 先检查静态路由
|
|
112
|
+
if (this.staticGateways.has(path)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 遍历动态路由
|
|
117
|
+
for (const gateway of this.dynamicGateways) {
|
|
118
|
+
if (gateway.pattern.test(path)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
67
124
|
}
|
|
68
125
|
|
|
69
126
|
public clear(): void {
|
|
70
127
|
this.gateways.clear();
|
|
128
|
+
this.staticGateways.clear();
|
|
129
|
+
this.dynamicGateways.length = 0;
|
|
71
130
|
}
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
|
|
132
|
+
/**
|
|
133
|
+
* 获取匹配的网关(支持动态路径匹配)
|
|
134
|
+
* @param path - 请求路径
|
|
135
|
+
* @returns 匹配的网关定义和路径参数
|
|
136
|
+
*/
|
|
137
|
+
private getGateway(path: string): { definition: GatewayDefinition; params: Record<string, string> } | undefined {
|
|
138
|
+
// 先检查静态路由
|
|
139
|
+
const staticGateway = this.staticGateways.get(path);
|
|
140
|
+
if (staticGateway) {
|
|
141
|
+
return { definition: staticGateway, params: {} };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 遍历动态路由
|
|
145
|
+
for (const gateway of this.dynamicGateways) {
|
|
146
|
+
const match = path.match(gateway.pattern);
|
|
147
|
+
if (match) {
|
|
148
|
+
// 提取路径参数
|
|
149
|
+
const params: Record<string, string> = {};
|
|
150
|
+
for (let i = 0; i < gateway.paramNames.length; i++) {
|
|
151
|
+
params[gateway.paramNames[i]] = match[i + 1] ?? '';
|
|
152
|
+
}
|
|
153
|
+
return { definition: gateway, params };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return undefined;
|
|
75
158
|
}
|
|
76
159
|
|
|
77
160
|
/**
|
|
@@ -129,7 +212,16 @@ export class WebSocketGatewayRegistry {
|
|
|
129
212
|
headers: new Headers(),
|
|
130
213
|
});
|
|
131
214
|
context = new Context(request);
|
|
215
|
+
// 设置路径参数到 Context
|
|
216
|
+
if (ws.data.params) {
|
|
217
|
+
context.params = ws.data.params;
|
|
218
|
+
}
|
|
132
219
|
ws.data.context = context;
|
|
220
|
+
} else {
|
|
221
|
+
// 如果 Context 已存在,更新路径参数
|
|
222
|
+
if (ws.data.params) {
|
|
223
|
+
context.params = ws.data.params;
|
|
224
|
+
}
|
|
133
225
|
}
|
|
134
226
|
|
|
135
227
|
// 使用 ParamBinder 绑定参数
|
|
@@ -189,12 +281,16 @@ export class WebSocketGatewayRegistry {
|
|
|
189
281
|
|
|
190
282
|
public async handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): Promise<void> {
|
|
191
283
|
const path = ws.data?.path;
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
284
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
285
|
+
if (!match) {
|
|
194
286
|
ws.close(1008, 'Gateway not found');
|
|
195
287
|
return;
|
|
196
288
|
}
|
|
197
|
-
|
|
289
|
+
// 保存路径参数到 WebSocket 连接数据
|
|
290
|
+
if (match.params && Object.keys(match.params).length > 0) {
|
|
291
|
+
ws.data.params = match.params;
|
|
292
|
+
}
|
|
293
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.open);
|
|
198
294
|
}
|
|
199
295
|
|
|
200
296
|
public async handleMessage(
|
|
@@ -202,12 +298,16 @@ export class WebSocketGatewayRegistry {
|
|
|
202
298
|
message: string | ArrayBuffer | ArrayBufferView,
|
|
203
299
|
): Promise<void> {
|
|
204
300
|
const path = ws.data?.path;
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
301
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
302
|
+
if (!match) {
|
|
207
303
|
ws.close(1008, 'Gateway not found');
|
|
208
304
|
return;
|
|
209
305
|
}
|
|
210
|
-
|
|
306
|
+
// 保存路径参数到 WebSocket 连接数据(如果还没有)
|
|
307
|
+
if (match.params && Object.keys(match.params).length > 0 && !ws.data.params) {
|
|
308
|
+
ws.data.params = match.params;
|
|
309
|
+
}
|
|
310
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.message, message);
|
|
211
311
|
}
|
|
212
312
|
|
|
213
313
|
public async handleClose(
|
|
@@ -216,11 +316,11 @@ export class WebSocketGatewayRegistry {
|
|
|
216
316
|
reason: string,
|
|
217
317
|
): Promise<void> {
|
|
218
318
|
const path = ws.data?.path;
|
|
219
|
-
const
|
|
220
|
-
if (!
|
|
319
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
320
|
+
if (!match) {
|
|
221
321
|
return;
|
|
222
322
|
}
|
|
223
|
-
await this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
323
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.close, code, reason);
|
|
224
324
|
}
|
|
225
325
|
}
|
|
226
326
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
3
|
+
import { Application } from '../../src/core/application';
|
|
4
|
+
import { Controller } from '../../src/controller/controller';
|
|
5
|
+
import { GET } from '../../src/router/decorators';
|
|
6
|
+
import { Param } from '../../src/controller/decorators';
|
|
7
|
+
import { Module } from '../../src/di/module';
|
|
8
|
+
import { Injectable, Inject } from '../../src/di/decorators';
|
|
9
|
+
import {
|
|
10
|
+
CacheModule,
|
|
11
|
+
CacheService,
|
|
12
|
+
Cacheable,
|
|
13
|
+
CacheEvict,
|
|
14
|
+
CachePut,
|
|
15
|
+
EnableCacheProxy,
|
|
16
|
+
CACHE_SERVICE_TOKEN,
|
|
17
|
+
} from '../../src/cache';
|
|
18
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
19
|
+
import { ControllerRegistry } from '../../src/controller/controller';
|
|
20
|
+
import { ModuleRegistry } from '../../src/di/module-registry';
|
|
21
|
+
import { getTestPort } from '../utils/test-port';
|
|
22
|
+
|
|
23
|
+
describe('Cache Decorators', () => {
|
|
24
|
+
let app: Application;
|
|
25
|
+
let port: number;
|
|
26
|
+
let callCount: number;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
port = getTestPort();
|
|
30
|
+
callCount = 0;
|
|
31
|
+
CacheModule.reset();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
if (app) {
|
|
36
|
+
await app.stop();
|
|
37
|
+
}
|
|
38
|
+
RouteRegistry.getInstance().clear();
|
|
39
|
+
ControllerRegistry.getInstance().clear();
|
|
40
|
+
ModuleRegistry.getInstance().clear();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('@Cacheable should cache method results in service layer', async () => {
|
|
44
|
+
// 定义服务
|
|
45
|
+
@Injectable()
|
|
46
|
+
@EnableCacheProxy()
|
|
47
|
+
class UserService {
|
|
48
|
+
@Cacheable({ key: 'user:{0}', ttl: 5000 })
|
|
49
|
+
public async findById(id: string): Promise<{ id: string; name: string }> {
|
|
50
|
+
callCount++;
|
|
51
|
+
return { id, name: `User ${id}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 定义控制器
|
|
56
|
+
@Controller('/api/users')
|
|
57
|
+
class UserController {
|
|
58
|
+
public constructor(private readonly userService: UserService) {}
|
|
59
|
+
|
|
60
|
+
@GET('/:id')
|
|
61
|
+
public async getUser(@Param('id') id: string): Promise<{ id: string; name: string }> {
|
|
62
|
+
return await this.userService.findById(id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 定义模块
|
|
67
|
+
@Module({
|
|
68
|
+
imports: [CacheModule.forRoot({ defaultTtl: 5000 })],
|
|
69
|
+
controllers: [UserController],
|
|
70
|
+
providers: [UserService],
|
|
71
|
+
})
|
|
72
|
+
class AppModule {}
|
|
73
|
+
|
|
74
|
+
// 创建应用
|
|
75
|
+
app = new Application({ port });
|
|
76
|
+
app.registerModule(AppModule);
|
|
77
|
+
await app.listen();
|
|
78
|
+
|
|
79
|
+
// 第一次调用 - 应该执行方法
|
|
80
|
+
const response1 = await fetch(`http://localhost:${port}/api/users/1`);
|
|
81
|
+
expect(response1.status).toBe(200);
|
|
82
|
+
const data1 = await response1.json();
|
|
83
|
+
expect(data1.id).toBe('1');
|
|
84
|
+
expect(data1.name).toBe('User 1');
|
|
85
|
+
expect(callCount).toBe(1);
|
|
86
|
+
|
|
87
|
+
// 第二次调用 - 应该使用缓存
|
|
88
|
+
const response2 = await fetch(`http://localhost:${port}/api/users/1`);
|
|
89
|
+
expect(response2.status).toBe(200);
|
|
90
|
+
const data2 = await response2.json();
|
|
91
|
+
expect(data2.id).toBe('1');
|
|
92
|
+
expect(data2.name).toBe('User 1');
|
|
93
|
+
expect(callCount).toBe(1); // 方法没有被再次调用
|
|
94
|
+
|
|
95
|
+
// 调用不同的 ID - 应该执行方法
|
|
96
|
+
const response3 = await fetch(`http://localhost:${port}/api/users/2`);
|
|
97
|
+
expect(response3.status).toBe(200);
|
|
98
|
+
const data3 = await response3.json();
|
|
99
|
+
expect(data3.id).toBe('2');
|
|
100
|
+
expect(data3.name).toBe('User 2');
|
|
101
|
+
expect(callCount).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('@CacheEvict should clear cache', async () => {
|
|
105
|
+
// 定义服务
|
|
106
|
+
@Injectable()
|
|
107
|
+
@EnableCacheProxy()
|
|
108
|
+
class ProductService {
|
|
109
|
+
@Cacheable({ key: 'product:{0}', ttl: 5000 })
|
|
110
|
+
public async findById(id: string): Promise<{ id: string; name: string }> {
|
|
111
|
+
callCount++;
|
|
112
|
+
return { id, name: `Product ${id} v${callCount}` };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@CacheEvict({ key: 'product:{0}' })
|
|
116
|
+
public async deleteById(id: string): Promise<void> {
|
|
117
|
+
// 删除产品
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 定义控制器
|
|
122
|
+
@Controller('/api/products')
|
|
123
|
+
class ProductController {
|
|
124
|
+
public constructor(private readonly productService: ProductService) {}
|
|
125
|
+
|
|
126
|
+
@GET('/:id')
|
|
127
|
+
public async getProduct(@Param('id') id: string): Promise<{ id: string; name: string }> {
|
|
128
|
+
return await this.productService.findById(id);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@GET('/:id/delete')
|
|
132
|
+
public async deleteProduct(@Param('id') id: string): Promise<{ success: boolean }> {
|
|
133
|
+
await this.productService.deleteById(id);
|
|
134
|
+
return { success: true };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 定义模块
|
|
139
|
+
@Module({
|
|
140
|
+
imports: [CacheModule.forRoot({ defaultTtl: 5000 })],
|
|
141
|
+
controllers: [ProductController],
|
|
142
|
+
providers: [ProductService],
|
|
143
|
+
})
|
|
144
|
+
class AppModule {}
|
|
145
|
+
|
|
146
|
+
// 创建应用
|
|
147
|
+
app = new Application({ port });
|
|
148
|
+
app.registerModule(AppModule);
|
|
149
|
+
await app.listen();
|
|
150
|
+
|
|
151
|
+
// 第一次调用 - 缓存产品
|
|
152
|
+
const response1 = await fetch(`http://localhost:${port}/api/products/1`);
|
|
153
|
+
expect(response1.status).toBe(200);
|
|
154
|
+
const data1 = await response1.json();
|
|
155
|
+
expect(data1.name).toBe('Product 1 v1');
|
|
156
|
+
expect(callCount).toBe(1);
|
|
157
|
+
|
|
158
|
+
// 第二次调用 - 使用缓存
|
|
159
|
+
const response2 = await fetch(`http://localhost:${port}/api/products/1`);
|
|
160
|
+
const data2 = await response2.json();
|
|
161
|
+
expect(data2.name).toBe('Product 1 v1');
|
|
162
|
+
expect(callCount).toBe(1);
|
|
163
|
+
|
|
164
|
+
// 删除产品 - 清除缓存
|
|
165
|
+
await fetch(`http://localhost:${port}/api/products/1/delete`);
|
|
166
|
+
|
|
167
|
+
// 再次获取 - 缓存已清除,应重新执行方法
|
|
168
|
+
const response3 = await fetch(`http://localhost:${port}/api/products/1`);
|
|
169
|
+
const data3 = await response3.json();
|
|
170
|
+
expect(data3.name).toBe('Product 1 v2');
|
|
171
|
+
expect(callCount).toBe(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('@CachePut should always execute method and update cache', async () => {
|
|
175
|
+
// 定义服务
|
|
176
|
+
@Injectable()
|
|
177
|
+
@EnableCacheProxy()
|
|
178
|
+
class OrderService {
|
|
179
|
+
@Cacheable({ key: 'order:{0}', ttl: 5000 })
|
|
180
|
+
public async findById(id: string): Promise<{ id: string; status: string }> {
|
|
181
|
+
callCount++;
|
|
182
|
+
return { id, status: 'pending' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@CachePut({ key: 'order:{0}', ttl: 5000 })
|
|
186
|
+
public async updateStatus(id: string, status: string): Promise<{ id: string; status: string }> {
|
|
187
|
+
return { id, status };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 定义控制器
|
|
192
|
+
@Controller('/api/orders')
|
|
193
|
+
class OrderController {
|
|
194
|
+
public constructor(private readonly orderService: OrderService) {}
|
|
195
|
+
|
|
196
|
+
@GET('/:id')
|
|
197
|
+
public async getOrder(@Param('id') id: string): Promise<{ id: string; status: string }> {
|
|
198
|
+
return await this.orderService.findById(id);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@GET('/:id/update/:status')
|
|
202
|
+
public async updateOrder(
|
|
203
|
+
@Param('id') id: string,
|
|
204
|
+
@Param('status') status: string,
|
|
205
|
+
): Promise<{ id: string; status: string }> {
|
|
206
|
+
return await this.orderService.updateStatus(id, status);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 定义模块
|
|
211
|
+
@Module({
|
|
212
|
+
imports: [CacheModule.forRoot({ defaultTtl: 5000 })],
|
|
213
|
+
controllers: [OrderController],
|
|
214
|
+
providers: [OrderService],
|
|
215
|
+
})
|
|
216
|
+
class AppModule {}
|
|
217
|
+
|
|
218
|
+
// 创建应用
|
|
219
|
+
app = new Application({ port });
|
|
220
|
+
app.registerModule(AppModule);
|
|
221
|
+
await app.listen();
|
|
222
|
+
|
|
223
|
+
// 第一次调用 - 缓存订单
|
|
224
|
+
const response1 = await fetch(`http://localhost:${port}/api/orders/1`);
|
|
225
|
+
expect(response1.status).toBe(200);
|
|
226
|
+
const data1 = await response1.json();
|
|
227
|
+
expect(data1.status).toBe('pending');
|
|
228
|
+
expect(callCount).toBe(1);
|
|
229
|
+
|
|
230
|
+
// 更新订单状态 - @CachePut 会更新缓存
|
|
231
|
+
const response2 = await fetch(`http://localhost:${port}/api/orders/1/update/completed`);
|
|
232
|
+
const data2 = await response2.json();
|
|
233
|
+
expect(data2.status).toBe('completed');
|
|
234
|
+
|
|
235
|
+
// 再次获取 - 应该返回更新后的缓存
|
|
236
|
+
const response3 = await fetch(`http://localhost:${port}/api/orders/1`);
|
|
237
|
+
const data3 = await response3.json();
|
|
238
|
+
expect(data3.status).toBe('completed');
|
|
239
|
+
expect(callCount).toBe(1); // findById 没有被再次调用
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('Service without @EnableCacheProxy should not use cache', async () => {
|
|
243
|
+
// 定义服务(没有 @EnableCacheProxy)
|
|
244
|
+
@Injectable()
|
|
245
|
+
class NoCacheService {
|
|
246
|
+
@Cacheable({ key: 'nocache:{0}', ttl: 5000 })
|
|
247
|
+
public async findById(id: string): Promise<{ id: string }> {
|
|
248
|
+
callCount++;
|
|
249
|
+
return { id };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 定义控制器
|
|
254
|
+
@Controller('/api/nocache')
|
|
255
|
+
class NoCacheController {
|
|
256
|
+
public constructor(private readonly noCacheService: NoCacheService) {}
|
|
257
|
+
|
|
258
|
+
@GET('/:id')
|
|
259
|
+
public async get(@Param('id') id: string): Promise<{ id: string }> {
|
|
260
|
+
return await this.noCacheService.findById(id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 定义模块
|
|
265
|
+
@Module({
|
|
266
|
+
imports: [CacheModule.forRoot({ defaultTtl: 5000 })],
|
|
267
|
+
controllers: [NoCacheController],
|
|
268
|
+
providers: [NoCacheService],
|
|
269
|
+
})
|
|
270
|
+
class AppModule {}
|
|
271
|
+
|
|
272
|
+
// 创建应用
|
|
273
|
+
app = new Application({ port });
|
|
274
|
+
app.registerModule(AppModule);
|
|
275
|
+
await app.listen();
|
|
276
|
+
|
|
277
|
+
// 两次调用都应该执行方法(没有缓存)
|
|
278
|
+
await fetch(`http://localhost:${port}/api/nocache/1`);
|
|
279
|
+
expect(callCount).toBe(1);
|
|
280
|
+
|
|
281
|
+
await fetch(`http://localhost:${port}/api/nocache/1`);
|
|
282
|
+
expect(callCount).toBe(2); // 方法被再次调用
|
|
283
|
+
});
|
|
284
|
+
});
|