@taicode/common-server 3.8.0 → 4.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/output/app/auth.test.d.ts +2 -0
  2. package/output/app/auth.test.d.ts.map +1 -0
  3. package/output/app/auth.test.js +148 -0
  4. package/output/app/boot.d.ts +39 -0
  5. package/output/app/boot.d.ts.map +1 -0
  6. package/output/app/boot.js +140 -0
  7. package/output/app/config.test.d.ts +2 -0
  8. package/output/app/config.test.d.ts.map +1 -0
  9. package/output/app/config.test.js +132 -0
  10. package/output/app/context.d.ts +70 -0
  11. package/output/app/context.d.ts.map +1 -0
  12. package/output/app/context.js +73 -0
  13. package/output/app/core/cookie.d.ts +25 -0
  14. package/output/app/core/cookie.d.ts.map +1 -0
  15. package/output/app/core/cookie.js +22 -0
  16. package/output/app/core/identity.d.ts +12 -0
  17. package/output/app/core/identity.d.ts.map +1 -0
  18. package/output/app/core/identity.js +14 -0
  19. package/output/app/core/index.d.ts +44 -0
  20. package/output/app/core/index.d.ts.map +1 -0
  21. package/output/app/core/index.js +57 -0
  22. package/output/app/core/logging.d.ts +13 -0
  23. package/output/app/core/logging.d.ts.map +1 -0
  24. package/output/app/core/logging.js +29 -0
  25. package/output/app/core/openapi.d.ts +17 -0
  26. package/output/app/core/openapi.d.ts.map +1 -0
  27. package/output/app/core/openapi.js +42 -0
  28. package/output/app/core/request-id.d.ts +36 -0
  29. package/output/app/core/request-id.d.ts.map +1 -0
  30. package/output/app/core/request-id.js +66 -0
  31. package/output/app/core/response.d.ts +41 -0
  32. package/output/app/core/response.d.ts.map +1 -0
  33. package/output/app/core/response.js +89 -0
  34. package/output/app/define-server.negative-cases.d.ts +40 -0
  35. package/output/app/define-server.negative-cases.d.ts.map +1 -0
  36. package/output/app/define-server.negative-cases.js +268 -0
  37. package/output/app/define-server.test.js +86 -288
  38. package/output/app/entries.d.ts +29 -0
  39. package/output/app/entries.d.ts.map +1 -0
  40. package/output/app/entries.js +64 -0
  41. package/output/app/env.d.ts +39 -4
  42. package/output/app/env.d.ts.map +1 -1
  43. package/output/app/env.js +32 -4
  44. package/output/app/env.test.js +67 -21
  45. package/output/app/error-contract.test.d.ts +2 -0
  46. package/output/app/error-contract.test.d.ts.map +1 -0
  47. package/output/app/error-contract.test.js +227 -0
  48. package/output/app/errors.d.ts +20 -0
  49. package/output/app/errors.d.ts.map +1 -0
  50. package/output/app/errors.js +51 -0
  51. package/output/app/generate-openapi.test.d.ts +2 -0
  52. package/output/app/generate-openapi.test.d.ts.map +1 -0
  53. package/output/app/generate-openapi.test.js +123 -0
  54. package/output/app/index.d.ts +15 -15
  55. package/output/app/index.d.ts.map +1 -1
  56. package/output/app/index.js +18 -9
  57. package/output/app/lifecycle-logging.test.d.ts +2 -0
  58. package/output/app/lifecycle-logging.test.d.ts.map +1 -0
  59. package/output/app/lifecycle-logging.test.js +155 -0
  60. package/output/app/lifecycle.d.ts +70 -0
  61. package/output/app/lifecycle.d.ts.map +1 -0
  62. package/output/app/lifecycle.js +99 -0
  63. package/output/app/lifecycle.test.d.ts +2 -0
  64. package/output/app/lifecycle.test.d.ts.map +1 -0
  65. package/output/app/lifecycle.test.js +152 -0
  66. package/output/app/middleware/auth.d.ts +71 -0
  67. package/output/app/middleware/auth.d.ts.map +1 -0
  68. package/output/app/middleware/auth.js +190 -0
  69. package/output/app/middleware/context.d.ts +13 -0
  70. package/output/app/middleware/context.d.ts.map +1 -0
  71. package/output/app/middleware/context.js +15 -0
  72. package/output/app/middleware/cors.d.ts +14 -0
  73. package/output/app/middleware/cors.d.ts.map +1 -0
  74. package/output/app/middleware/cors.js +30 -0
  75. package/output/app/middleware/index.d.ts +17 -0
  76. package/output/app/middleware/index.d.ts.map +1 -0
  77. package/output/app/middleware/index.js +16 -0
  78. package/output/app/middleware/rate-limit.d.ts +24 -0
  79. package/output/app/middleware/rate-limit.d.ts.map +1 -0
  80. package/output/app/middleware/rate-limit.js +46 -0
  81. package/output/app/middleware-order.test.d.ts +2 -0
  82. package/output/app/middleware-order.test.d.ts.map +1 -0
  83. package/output/app/middleware-order.test.js +148 -0
  84. package/output/app/permission-operations.d.ts +48 -0
  85. package/output/app/permission-operations.d.ts.map +1 -0
  86. package/output/app/permission-operations.js +77 -0
  87. package/output/app/permission-operations.test.d.ts +2 -0
  88. package/output/app/permission-operations.test.d.ts.map +1 -0
  89. package/output/app/permission-operations.test.js +72 -0
  90. package/output/app/permission-registration.test.d.ts +2 -0
  91. package/output/app/permission-registration.test.d.ts.map +1 -0
  92. package/output/app/permission-registration.test.js +138 -0
  93. package/output/app/redis.d.ts +45 -0
  94. package/output/app/redis.d.ts.map +1 -0
  95. package/output/app/redis.js +61 -0
  96. package/output/app/registered-operations.test.d.ts +2 -0
  97. package/output/app/registered-operations.test.d.ts.map +1 -0
  98. package/output/app/registered-operations.test.js +57 -0
  99. package/output/app/request-id.test.d.ts +2 -0
  100. package/output/app/request-id.test.d.ts.map +1 -0
  101. package/output/app/request-id.test.js +130 -0
  102. package/output/app/resources.test.d.ts +2 -0
  103. package/output/app/resources.test.d.ts.map +1 -0
  104. package/output/app/resources.test.js +199 -0
  105. package/output/app/shutdown.d.ts +17 -4
  106. package/output/app/shutdown.d.ts.map +1 -1
  107. package/output/app/shutdown.js +27 -12
  108. package/output/app/shutdown.test.js +78 -124
  109. package/output/app/signal.d.ts +4 -4
  110. package/output/app/signal.d.ts.map +1 -1
  111. package/output/app/signal.js +3 -3
  112. package/output/app/smoke.test.d.ts +2 -0
  113. package/output/app/smoke.test.d.ts.map +1 -0
  114. package/output/app/smoke.test.js +210 -0
  115. package/output/app/test-support.d.ts +21 -0
  116. package/output/app/test-support.d.ts.map +1 -0
  117. package/output/app/test-support.js +24 -0
  118. package/output/app/type-augment.d.ts +34 -0
  119. package/output/app/type-augment.d.ts.map +1 -0
  120. package/output/app/type-augment.js +7 -0
  121. package/output/app/types.d.ts +205 -126
  122. package/output/app/types.d.ts.map +1 -1
  123. package/output/app/types.js +2 -1
  124. package/package.json +10 -6
  125. package/output/app/assemble.d.ts +0 -16
  126. package/output/app/assemble.d.ts.map +0 -1
  127. package/output/app/assemble.js +0 -124
  128. package/output/app/database.d.ts +0 -36
  129. package/output/app/database.d.ts.map +0 -1
  130. package/output/app/database.js +0 -40
  131. package/output/app/database.test.d.ts +0 -2
  132. package/output/app/database.test.d.ts.map +0 -1
  133. package/output/app/database.test.js +0 -66
  134. package/output/app/default-modules.d.ts +0 -23
  135. package/output/app/default-modules.d.ts.map +0 -1
  136. package/output/app/default-modules.js +0 -40
  137. package/output/app/define-server.d.ts +0 -12
  138. package/output/app/define-server.d.ts.map +0 -1
  139. package/output/app/define-server.js +0 -289
  140. package/output/app/module.d.ts +0 -88
  141. package/output/app/module.d.ts.map +0 -1
  142. package/output/app/module.js +0 -34
  143. package/output/app/steps.d.ts +0 -17
  144. package/output/app/steps.d.ts.map +0 -1
  145. package/output/app/steps.js +0 -41
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defineServer, generateOpenApi, startServer } from './entries.js';
3
+ import { createContextMiddleware } from './middleware/context.js';
4
+ import { readRegisteredOperations, toPermissionOperations } from './permission-operations.js';
5
+ /**
6
+ * 路由注册表 + swagger 时序。
7
+ *
8
+ * 关键点:自定义 `middlewares`(**不含 cors**)时,swagger 插件体也必须在任何路由注册之前执行完
9
+ * (`registerOpenApi` 内 `await app.after()`),路由注册表同样非空——两者都不依赖「恰好有 cors
10
+ * 顺带冲刷队列」这种隐式巧合。
11
+ */
12
+ const listWidgetSchema = {
13
+ operationId: 'listWidget',
14
+ summary: 'List widgets',
15
+ tags: ['Widget'],
16
+ };
17
+ /** 未声明 `operationId` 的路由(回落 url,且不进权限目录) */
18
+ const plainSchema = {};
19
+ function createServer() {
20
+ return defineServer({
21
+ app: { service: 'registry', title: 'Registry', version: '1.0.0' },
22
+ resources: () => ({}),
23
+ config: () => ({}),
24
+ services: () => ({}),
25
+ // 自定义中间件:只有 context,**没有 cors**(不得靠 cors 冲刷 swagger 队列)
26
+ middlewares: () => [createContextMiddleware()],
27
+ routers: (ctx) => {
28
+ ctx.app.get('/widget/list', { schema: listWidgetSchema }, () => ({ items: [] }));
29
+ ctx.app.post('/widget/plain', { schema: plainSchema }, () => ({ ok: true }));
30
+ },
31
+ });
32
+ }
33
+ describe('路由注册表', () => {
34
+ it('自定义 middlewares(无 cors)时,注册表非空且能拿到 operationId', async () => {
35
+ const app = await startServer(createServer(), { listen: false });
36
+ try {
37
+ const operations = readRegisteredOperations(app);
38
+ expect(operations.length).toBeGreaterThan(0);
39
+ const listWidget = operations.find((entry) => entry.url === '/widget/list');
40
+ expect(listWidget).toMatchObject({ method: 'GET', url: '/widget/list', operationId: 'listWidget' });
41
+ // Fastify 为 GET 自动生成的 HEAD 路由不进注册表(与 swagger 同口径)
42
+ expect(operations.some((entry) => entry.method === 'HEAD')).toBe(false);
43
+ // 未声明 operationId 的路由不进权限目录
44
+ expect(toPermissionOperations(operations)).toEqual([
45
+ { operation: 'listWidget', summary: 'List widgets', description: '', tags: ['Widget'] },
46
+ ]);
47
+ }
48
+ finally {
49
+ await app.close();
50
+ }
51
+ });
52
+ it('自定义 middlewares(无 cors)时,swagger 文档仍包含已注册路由(确定性冲刷)', async () => {
53
+ const spec = (await generateOpenApi(createServer()));
54
+ const paths = spec.paths ?? {};
55
+ expect(Object.keys(paths)).toEqual(expect.arrayContaining(['/widget/list', '/widget/plain']));
56
+ });
57
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=request-id.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-id.test.d.ts","sourceRoot":"","sources":["../../source/app/request-id.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createRequestIdOptions, normalizeInboundRequestId, REQUEST_ID_HEADER, REQUEST_ID_LOG_LABEL, } from './core/request-id.js';
3
+ import { defineServer, startServer } from './entries.js';
4
+ /**
5
+ * `requestId` 契约(设计 §5.1 / §8)——**框架内置、锁定**。
6
+ *
7
+ * 入站 `x-request-id` 存在且非空 → 复用,否则 `randomUUID`;响应 header 统一回吐
8
+ * `x-request-id`;**不进响应体**;日志字段键名统一为全拼 `requestId`。
9
+ */
10
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
11
+ function createRequestIdServer() {
12
+ return defineServer({
13
+ app: { service: 'request-id-test', title: 'Request Id Test', version: '1.0.0' },
14
+ resources: () => ({}),
15
+ config: () => ({}),
16
+ services: () => ({}),
17
+ middlewares: () => [],
18
+ routers: (ctx) => {
19
+ ctx.app.get('/echo', () => ({ ok: true }));
20
+ },
21
+ });
22
+ }
23
+ /** 启动一个可读取请求日志 bindings 的 app(`x-request-id` 归一化断言用) */
24
+ async function startBindingApp() {
25
+ let bindings;
26
+ const server = defineServer({
27
+ app: { service: 'request-id-normalize', title: 'Request Id Normalize', version: '1.0.0' },
28
+ resources: () => ({}),
29
+ config: () => ({}),
30
+ services: () => ({}),
31
+ middlewares: () => [],
32
+ routers: (ctx) => {
33
+ ctx.app.addHook('onRequest', (request, _reply, done) => {
34
+ bindings = request.log.bindings();
35
+ done();
36
+ });
37
+ ctx.app.get('/echo', () => ({ ok: true }));
38
+ },
39
+ });
40
+ const app = await startServer(server, { listen: false });
41
+ return { app, getBindings: () => bindings };
42
+ }
43
+ describe('requestId(设计 §5.1 / §8)', () => {
44
+ it('固定 header 名与日志字段名', () => {
45
+ expect(REQUEST_ID_HEADER).toBe('x-request-id');
46
+ expect(REQUEST_ID_LOG_LABEL).toBe('requestId');
47
+ const options = createRequestIdOptions();
48
+ expect(options.requestIdHeader).toBe('x-request-id');
49
+ expect(options.requestIdLogLabel).toBe('requestId');
50
+ });
51
+ it('genReqId:入站值非空则复用,否则生成 UUID', () => {
52
+ const { genReqId } = createRequestIdOptions();
53
+ expect(genReqId({ headers: { 'x-request-id': 'abc' } })).toBe('abc');
54
+ expect(genReqId({ headers: { 'x-request-id': ['abc', 'def'] } })).toBe('abc');
55
+ expect(genReqId({ headers: {} })).toMatch(UUID_PATTERN);
56
+ expect(genReqId({ headers: { 'x-request-id': ' ' } })).toMatch(UUID_PATTERN);
57
+ });
58
+ it('normalizeInboundRequestId:trim 后为空视为缺失(undefined),非空返回 trim 值', () => {
59
+ expect(normalizeInboundRequestId(undefined)).toBeUndefined();
60
+ expect(normalizeInboundRequestId('')).toBeUndefined();
61
+ expect(normalizeInboundRequestId(' ')).toBeUndefined();
62
+ expect(normalizeInboundRequestId('trace-abc')).toBe('trace-abc');
63
+ expect(normalizeInboundRequestId(' trace-abc ')).toBe('trace-abc');
64
+ expect(normalizeInboundRequestId(['abc', 'def'])).toBe('abc');
65
+ });
66
+ it('空白入站 header → 视为缺失,重新生成 UUID(request.id / 日志字段 / 响应 header 一致)', async () => {
67
+ const { app, getBindings } = await startBindingApp();
68
+ const response = await app.inject({ method: 'GET', url: '/echo', headers: { [REQUEST_ID_HEADER]: ' ' } });
69
+ expect(response.statusCode).toBe(200);
70
+ const requestId = String(response.headers[REQUEST_ID_HEADER]);
71
+ expect(requestId).toMatch(UUID_PATTERN);
72
+ expect(getBindings()).toEqual({ [REQUEST_ID_LOG_LABEL]: requestId });
73
+ await app.close();
74
+ });
75
+ it('带空格的入站 header → trim 后复用(request.id / 日志字段 / 响应 header 一致)', async () => {
76
+ const { app, getBindings } = await startBindingApp();
77
+ const response = await app.inject({
78
+ method: 'GET',
79
+ url: '/echo',
80
+ headers: { [REQUEST_ID_HEADER]: ' trace-trim ' },
81
+ });
82
+ expect(response.statusCode).toBe(200);
83
+ expect(response.headers[REQUEST_ID_HEADER]).toBe('trace-trim');
84
+ expect(getBindings()).toEqual({ [REQUEST_ID_LOG_LABEL]: 'trace-trim' });
85
+ await app.close();
86
+ });
87
+ it('缺失入站 header → 生成 UUID(request.id / 日志字段 / 响应 header 一致)', async () => {
88
+ const { app, getBindings } = await startBindingApp();
89
+ const response = await app.inject({ method: 'GET', url: '/echo' });
90
+ expect(response.statusCode).toBe(200);
91
+ const requestId = String(response.headers[REQUEST_ID_HEADER]);
92
+ expect(requestId).toMatch(UUID_PATTERN);
93
+ expect(getBindings()).toEqual({ [REQUEST_ID_LOG_LABEL]: requestId });
94
+ await app.close();
95
+ });
96
+ it('复用入站 header 并回吐到响应 header;日志绑定字段为 requestId;不进响应体', async () => {
97
+ let bindings;
98
+ const server = defineServer({
99
+ app: { service: 'request-id-integration', title: 'Request Id Integration', version: '1.0.0' },
100
+ resources: () => ({}),
101
+ config: () => ({}),
102
+ services: () => ({}),
103
+ middlewares: () => [],
104
+ routers: (ctx) => {
105
+ ctx.app.addHook('onRequest', (request, _reply, done) => {
106
+ bindings = request.log.bindings();
107
+ done();
108
+ });
109
+ ctx.app.get('/echo', () => ({ ok: true }));
110
+ },
111
+ });
112
+ const app = await startServer(server, { listen: false });
113
+ const reused = await app.inject({ method: 'GET', url: '/echo', headers: { 'x-request-id': 'trace-abc' } });
114
+ expect(reused.statusCode).toBe(200);
115
+ expect(reused.headers[REQUEST_ID_HEADER]).toBe('trace-abc');
116
+ expect(bindings).toEqual({ requestId: 'trace-abc' });
117
+ expect(reused.body).not.toContain('trace-abc');
118
+ const generated = await app.inject({ method: 'GET', url: '/echo' });
119
+ expect(String(generated.headers[REQUEST_ID_HEADER])).toMatch(UUID_PATTERN);
120
+ expect(bindings).toHaveProperty(REQUEST_ID_LOG_LABEL);
121
+ await app.close();
122
+ });
123
+ it('startServer 冒烟:定义可跑、路由可用', async () => {
124
+ const app = await startServer(createRequestIdServer(), { listen: false });
125
+ const response = await app.inject({ method: 'GET', url: '/echo' });
126
+ expect(response.statusCode).toBe(200);
127
+ expect(response.json()).toEqual({ status: 'SUCCESS', message: 'ok', data: { ok: true } });
128
+ await app.close();
129
+ });
130
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=resources.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.test.d.ts","sourceRoot":"","sources":["../../source/app/resources.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,199 @@
1
+ import { describe, expect, expectTypeOf, it } from 'vitest';
2
+ import { defineServer, generateOpenApi, startServer } from './entries.js';
3
+ import { createTestConfigEnv } from './test-support.js';
4
+ /** 资源包实现(显式标注返回类型:否则资源实现的回调参数会吃隐式 `any`) */
5
+ function createResources() {
6
+ return {
7
+ database: { query: (sql) => `rows:${sql}` },
8
+ cache: { read: (key) => `${key}:cached` },
9
+ telemetry: false,
10
+ queue: undefined,
11
+ };
12
+ }
13
+ // ── 类型自证:`false` 键在 `Live<TestResources>` 中被剔除 ─────────
14
+ const assertLiveKeysRemovesFalse = () => {
15
+ expectTypeOf().toEqualTypeOf();
16
+ };
17
+ void assertLiveKeysRemovesFalse;
18
+ describe('resources 槽位(N1 §2 / N2 §2.3)', () => {
19
+ it('① 入参为 env + log + registerClassifyError,且资源嵌套在 ctx.resources 下(config / services 都能用)', async () => {
20
+ let captured;
21
+ const observed = {};
22
+ const server = defineServer({
23
+ app: { service: 'resources-basic', title: 'Resources Basic', version: '1.0.0' },
24
+ resources: (ctx) => {
25
+ captured = ctx;
26
+ return createResources();
27
+ },
28
+ config: (ctx) => {
29
+ // 类型自证:精确到资源包里的声明类型(不是 unknown / any)
30
+ expectTypeOf(ctx.resources.database.query).toEqualTypeOf();
31
+ observed.inConfig = ctx.resources.database.query('select 1');
32
+ return { domain: 'example.com' };
33
+ },
34
+ services: (ctx) => {
35
+ expectTypeOf(ctx.resources.cache.read).toEqualTypeOf();
36
+ observed.inServices = ctx.resources.cache.read('domain');
37
+ return {};
38
+ },
39
+ middlewares: () => [],
40
+ routers: (ctx) => {
41
+ ctx.app.get('/resource', () => ({ ok: true }));
42
+ },
43
+ });
44
+ const app = await startServer(server, { listen: false });
45
+ try {
46
+ expect(Object.keys(captured ?? {}).sort()).toEqual(['env', 'log', 'registerClassifyError']);
47
+ expect(captured?.env.ENV).toBeTypeOf('string');
48
+ expect(typeof captured?.log.info).toBe('function');
49
+ // resources 槽位专属能力:每个资源注册自己的错误分类器(函数)
50
+ expect(typeof captured?.registerClassifyError).toBe('function');
51
+ expect(observed.inConfig).toBe('rows:select 1');
52
+ expect(observed.inServices).toBe('domain:cached');
53
+ const response = await app.inject({ method: 'GET', url: '/resource' });
54
+ expect(response.statusCode).toBe(200);
55
+ }
56
+ finally {
57
+ await app.close();
58
+ }
59
+ });
60
+ it('② `false` 关闭资源:装配正常,键在类型与运行时都不存在', async () => {
61
+ let ctxInServices;
62
+ const server = defineServer({
63
+ app: { service: 'resources-disabled', title: 'Resources Disabled', version: '1.0.0' },
64
+ resources: createResources,
65
+ config: () => ({}),
66
+ services: (ctx) => {
67
+ // 编译期:`telemetry` 被 `Live<>` 剔除 → 访问必须编译失败
68
+ // @ts-expect-error `telemetry: false` 已从资源视图中剔除
69
+ void ctx.resources.telemetry;
70
+ ctxInServices = ctx;
71
+ return {};
72
+ },
73
+ middlewares: () => [],
74
+ routers: (ctx) => {
75
+ ctx.app.get('/alive', () => ({ ok: true }));
76
+ },
77
+ });
78
+ const app = await startServer(server, { listen: false });
79
+ try {
80
+ // 运行时也不存在该键(与类型一致)
81
+ const resources = ctxInServices.resources;
82
+ expect('telemetry' in resources).toBe(false);
83
+ const response = await app.inject({ method: 'GET', url: '/alive' });
84
+ expect(response.statusCode).toBe(200);
85
+ }
86
+ finally {
87
+ await app.close();
88
+ }
89
+ });
90
+ it('③ 缺省可选资源:类型为 `T | undefined`,运行时为 undefined(键仍在)', async () => {
91
+ const observed = {};
92
+ const server = defineServer({
93
+ app: { service: 'resources-optional', title: 'Resources Optional', version: '1.0.0' },
94
+ resources: createResources,
95
+ config: (ctx) => {
96
+ expectTypeOf(ctx.resources.queue).toEqualTypeOf();
97
+ observed.inConfig = ctx.resources.queue;
98
+ return {};
99
+ },
100
+ services: (ctx) => {
101
+ observed.inServices = ctx.resources.queue;
102
+ observed.keys = Object.keys(ctx.resources);
103
+ return {};
104
+ },
105
+ middlewares: () => [],
106
+ routers: () => undefined,
107
+ });
108
+ const app = await startServer(server, { listen: false });
109
+ try {
110
+ expect(observed.inConfig).toBeUndefined();
111
+ expect(observed.inServices).toBeUndefined();
112
+ expect(observed.keys).toContain('queue');
113
+ }
114
+ finally {
115
+ await app.close();
116
+ }
117
+ });
118
+ it('④ resources 允许异步:返回 Promise<资源包> 时同样是精确类型', async () => {
119
+ let domain;
120
+ const server = defineServer({
121
+ app: { service: 'resources-async', title: 'Resources Async', version: '1.0.0' },
122
+ resources: async () => ({
123
+ database: { query: (sql) => `async:${sql}` },
124
+ }),
125
+ config: (ctx) => {
126
+ expectTypeOf(ctx.resources.database.query).toEqualTypeOf();
127
+ return { domain: ctx.resources.database.query('domain') };
128
+ },
129
+ services: (ctx) => {
130
+ domain = ctx.config.domain;
131
+ return {};
132
+ },
133
+ middlewares: () => [],
134
+ routers: (ctx) => {
135
+ ctx.app.get('/ping', () => ({ ok: true }));
136
+ },
137
+ });
138
+ const app = await startServer(server, { listen: false });
139
+ try {
140
+ expect(domain).toBe('async:domain');
141
+ }
142
+ finally {
143
+ await app.close();
144
+ }
145
+ });
146
+ it('⑤ 框架不管资源生命周期:资源上的 dispose 不被调用(释放要放 services)', async () => {
147
+ const calls = [];
148
+ const server = defineServer({
149
+ app: { service: 'resources-lifecycle', title: 'Resources Lifecycle', version: '1.0.0' },
150
+ resources: () => ({
151
+ database: {
152
+ dispose: () => {
153
+ calls.push('dispose:resource');
154
+ },
155
+ },
156
+ }),
157
+ config: () => ({}),
158
+ services: () => ({
159
+ databaseClient: {
160
+ dispose: () => {
161
+ calls.push('dispose:service');
162
+ },
163
+ },
164
+ }),
165
+ middlewares: () => [],
166
+ routers: () => undefined,
167
+ });
168
+ const app = await startServer(server, { listen: false });
169
+ await app.close();
170
+ expect(calls).toEqual(['dispose:service']);
171
+ });
172
+ it('⑥ 出 spec 路径同样跑 resources(只构造,不建连)', async () => {
173
+ let calls = 0;
174
+ const server = defineServer({
175
+ app: { service: 'resources-openapi', title: 'Resources OpenApi', version: '1.0.0' },
176
+ resources: () => {
177
+ calls += 1;
178
+ return {};
179
+ },
180
+ config: () => ({}),
181
+ services: () => ({}),
182
+ middlewares: () => [],
183
+ routers: () => undefined,
184
+ });
185
+ await generateOpenApi(server);
186
+ expect(calls).toBe(1);
187
+ });
188
+ });
189
+ /** `ResourcesView` 自证:`Awaited<>` 归一(同步 / 异步两写法等价) */
190
+ const assertResourcesViewNormalizes = () => {
191
+ expectTypeOf().toEqualTypeOf();
192
+ expectTypeOf().toEqualTypeOf();
193
+ };
194
+ void assertResourcesViewNormalizes;
195
+ /** 测试用 env 自证:固定读取约定里的五项 */
196
+ const assertTestConfigEnv = () => {
197
+ expectTypeOf(createTestConfigEnv().ENV).toEqualTypeOf();
198
+ };
199
+ void assertTestConfigEnv;
@@ -1,9 +1,22 @@
1
1
  /**
2
- * 固定关闭 7 步(design §5)——**顺序写死在数组里**。
2
+ * 固定关闭步(N1 §5)——**顺序写死在数组里**。
3
3
  *
4
- * 关键修正(相对现状):Redis 晚于 queue/cache 消费者释放;afterInit(如 LDAP)资源进第 1 步。
5
- * 单步抛错记录后**继续**后续步骤,最后聚合上报(避免「config 报错导致 DB 没关」)。
4
+ * 新口径下框架不再持有 Redis / 数据库 / 外链客户端(这些都在 `resources` 里,由各仓自行构造,
5
+ * 需要释放时放 `services` 单元);配置失效通道已删除,故没有 `unsubscribeConfig` 步。
6
+ *
7
+ * 固定两步:`stopProducers`(`ctx.disposer` 注册的清理)→ `disposeServices`(services 逆序)。
8
+ *
9
+ * 每步失败记日志、不阻断后续,最后聚合上报。
6
10
  */
7
- export declare const SHUTDOWN_ORDER: readonly ["stopProducers", "unsubscribeConfig", "disposeConfig", "disposeServices", "disposeClients", "disposeRedis", "disposeDb"];
11
+ export declare const SHUTDOWN_ORDER: readonly ["stopProducers", "disposeServices"];
8
12
  export type ShutdownStepName = (typeof SHUTDOWN_ORDER)[number];
13
+ export type ShutdownHandles = Record<ShutdownStepName, () => void | Promise<void>>;
14
+ export interface ShutdownLogger {
15
+ error: (obj: unknown, msg?: string) => void;
16
+ }
17
+ /**
18
+ * 顺序执行关闭步骤;单步抛错记录后**继续**后续步骤,最后聚合上报
19
+ * (避免「一步报错导致后面资源没关」)。
20
+ */
21
+ export declare function runShutdown(handles: ShutdownHandles, log: ShutdownLogger): Promise<void>;
9
22
  //# sourceMappingURL=shutdown.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../source/app/shutdown.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,cAAc,oIAQjB,CAAA;AAEV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA"}
1
+ {"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../source/app/shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,+CAAgD,CAAA;AAE3E,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,gBAAgB,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;AAElF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5C;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAe9F"}
@@ -1,15 +1,30 @@
1
1
  /**
2
- * 固定关闭 7 步(design §5)——**顺序写死在数组里**。
2
+ * 固定关闭步(N1 §5)——**顺序写死在数组里**。
3
3
  *
4
- * 关键修正(相对现状):Redis 晚于 queue/cache 消费者释放;afterInit(如 LDAP)资源进第 1 步。
5
- * 单步抛错记录后**继续**后续步骤,最后聚合上报(避免「config 报错导致 DB 没关」)。
4
+ * 新口径下框架不再持有 Redis / 数据库 / 外链客户端(这些都在 `resources` 里,由各仓自行构造,
5
+ * 需要释放时放 `services` 单元);配置失效通道已删除,故没有 `unsubscribeConfig` 步。
6
+ *
7
+ * 固定两步:`stopProducers`(`ctx.disposer` 注册的清理)→ `disposeServices`(services 逆序)。
8
+ *
9
+ * 每步失败记日志、不阻断后续,最后聚合上报。
10
+ */
11
+ export const SHUTDOWN_ORDER = ['stopProducers', 'disposeServices'];
12
+ /**
13
+ * 顺序执行关闭步骤;单步抛错记录后**继续**后续步骤,最后聚合上报
14
+ * (避免「一步报错导致后面资源没关」)。
6
15
  */
7
- export const SHUTDOWN_ORDER = [
8
- 'stopProducers',
9
- 'unsubscribeConfig',
10
- 'disposeConfig',
11
- 'disposeServices',
12
- 'disposeClients',
13
- 'disposeRedis',
14
- 'disposeDb',
15
- ];
16
+ export async function runShutdown(handles, log) {
17
+ const errors = [];
18
+ for (const step of SHUTDOWN_ORDER) {
19
+ try {
20
+ await handles[step]();
21
+ }
22
+ catch (error) {
23
+ errors.push(error);
24
+ log.error({ error, step }, '[shutdown] step failed');
25
+ }
26
+ }
27
+ if (errors.length > 0) {
28
+ log.error({ errors, count: errors.length }, '[shutdown] completed with errors');
29
+ }
30
+ }