@taicode/common-server 3.8.0 → 4.0.0-beta.10
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/output/app/index.d.ts +18 -15
- package/output/app/index.d.ts.map +1 -1
- package/output/app/index.js +330 -9
- package/output/auth/index.d.ts +73 -0
- package/output/auth/index.d.ts.map +1 -0
- package/output/auth/index.js +196 -0
- package/output/core/assemble.d.ts +64 -0
- package/output/core/assemble.d.ts.map +1 -0
- package/output/core/assemble.js +88 -0
- package/output/core/boot/context.d.ts +109 -0
- package/output/core/boot/context.d.ts.map +1 -0
- package/output/core/boot/context.js +145 -0
- package/output/core/boot/lifecycle.d.ts +206 -0
- package/output/core/boot/lifecycle.d.ts.map +1 -0
- package/output/core/boot/lifecycle.js +255 -0
- package/output/core/boot/shutdown.d.ts +34 -0
- package/output/core/boot/shutdown.d.ts.map +1 -0
- package/output/core/boot/shutdown.js +50 -0
- package/output/core/boot/signal.d.ts +13 -0
- package/output/core/boot/signal.d.ts.map +1 -0
- package/output/{app → core/boot}/signal.js +5 -4
- package/output/core/boot/steps.d.ts +138 -0
- package/output/core/boot/steps.d.ts.map +1 -0
- package/output/core/boot/steps.js +259 -0
- package/output/core/entries.d.ts +151 -0
- package/output/core/entries.d.ts.map +1 -0
- package/output/core/entries.js +242 -0
- package/output/core/entries.test.d.ts +2 -0
- package/output/core/entries.test.d.ts.map +1 -0
- package/output/core/entries.test.js +458 -0
- package/output/core/env.d.ts +83 -0
- package/output/core/env.d.ts.map +1 -0
- package/output/core/env.js +124 -0
- package/output/{app → core}/env.test.d.ts.map +1 -1
- package/output/core/env.test.js +105 -0
- package/output/core/modules/auth-provider/index.d.ts +10 -0
- package/output/core/modules/auth-provider/index.d.ts.map +1 -0
- package/output/core/modules/auth-provider/index.js +9 -0
- package/output/core/modules/auth-provider/types.d.ts +71 -0
- package/output/core/modules/auth-provider/types.d.ts.map +1 -0
- package/output/core/modules/context/index.d.ts +18 -0
- package/output/core/modules/context/index.d.ts.map +1 -0
- package/output/core/modules/context/index.js +23 -0
- package/output/core/modules/context/index.test.d.ts.map +1 -0
- package/output/core/modules/cookie/index.d.ts +21 -0
- package/output/core/modules/cookie/index.d.ts.map +1 -0
- package/output/core/modules/cookie/index.js +17 -0
- package/output/core/modules/identity/index.d.ts +4 -0
- package/output/core/modules/identity/index.d.ts.map +1 -0
- package/output/core/modules/identity/index.js +14 -0
- package/output/core/modules/logging/index.d.ts +5 -0
- package/output/core/modules/logging/index.d.ts.map +1 -0
- package/output/core/modules/logging/index.js +29 -0
- package/output/core/modules/openapi/index.d.ts +7 -0
- package/output/core/modules/openapi/index.d.ts.map +1 -0
- package/output/core/modules/openapi/index.js +41 -0
- package/output/core/modules/permission-operations/index.d.ts +26 -0
- package/output/core/modules/permission-operations/index.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.js +36 -0
- package/output/core/modules/permission-operations/index.test.d.ts +2 -0
- package/output/core/modules/permission-operations/index.test.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.test.js +45 -0
- package/output/core/modules/request-id/index.d.ts +33 -0
- package/output/core/modules/request-id/index.d.ts.map +1 -0
- package/output/core/modules/request-id/index.js +66 -0
- package/output/core/modules/response/index.d.ts +50 -0
- package/output/core/modules/response/index.d.ts.map +1 -0
- package/output/core/modules/response/index.js +102 -0
- package/output/{schema/index.d.ts → core/modules/schema/builders.d.ts} +8 -1
- package/output/core/modules/schema/builders.d.ts.map +1 -0
- package/output/{schema/index.js → core/modules/schema/builders.js} +7 -4
- package/output/core/modules/schema/index.d.ts +34 -0
- package/output/core/modules/schema/index.d.ts.map +1 -0
- package/output/core/modules/schema/index.js +62 -0
- package/output/core/modules/schema/index.test.d.ts +2 -0
- package/output/core/modules/schema/index.test.d.ts.map +1 -0
- package/output/core/modules/schema/index.test.js +55 -0
- package/output/core/modules/system-routes/index.d.ts +15 -0
- package/output/core/modules/system-routes/index.d.ts.map +1 -0
- package/output/core/modules/system-routes/index.js +205 -0
- package/output/core/resource.d.ts +56 -0
- package/output/core/resource.d.ts.map +1 -0
- package/output/core/resource.js +56 -0
- package/output/core/test-support/database.d.ts +89 -0
- package/output/core/test-support/database.d.ts.map +1 -0
- package/output/core/test-support/database.js +131 -0
- package/output/core/test-support/database.test.d.ts.map +1 -0
- package/output/core/test-support/database.test.js +111 -0
- package/output/core/test-support/environment.d.ts +38 -0
- package/output/core/test-support/environment.d.ts.map +1 -0
- package/output/core/test-support/environment.js +45 -0
- package/output/core/test-support/service-context.d.ts +64 -0
- package/output/core/test-support/service-context.d.ts.map +1 -0
- package/output/core/test-support/service-context.js +63 -0
- package/output/core/test-support/service-context.test.d.ts +2 -0
- package/output/core/test-support/service-context.test.d.ts.map +1 -0
- package/output/core/test-support/service-context.test.js +55 -0
- package/output/core/test-support/test-server.d.ts +154 -0
- package/output/core/test-support/test-server.d.ts.map +1 -0
- package/output/core/test-support/test-server.js +114 -0
- package/output/core/test-support/test-server.test.d.ts +2 -0
- package/output/core/test-support/test-server.test.d.ts.map +1 -0
- package/output/core/test-support/test-server.test.js +296 -0
- package/output/core/test-support/testdata.d.ts +255 -0
- package/output/core/test-support/testdata.d.ts.map +1 -0
- package/output/core/test-support/testdata.js +271 -0
- package/output/core/test-support/testdata.test.d.ts +2 -0
- package/output/core/test-support/testdata.test.d.ts.map +1 -0
- package/output/core/test-support/testdata.test.js +334 -0
- package/output/core/type-augment.d.ts +34 -0
- package/output/core/type-augment.d.ts.map +1 -0
- package/output/core/type-augment.js +7 -0
- package/output/core/types.d.ts +582 -0
- package/output/core/types.d.ts.map +1 -0
- package/output/core/types.js +2 -0
- package/output/cors/index.d.ts +3 -0
- package/output/cors/index.d.ts.map +1 -0
- package/output/cors/index.js +28 -0
- package/output/index.d.ts +10 -4
- package/output/index.d.ts.map +1 -1
- package/output/index.js +25 -5
- package/output/rate-limit/index.d.ts +23 -0
- package/output/rate-limit/index.d.ts.map +1 -0
- package/output/rate-limit/index.js +43 -0
- package/output/{rate-limiter/rate-limiter.d.ts → rate-limit/limiter.d.ts} +1 -1
- package/output/rate-limit/limiter.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.d.ts +6 -0
- package/output/rate-limit/memory-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.js +8 -0
- package/output/{rate-limiter → rate-limit}/redis-store.d.ts +18 -0
- package/output/rate-limit/redis-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.js +35 -0
- package/output/rate-limit/redis-store.test.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.test.js +2 -2
- package/output/rate-limit/types.d.ts +45 -0
- package/output/rate-limit/types.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.d.ts → redis/cache.d.ts} +1 -1
- package/output/redis/cache.d.ts.map +1 -0
- package/output/redis/cache.test.d.ts +2 -0
- package/output/redis/cache.test.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.test.js → redis/cache.test.js} +1 -1
- package/output/redis/index.d.ts +50 -0
- package/output/redis/index.d.ts.map +1 -0
- package/output/redis/index.js +71 -0
- package/output/redis/index.test.d.ts +2 -0
- package/output/{context → redis}/index.test.d.ts.map +1 -1
- package/output/redis/index.test.js +55 -0
- package/output/redis-queue/index.js +1 -1
- package/output/redis-queue/test-helpers.d.ts +1 -1
- package/output/redis-queue/test-helpers.js +1 -1
- package/output/redis-queue/types.d.ts +3 -12
- package/output/redis-queue/types.d.ts.map +1 -1
- package/package.json +11 -7
- package/output/app/assemble.d.ts +0 -16
- package/output/app/assemble.d.ts.map +0 -1
- package/output/app/assemble.js +0 -124
- package/output/app/database.d.ts +0 -36
- package/output/app/database.d.ts.map +0 -1
- package/output/app/database.js +0 -40
- package/output/app/database.test.d.ts.map +0 -1
- package/output/app/database.test.js +0 -66
- package/output/app/default-modules.d.ts +0 -23
- package/output/app/default-modules.d.ts.map +0 -1
- package/output/app/default-modules.js +0 -40
- package/output/app/define-server.d.ts +0 -12
- package/output/app/define-server.d.ts.map +0 -1
- package/output/app/define-server.js +0 -289
- package/output/app/define-server.test.d.ts +0 -2
- package/output/app/define-server.test.d.ts.map +0 -1
- package/output/app/define-server.test.js +0 -299
- package/output/app/env.d.ts +0 -24
- package/output/app/env.d.ts.map +0 -1
- package/output/app/env.js +0 -38
- package/output/app/env.test.js +0 -78
- package/output/app/module.d.ts +0 -88
- package/output/app/module.d.ts.map +0 -1
- package/output/app/module.js +0 -34
- package/output/app/shutdown.d.ts +0 -9
- package/output/app/shutdown.d.ts.map +0 -1
- package/output/app/shutdown.js +0 -15
- package/output/app/shutdown.test.d.ts +0 -2
- package/output/app/shutdown.test.d.ts.map +0 -1
- package/output/app/shutdown.test.js +0 -149
- package/output/app/signal.d.ts +0 -12
- package/output/app/signal.d.ts.map +0 -1
- package/output/app/steps.d.ts +0 -17
- package/output/app/steps.d.ts.map +0 -1
- package/output/app/steps.js +0 -41
- package/output/app/types.d.ts +0 -149
- package/output/app/types.d.ts.map +0 -1
- package/output/context/index.d.ts +0 -6
- package/output/context/index.d.ts.map +0 -1
- package/output/context/index.js +0 -14
- package/output/rate-limiter/index.d.ts +0 -5
- package/output/rate-limiter/index.d.ts.map +0 -1
- package/output/rate-limiter/index.js +0 -4
- package/output/rate-limiter/memory-store.d.ts.map +0 -1
- package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.test.d.ts.map +0 -1
- package/output/rate-limiter/types.d.ts +0 -27
- package/output/rate-limiter/types.d.ts.map +0 -1
- package/output/redis-cache/index.d.ts +0 -4
- package/output/redis-cache/index.d.ts.map +0 -1
- package/output/redis-cache/index.js +0 -1
- package/output/redis-cache/redis-cache.d.ts.map +0 -1
- package/output/redis-cache/redis-cache.test.d.ts +0 -2
- package/output/redis-cache/redis-cache.test.d.ts.map +0 -1
- package/output/schema/index.d.ts.map +0 -1
- /package/output/{app → core}/env.test.d.ts +0 -0
- /package/output/{app → core/modules/auth-provider}/types.js +0 -0
- /package/output/{context → core/modules/context}/index.test.d.ts +0 -0
- /package/output/{context → core/modules/context}/index.test.js +0 -0
- /package/output/{app → core/test-support}/database.test.d.ts +0 -0
- /package/output/{rate-limiter/rate-limiter.js → rate-limit/limiter.js} +0 -0
- /package/output/{rate-limiter → rate-limit}/redis-store.test.d.ts +0 -0
- /package/output/{rate-limiter → rate-limit}/types.js +0 -0
- /package/output/{redis-cache/redis-cache.js → redis/cache.js} +0 -0
package/output/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"AAiBA,cAAc,mCAAmC,CAAA;AACjD,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAC7F,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAC/D,YAAY,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAA;AACrE,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA"}
|
package/output/index.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// 包根出口(`@taicode/common-server`):业务侧直接使用的**构件**——
|
|
2
|
+
// 路由 / 查询 schema 工厂、请求上下文构造、Redis 缓存、限流实现、Redis 队列。
|
|
3
|
+
//
|
|
4
|
+
// 目录布局(见 `common/product/designs/create-server/design.md` §7.1):
|
|
5
|
+
// ```
|
|
6
|
+
// source/
|
|
7
|
+
// ├── core/ 核心设计(锁定:不可替换、不可修改,**不对外导出装配**)
|
|
8
|
+
// ├── app/ 服务入口 barrel(defineServer / startServer / generateOpenApi)
|
|
9
|
+
// ├── auth/ 可选:鉴权中间件
|
|
10
|
+
// ├── cors/ 可选:CORS 中间件
|
|
11
|
+
// ├── rate-limit/ 可选:限流中间件 + 限流实现
|
|
12
|
+
// ├── redis/ 请求期 Redis / 缓存
|
|
13
|
+
// └── redis-queue/ 可选:Redis 队列
|
|
14
|
+
// ```
|
|
15
|
+
//
|
|
16
|
+
// 框架自己装配的一切(`createFastify` / `registerCore` / 各 core 单元 / `request.ctx` 装配 /
|
|
17
|
+
// 默认中间件预设)都不在这里:**固定下来的东西不再交给外面组装**。
|
|
18
|
+
export * from './core/modules/schema/builders.js';
|
|
19
|
+
export { createSuccessResponse, createErrorResponse } from './core/modules/response/index.js';
|
|
20
|
+
export { createContext } from './core/modules/context/index.js';
|
|
21
|
+
export * from './redis/index.js';
|
|
22
|
+
export * from './rate-limit/limiter.js';
|
|
23
|
+
export * from './rate-limit/memory-store.js';
|
|
24
|
+
export * from './rate-limit/redis-store.js';
|
|
25
|
+
export * from './rate-limit/types.js';
|
|
3
26
|
export * from './redis-queue/index.js';
|
|
4
|
-
export * from './redis-cache/index.js';
|
|
5
|
-
export * from './rate-limiter/index.js';
|
|
6
|
-
export * from './context/index.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MiddlewareContext, MiddlewareUnit } from '../core/types.js';
|
|
2
|
+
import type { RateLimitStore } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 标准中间件单元 `rateLimit`(可替换;**不在默认预设内**)。
|
|
5
|
+
*
|
|
6
|
+
* **规则只有一个来源:路由的 `config.rateLimit`**(与 `auth` 的 `accessControls` 同一取向)。
|
|
7
|
+
* 存储后端由调用方注入(缺省进程内 memory)。`TOO_MANY_REQUESTS` 经 `UserError` 仍映射 **400**。
|
|
8
|
+
* 路由未声明 `config.rateLimit` 时该项空转。
|
|
9
|
+
*
|
|
10
|
+
* 注:中间件的 ctx 看不到 `resources`(中间件不得直连数据层),因此 Redis store 需由调用方
|
|
11
|
+
* 在 `services` 侧构造后注入(`store`),框架不再自行探测 Redis。
|
|
12
|
+
*/
|
|
13
|
+
export interface RateLimitMiddlewareOptions {
|
|
14
|
+
/** 存储后端(缺省进程内 memory) */
|
|
15
|
+
store?: RateLimitStore;
|
|
16
|
+
}
|
|
17
|
+
export declare function createRateLimitMiddleware<C = unknown, S = unknown>(_ctx: MiddlewareContext<C, S>, options?: RateLimitMiddlewareOptions): MiddlewareUnit;
|
|
18
|
+
export { RateLimiter, createRateLimitKey } from './limiter.js';
|
|
19
|
+
export { MemoryRateLimitStore } from './memory-store.js';
|
|
20
|
+
export { RedisRateLimitStore } from './redis-store.js';
|
|
21
|
+
export type { RedisRateLimitStoreOptions } from './redis-store.js';
|
|
22
|
+
export type { RateLimitConfig, RateLimitInfo, RateLimiterOptions, RateLimitResult, RateLimitStore } from './types.js';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/rate-limit/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGhD;;;;;;;;;GASG;AAEH,MAAM,WAAW,0BAA0B;IACzC,yBAAyB;IACzB,KAAK,CAAC,EAAE,cAAc,CAAA;CACvB;AAqBD,wBAAgB,yBAAyB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,GAAE,0BAA+B,GAAG,cAAc,CAoB3J;AAGD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AACtD,YAAY,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AAClE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { UserError } from '@taicode/common-base';
|
|
2
|
+
import { createRateLimitKey, RateLimiter } from './limiter.js';
|
|
3
|
+
import { MemoryRateLimitStore } from './memory-store.js';
|
|
4
|
+
import { readServerIdentity } from '../core/modules/identity/index.js';
|
|
5
|
+
function resolveRule(request) {
|
|
6
|
+
return request.routeOptions?.config?.rateLimit;
|
|
7
|
+
}
|
|
8
|
+
function buildKeys(request, keys) {
|
|
9
|
+
return keys.map((key) => {
|
|
10
|
+
if (key === 'ip') {
|
|
11
|
+
return `ip:${request.ip}`;
|
|
12
|
+
}
|
|
13
|
+
if (key === 'user') {
|
|
14
|
+
return `user:${String(request.user?.id ?? 'anonymous')}`;
|
|
15
|
+
}
|
|
16
|
+
if (key === 'path') {
|
|
17
|
+
return `path:${request.routeOptions?.url ?? request.url}`;
|
|
18
|
+
}
|
|
19
|
+
return `${key}:${String(request[key] ?? '')}`;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function createRateLimitMiddleware(_ctx, options = {}) {
|
|
23
|
+
const register = (app) => {
|
|
24
|
+
const limiter = new RateLimiter({ store: options.store ?? new MemoryRateLimitStore() });
|
|
25
|
+
const prefix = `${readServerIdentity(app)?.service ?? ''}:`;
|
|
26
|
+
app.addHook('preValidation', async (request) => {
|
|
27
|
+
const rule = resolveRule(request);
|
|
28
|
+
if (!rule) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const keys = buildKeys(request, rule.keys ?? ['ip']).map((key) => createRateLimitKey(prefix, key));
|
|
32
|
+
const result = await limiter.checkMultiLimit(keys, { limit: rule.limit, window: rule.window });
|
|
33
|
+
if (!result.allowed) {
|
|
34
|
+
throw new UserError('TOO_MANY_REQUESTS', undefined, '请求过于频繁');
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
return { name: 'rateLimit', register };
|
|
39
|
+
}
|
|
40
|
+
// ── 限流实现(与中间件同一模块)─────────────────────────────────
|
|
41
|
+
export { RateLimiter, createRateLimitKey } from './limiter.js';
|
|
42
|
+
export { MemoryRateLimitStore } from './memory-store.js';
|
|
43
|
+
export { RedisRateLimitStore } from './redis-store.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../source/rate-limit/limiter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAkB,MAAM,YAAY,CAAA;AAErH,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAgB;gBAEjB,OAAO,GAAE,kBAAuB;IAItC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IAoC1E,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAI9D;;OAEG;IACG,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;CAezF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAE7E"}
|
|
@@ -7,6 +7,12 @@ export declare class MemoryRateLimitStore implements RateLimitStore {
|
|
|
7
7
|
set(key: string, info: RateLimitInfo): Promise<void>;
|
|
8
8
|
delete(key: string): Promise<void>;
|
|
9
9
|
cleanup(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* 清空全部计数,**保留定时清理器**(它只是 GC,不属于「数据」)。
|
|
12
|
+
*
|
|
13
|
+
* 只影响本实例:进程内 store 的键本来就只在本进程可见。
|
|
14
|
+
*/
|
|
15
|
+
clear(): Promise<void>;
|
|
10
16
|
destroy(): void;
|
|
11
17
|
}
|
|
12
18
|
//# sourceMappingURL=memory-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../source/rate-limit/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAO/D,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,eAAe,CAA8B;gBAEzC,iBAAiB,SAAQ;IAY/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAY/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,OAAO,IAAI,IAAI;CAOhB"}
|
|
@@ -36,6 +36,14 @@ export class MemoryRateLimitStore {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* 清空全部计数,**保留定时清理器**(它只是 GC,不属于「数据」)。
|
|
41
|
+
*
|
|
42
|
+
* 只影响本实例:进程内 store 的键本来就只在本进程可见。
|
|
43
|
+
*/
|
|
44
|
+
async clear() {
|
|
45
|
+
this.store.clear();
|
|
46
|
+
}
|
|
39
47
|
destroy() {
|
|
40
48
|
if (this.cleanupInterval) {
|
|
41
49
|
clearInterval(this.cleanupInterval);
|
|
@@ -39,6 +39,24 @@ export declare class RedisRateLimitStore implements RateLimitStore {
|
|
|
39
39
|
delete(key: string): Promise<void>;
|
|
40
40
|
/** Redis 的键自带过期时间,无需额外清理 */
|
|
41
41
|
cleanup(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* 清空**本前缀下**的全部计数(`SCAN` + `DEL`,不用 `KEYS`——后者在大库上会阻塞)。
|
|
44
|
+
*
|
|
45
|
+
* `fallback` 一并清掉:降级期间写入的计数也是计数,漏清就会留下「只有降级那段没重置」
|
|
46
|
+
* 的诡异状态。
|
|
47
|
+
*
|
|
48
|
+
* 前缀是构造参数,所以它只会删自己这批键,不会碰同一 Redis 上其它用途的数据。
|
|
49
|
+
*
|
|
50
|
+
* ## 已知不足:扫描中途失败只记日志、不抛
|
|
51
|
+
*
|
|
52
|
+
* 与 `get` / `set` 的降级策略保持一致(Redis 抖动不该让业务抛错),但 `clear()` 的
|
|
53
|
+
* 唯一用途是「让下一条用例从零配额开始」:静默失败 = 旧桶保留 = 又变成「顺序相关的
|
|
54
|
+
* 偶发红」,正是本方法要消灭的形态。
|
|
55
|
+
*
|
|
56
|
+
* TODO(需单独评审,行为变更):测试路径上让失败冒泡(或返回 `boolean` 交由
|
|
57
|
+
* `cleanServiceUnits()` 判错),生产路径继续降级。当前 Redis 侧无对应单测。
|
|
58
|
+
*/
|
|
59
|
+
clear(): Promise<void>;
|
|
42
60
|
private buildKey;
|
|
43
61
|
private logError;
|
|
44
62
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../../source/rate-limit/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,OAAO,CAAA;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,WAAW,0BAA0B;IACzC,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kCAAkC;IAClC,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,mBAAoB,YAAW,cAAc;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC,OAAO,CAAC,gBAAgB,CAAI;gBAEhB,OAAO,GAAE,0BAA+B;IAyBvC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAK9B,UAAU,IAAI,IAAI;IAKZ,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAqB/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/C,4BAA4B;IACf,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;;;;;;;;;;;;;OAgBG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBnC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,QAAQ;CAOjB"}
|
|
@@ -101,6 +101,41 @@ export class RedisRateLimitStore {
|
|
|
101
101
|
async cleanup() {
|
|
102
102
|
await this.fallback.cleanup?.();
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* 清空**本前缀下**的全部计数(`SCAN` + `DEL`,不用 `KEYS`——后者在大库上会阻塞)。
|
|
106
|
+
*
|
|
107
|
+
* `fallback` 一并清掉:降级期间写入的计数也是计数,漏清就会留下「只有降级那段没重置」
|
|
108
|
+
* 的诡异状态。
|
|
109
|
+
*
|
|
110
|
+
* 前缀是构造参数,所以它只会删自己这批键,不会碰同一 Redis 上其它用途的数据。
|
|
111
|
+
*
|
|
112
|
+
* ## 已知不足:扫描中途失败只记日志、不抛
|
|
113
|
+
*
|
|
114
|
+
* 与 `get` / `set` 的降级策略保持一致(Redis 抖动不该让业务抛错),但 `clear()` 的
|
|
115
|
+
* 唯一用途是「让下一条用例从零配额开始」:静默失败 = 旧桶保留 = 又变成「顺序相关的
|
|
116
|
+
* 偶发红」,正是本方法要消灭的形态。
|
|
117
|
+
*
|
|
118
|
+
* TODO(需单独评审,行为变更):测试路径上让失败冒泡(或返回 `boolean` 交由
|
|
119
|
+
* `cleanServiceUnits()` 判错),生产路径继续降级。当前 Redis 侧无对应单测。
|
|
120
|
+
*/
|
|
121
|
+
async clear() {
|
|
122
|
+
await this.fallback.clear?.();
|
|
123
|
+
if (!this.redis?.isReady)
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
let cursor = '0';
|
|
127
|
+
do {
|
|
128
|
+
const reply = await this.redis.scan(cursor, { MATCH: `${this.prefix}*`, COUNT: 200 });
|
|
129
|
+
cursor = reply.cursor;
|
|
130
|
+
if (reply.keys.length > 0) {
|
|
131
|
+
await this.redis.del(reply.keys);
|
|
132
|
+
}
|
|
133
|
+
} while (cursor !== '0');
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.logError('clear', error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
104
139
|
buildKey(key) {
|
|
105
140
|
return `${this.prefix}${key}`;
|
|
106
141
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.test.d.ts","sourceRoot":"","sources":["../../source/rate-limit/redis-store.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { createRedisClient } from '../redis
|
|
3
|
-
import { RateLimiter } from './
|
|
2
|
+
import { createRedisClient } from '../redis/cache.js';
|
|
3
|
+
import { RateLimiter } from './limiter.js';
|
|
4
4
|
import { RedisRateLimitStore } from './redis-store.js';
|
|
5
5
|
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
6
6
|
describe('RedisRateLimitStore', () => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface RateLimitInfo {
|
|
2
|
+
count: number;
|
|
3
|
+
windowStart: Date;
|
|
4
|
+
windowEnd: Date;
|
|
5
|
+
}
|
|
6
|
+
export interface RateLimitResult {
|
|
7
|
+
allowed: boolean;
|
|
8
|
+
remaining: number;
|
|
9
|
+
resetAt: Date;
|
|
10
|
+
limit: number;
|
|
11
|
+
window: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RateLimitStore {
|
|
14
|
+
get(key: string): Promise<RateLimitInfo | null>;
|
|
15
|
+
set(key: string, info: RateLimitInfo): Promise<void>;
|
|
16
|
+
delete(key: string): Promise<void>;
|
|
17
|
+
cleanup?(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* **清空全部计数**(不是「删一个键」——那是 `delete`)。
|
|
20
|
+
*
|
|
21
|
+
* ## 为什么这是存储层的职责,而不是「测试自己换个 key」
|
|
22
|
+
*
|
|
23
|
+
* 限流的键是 `ip:` / `user:` / `path:` —— 由请求本身的属性拼成,测试**改不了**(除非
|
|
24
|
+
* 伪造 `remoteAddress`)。没有 `clear()` 的时候,想「从干净配额开始」只能给每次请求换一个
|
|
25
|
+
* 假 IP:一个测试文件里铺一串 `ipSeq` 计数器,注释里还得解释「为什么不能回退序号」。
|
|
26
|
+
*
|
|
27
|
+
* 这完全是**测试在迁就实现细节**:用例真正想表达的是「这条用例从零计数开始」,而它被
|
|
28
|
+
* 迫描述成「这次请用 10.31.0.7 访问」。有了 `clear()`,`beforeEach` 一行搞定,而且
|
|
29
|
+
* 不再需要任何假 IP——测试与真实请求的差别越小,证明力越强。
|
|
30
|
+
*
|
|
31
|
+
* ## 可选,但不是可忽略
|
|
32
|
+
*
|
|
33
|
+
* 声明为可选只为了让**第三方自定义 store** 不必立刻实现它;框架自带的两个实现都有。
|
|
34
|
+
* 测试里用 `store.clear?.()` 而不是硬调。
|
|
35
|
+
*/
|
|
36
|
+
clear?(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export interface RateLimitConfig {
|
|
39
|
+
limit: number;
|
|
40
|
+
window: number;
|
|
41
|
+
}
|
|
42
|
+
export interface RateLimiterOptions {
|
|
43
|
+
store?: RateLimitStore;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../source/rate-limit/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,IAAI,CAAA;IACjB,SAAS,EAAE,IAAI,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,IAAI,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,cAAc,CAAA;CACvB"}
|
|
@@ -136,4 +136,4 @@ export interface CreateRedisClientOptions {
|
|
|
136
136
|
connectTimeoutMs?: number;
|
|
137
137
|
}
|
|
138
138
|
export declare function createRedisClient(options: CreateRedisClientOptions): RedisClientType;
|
|
139
|
-
//# sourceMappingURL=
|
|
139
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../source/redis/cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,OAAO,CAAA;AAE1D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,uBAAuB;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,EAAE,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAA;CACjC;AAaD;;;;;;;;;;GAUG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IAEvC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAA0B;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAEhC,gDAAgD;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAE/D,OAAO,CAAC,gBAAgB,CAAI;gBAEhB,OAAO,EAAE,iBAAiB;IA6BtC,uBAAuB;IACvB,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,sBAAsB;IACtB,IAAW,QAAQ,IAAI,MAAM,CAE5B;IAED,wCAAwC;IACxC,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;;OAGG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrC,0BAA0B;IACnB,UAAU,IAAI,IAAI;IAOzB;;;;;OAKG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrC;;;OAGG;IACI,KAAK,IAAI,IAAI;IAQP,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAItC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/C;;;;;;OAMG;IACU,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB5D;;;;;;OAMG;IACU,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IA6BhG;;;;OAIG;IACH,OAAO,CAAC,QAAQ;IAIhB,8CAA8C;YAChC,aAAa;YAmBb,aAAa;IAY3B,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,QAAQ;CAOjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,eAAe,CAkBpF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../source/redis/cache.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { RedisCache, createRedisClient } from './
|
|
2
|
+
import { RedisCache, createRedisClient } from './cache.js';
|
|
3
3
|
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
4
4
|
describe('RedisCache', () => {
|
|
5
5
|
describe('纯一级缓存(未配置 Redis)', () => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RedisCache } from './cache.js';
|
|
2
|
+
import type { ConfigEnv } from '../core/env.js';
|
|
3
|
+
import type { LoggerLike, RedisHandle } from '../core/types.js';
|
|
4
|
+
/**
|
|
5
|
+
* 引导期 Redis / cache helper(N2 §2.13)。
|
|
6
|
+
*
|
|
7
|
+
* 在 `resources(ctx)` 槽位里按需调用(框架自己**不**创建 Redis);释放**在创建处**登记:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* resources: (ctx) => {
|
|
11
|
+
* const redis = createRedis(ctx.env, ctx.log) // 无 REDIS_URL → undefined
|
|
12
|
+
* if (redis) {
|
|
13
|
+
* ctx.disposer.add(() => redis.dispose()) // 关闭时 resources 阶段最后 flush
|
|
14
|
+
* }
|
|
15
|
+
* return { redis, cache: createCache({ env: ctx.env, prefix: 'account', redis }) }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* `REDIS_URL` 由 helper 直接读 `env`(固定约定),**不**取自 `config`;`redis` / `cache` 一律是
|
|
20
|
+
* **惰性包装**:构造时不建连、不 ping,首次真正使用才建连 —— 「出 spec 不连库/Redis」结构性成立。
|
|
21
|
+
* 释放永远走调用方在 `ctx.disposer` 里的登记,框架不替资源持有句柄。
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* 构造 Redis 句柄(惰性包装:不建连、不 ping)。
|
|
25
|
+
*
|
|
26
|
+
* 无 `REDIS_URL` 时返回 `undefined` —— 资源包里写成 `redis: RedisHandle | undefined` 即可
|
|
27
|
+
* (显式可选,运行时缺省)。
|
|
28
|
+
*/
|
|
29
|
+
export declare function createRedis(env: ConfigEnv, log?: LoggerLike): RedisHandle | undefined;
|
|
30
|
+
/** `createCache` 入参 */
|
|
31
|
+
export interface CacheSetupParams {
|
|
32
|
+
env: ConfigEnv;
|
|
33
|
+
/** 键前缀(通常形如 `<service>:`),避免不同服务 / 环境互相污染 */
|
|
34
|
+
prefix: string;
|
|
35
|
+
/** 复用既有 Redis 句柄(**推荐**:一条连接同时给 cache 与其它消费者) */
|
|
36
|
+
redis?: RedisHandle;
|
|
37
|
+
log?: LoggerLike;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 构造缓存(前缀由调用方给出)。
|
|
41
|
+
*
|
|
42
|
+
* - 传 `redis` → 复用同一个客户端;
|
|
43
|
+
* - 未传 `redis` 但有 `REDIS_URL` → 自建客户端(由 `RedisCache` 自己持有并断开);
|
|
44
|
+
* - 都没有 → 纯进程内一级缓存(不建连)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function createCache(params: CacheSetupParams): RedisCache;
|
|
47
|
+
export { RedisCache, createRedisClient } from './cache.js';
|
|
48
|
+
export type { CreateRedisClientOptions, RedisCacheL1Options, RedisCacheOptions } from './cache.js';
|
|
49
|
+
export type { RedisClientType } from 'redis';
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/redis/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,UAAU,EAAE,MAAM,YAAY,CAAA;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE/D;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CAwBrF;AAED,uBAAuB;AACvB,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,SAAS,CAAA;IACd,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,GAAG,CAAC,EAAE,UAAU,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAYhE;AAGD,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC1D,YAAY,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAClG,YAAY,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRedisClient, RedisCache } from './cache.js';
|
|
2
|
+
/**
|
|
3
|
+
* 引导期 Redis / cache helper(N2 §2.13)。
|
|
4
|
+
*
|
|
5
|
+
* 在 `resources(ctx)` 槽位里按需调用(框架自己**不**创建 Redis);释放**在创建处**登记:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* resources: (ctx) => {
|
|
9
|
+
* const redis = createRedis(ctx.env, ctx.log) // 无 REDIS_URL → undefined
|
|
10
|
+
* if (redis) {
|
|
11
|
+
* ctx.disposer.add(() => redis.dispose()) // 关闭时 resources 阶段最后 flush
|
|
12
|
+
* }
|
|
13
|
+
* return { redis, cache: createCache({ env: ctx.env, prefix: 'account', redis }) }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* `REDIS_URL` 由 helper 直接读 `env`(固定约定),**不**取自 `config`;`redis` / `cache` 一律是
|
|
18
|
+
* **惰性包装**:构造时不建连、不 ping,首次真正使用才建连 —— 「出 spec 不连库/Redis」结构性成立。
|
|
19
|
+
* 释放永远走调用方在 `ctx.disposer` 里的登记,框架不替资源持有句柄。
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* 构造 Redis 句柄(惰性包装:不建连、不 ping)。
|
|
23
|
+
*
|
|
24
|
+
* 无 `REDIS_URL` 时返回 `undefined` —— 资源包里写成 `redis: RedisHandle | undefined` 即可
|
|
25
|
+
* (显式可选,运行时缺省)。
|
|
26
|
+
*/
|
|
27
|
+
export function createRedis(env, log) {
|
|
28
|
+
const redisUrl = env.REDIS_URL;
|
|
29
|
+
if (!redisUrl) {
|
|
30
|
+
log?.debug({}, '[redis] REDIS_URL not configured; cache falls back to in-process L1');
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const client = createRedisClient({ url: redisUrl });
|
|
34
|
+
return {
|
|
35
|
+
client,
|
|
36
|
+
dispose: async () => {
|
|
37
|
+
if (client.isOpen) {
|
|
38
|
+
await client.quit().catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// 未建连(从未使用过)时 `destroy()` 会抛 "The client is closed";释放必须是幂等的
|
|
42
|
+
try {
|
|
43
|
+
client.destroy();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 已关闭:无事可做
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 构造缓存(前缀由调用方给出)。
|
|
54
|
+
*
|
|
55
|
+
* - 传 `redis` → 复用同一个客户端;
|
|
56
|
+
* - 未传 `redis` 但有 `REDIS_URL` → 自建客户端(由 `RedisCache` 自己持有并断开);
|
|
57
|
+
* - 都没有 → 纯进程内一级缓存(不建连)。
|
|
58
|
+
*/
|
|
59
|
+
export function createCache(params) {
|
|
60
|
+
if (params.redis) {
|
|
61
|
+
return new RedisCache({ prefix: params.prefix, redisClient: params.redis.client });
|
|
62
|
+
}
|
|
63
|
+
const redisUrl = params.env.REDIS_URL;
|
|
64
|
+
if (redisUrl) {
|
|
65
|
+
return new RedisCache({ prefix: params.prefix, redisUrl });
|
|
66
|
+
}
|
|
67
|
+
params.log?.debug({}, '[redis] REDIS_URL not configured; cache falls back to in-process L1');
|
|
68
|
+
return new RedisCache({ prefix: params.prefix });
|
|
69
|
+
}
|
|
70
|
+
// ── 缓存 / 客户端实现(与引导期 helper 同一模块)───────────────
|
|
71
|
+
export { RedisCache, createRedisClient } from './cache.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../source/
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../source/redis/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { RedisCache } from './cache.js';
|
|
3
|
+
import { createCache, createRedis } from './index.js';
|
|
4
|
+
function createRecordingLogger() {
|
|
5
|
+
const entries = [];
|
|
6
|
+
const record = (level) => (obj, msg) => {
|
|
7
|
+
entries.push({ level, obj, msg });
|
|
8
|
+
};
|
|
9
|
+
return { entries, debug: record('debug'), info: record('info'), warn: record('warn'), error: record('error') };
|
|
10
|
+
}
|
|
11
|
+
/** 测试用 `ConfigEnv`(固定读取约定:`ENV` / `API_PORT` / `LOG_LEVEL` / `DATABASE_URL` / `REDIS_URL`) */
|
|
12
|
+
function createTestConfigEnv(overrides = {}) {
|
|
13
|
+
return { ENV: 'test', API_PORT: '', LOG_LEVEL: 'info', DATABASE_URL: '', REDIS_URL: undefined, ...overrides };
|
|
14
|
+
}
|
|
15
|
+
describe('createRedis', () => {
|
|
16
|
+
it('无 REDIS_URL → undefined,并留一条 debug 线索', () => {
|
|
17
|
+
const log = createRecordingLogger();
|
|
18
|
+
const handle = createRedis(createTestConfigEnv({ REDIS_URL: undefined }), log);
|
|
19
|
+
expect(handle).toBeUndefined();
|
|
20
|
+
expect(log.entries.filter((entry) => entry.level === 'debug')).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
it('有 REDIS_URL → 句柄(client + dispose),构造期不建连', () => {
|
|
23
|
+
const handle = createRedis(createTestConfigEnv({ REDIS_URL: 'redis://127.0.0.1:6379/0' }));
|
|
24
|
+
expect(handle).toBeDefined();
|
|
25
|
+
expect(handle?.client).toBeDefined();
|
|
26
|
+
expect(typeof handle?.dispose).toBe('function');
|
|
27
|
+
// 惰性:尚未 connect,客户端未打开
|
|
28
|
+
expect((handle?.client).isOpen).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it('dispose:未建连时直接销毁客户端,不抛错、可重复调用', async () => {
|
|
31
|
+
const handle = createRedis(createTestConfigEnv({ REDIS_URL: 'redis://127.0.0.1:6379/0' }));
|
|
32
|
+
await expect(handle?.dispose()).resolves.toBeUndefined();
|
|
33
|
+
await expect(handle?.dispose()).resolves.toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('createCache', () => {
|
|
37
|
+
it('无 redis 句柄、无 REDIS_URL → 纯进程内 L1(不建连)', () => {
|
|
38
|
+
const log = createRecordingLogger();
|
|
39
|
+
const cache = createCache({ env: createTestConfigEnv({ REDIS_URL: undefined }), prefix: 'test:', log });
|
|
40
|
+
expect(cache).toBeInstanceOf(RedisCache);
|
|
41
|
+
expect(log.entries.filter((entry) => entry.level === 'debug')).toHaveLength(1);
|
|
42
|
+
});
|
|
43
|
+
it('复用传入的 redis 句柄(一条连接同时给 cache 与其它消费者)', () => {
|
|
44
|
+
const handle = createRedis(createTestConfigEnv({ REDIS_URL: 'redis://127.0.0.1:6379/0' }));
|
|
45
|
+
const cache = createCache({ env: createTestConfigEnv(), prefix: 'test:', redis: handle });
|
|
46
|
+
expect(cache).toBeInstanceOf(RedisCache);
|
|
47
|
+
});
|
|
48
|
+
it('无句柄但有 REDIS_URL → 自建客户端(由 RedisCache 自己持有)', () => {
|
|
49
|
+
const cache = createCache({
|
|
50
|
+
env: createTestConfigEnv({ REDIS_URL: 'redis://127.0.0.1:6379/0' }),
|
|
51
|
+
prefix: 'test:',
|
|
52
|
+
});
|
|
53
|
+
expect(cache).toBeInstanceOf(RedisCache);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
export { RedisQueueProvider } from './redis-queue-provider.js';
|
|
3
3
|
export { RedisQueueConsumer } from './redis-queue-consumer.js';
|
|
4
4
|
export { RedisQueueBatchConsumer } from './redis-queue-batch-consumer.js';
|
|
5
|
-
//
|
|
5
|
+
// 测试辅助函数导出(与队列实现同一出口:谁提供的功能,谁提供配套的测试工具)
|
|
6
6
|
export * as test from './test-helpers.js';
|
|
@@ -14,7 +14,7 @@ import type { AnyRegistry, QueueStats, RedisQueueRegistry, Task } from './types.
|
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```ts
|
|
17
|
-
* import { dispatchQueueTask } from '@taicode/common-server/redis-queue
|
|
17
|
+
* import { dispatchQueueTask } from '@taicode/common-server/redis-queue'
|
|
18
18
|
* import { RedisQueueProvider, RedisQueueConsumer } from '@taicode/common-server/redis-queue'
|
|
19
19
|
*
|
|
20
20
|
* describe('My Queue Tests', () => {
|
|
@@ -11,7 +11,7 @@ import { RedisQueueConsumer } from './redis-queue-consumer.js';
|
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```ts
|
|
14
|
-
* import { dispatchQueueTask } from '@taicode/common-server/redis-queue
|
|
14
|
+
* import { dispatchQueueTask } from '@taicode/common-server/redis-queue'
|
|
15
15
|
* import { RedisQueueProvider, RedisQueueConsumer } from '@taicode/common-server/redis-queue'
|
|
16
16
|
*
|
|
17
17
|
* describe('My Queue Tests', () => {
|
|
@@ -109,24 +109,18 @@ export interface RedisQueueCommonConfig {
|
|
|
109
109
|
/**
|
|
110
110
|
* 队列生产者配置
|
|
111
111
|
*/
|
|
112
|
-
export interface RedisQueueProviderConfig {
|
|
113
|
-
redisUrl?: string;
|
|
114
|
-
redisClient?: RedisClientType;
|
|
115
|
-
cleanupDelay?: number;
|
|
112
|
+
export interface RedisQueueProviderConfig extends RedisQueueCommonConfig {
|
|
116
113
|
processingDelay?: number;
|
|
117
114
|
}
|
|
118
115
|
/**
|
|
119
116
|
* 队列消费者配置
|
|
120
117
|
*/
|
|
121
|
-
export interface RedisQueueConsumerConfig<T extends TaskData = TaskData> {
|
|
122
|
-
redisUrl?: string;
|
|
123
|
-
redisClient?: RedisClientType;
|
|
118
|
+
export interface RedisQueueConsumerConfig<T extends TaskData = TaskData> extends RedisQueueCommonConfig {
|
|
124
119
|
handler: TaskHandler<T>;
|
|
125
120
|
maxRetries?: number;
|
|
126
121
|
concurrency?: number;
|
|
127
122
|
consumerInterval?: number;
|
|
128
123
|
processingTimeout?: number;
|
|
129
|
-
cleanupDelay?: number;
|
|
130
124
|
}
|
|
131
125
|
/**
|
|
132
126
|
* 任务处理器函数
|
|
@@ -135,14 +129,11 @@ export type TaskHandler<T extends TaskData = TaskData> = (data: T) => Promise<Ca
|
|
|
135
129
|
/**
|
|
136
130
|
* 批量消费者配置
|
|
137
131
|
*/
|
|
138
|
-
export interface BatchConsumerConfig<T extends TaskData = TaskData> {
|
|
139
|
-
redisUrl?: string;
|
|
140
|
-
redisClient?: RedisClientType;
|
|
132
|
+
export interface BatchConsumerConfig<T extends TaskData = TaskData> extends RedisQueueCommonConfig {
|
|
141
133
|
handler: BatchTaskHandler<T>;
|
|
142
134
|
batchSize?: number;
|
|
143
135
|
maxRetries?: number;
|
|
144
136
|
concurrency?: number;
|
|
145
|
-
cleanupDelay?: number;
|
|
146
137
|
consumerInterval?: number;
|
|
147
138
|
processingTimeout?: number;
|
|
148
139
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../source/redis-queue/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,MAAM,WAAW,kBAAkB;CAElC;AAED;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAA;AAEtE;;;;;GAKG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAE7C;;GAEG;AAEH,MAAM,MAAM,QAAQ,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAA;AAE1D;;GAEG;AACH,MAAM,WAAW,IAAI,CAAC,CAAC,GAAG,QAAQ;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,CAAC,CAAA;IACP,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../source/redis-queue/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,MAAM,WAAW,kBAAkB;CAElC;AAED;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAA;AAEtE;;;;;GAKG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAE7C;;GAEG;AAEH,MAAM,MAAM,QAAQ,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAA;AAE1D;;GAEG;AACH,MAAM,WAAW,IAAI,CAAC,CAAC,GAAG,QAAQ;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,CAAC,CAAA;IACP,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAyB,SAAQ,sBAAsB;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,CAAE,SAAQ,sBAAsB;IACrG,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA;AAE3G;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,CAAE,SAAQ,sBAAsB;IAChG,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA"}
|