@taicode/common-server 4.0.0-beta.1 → 4.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/output/app/boot.d.ts +9 -13
- package/output/app/boot.d.ts.map +1 -1
- package/output/app/boot.js +62 -27
- package/output/app/context.d.ts +7 -1
- package/output/app/context.d.ts.map +1 -1
- package/output/app/context.js +9 -3
- package/output/app/core/index.d.ts +3 -3
- package/output/app/core/index.d.ts.map +1 -1
- package/output/app/core/index.js +1 -1
- package/output/app/core/response.d.ts +8 -8
- package/output/app/core/response.d.ts.map +1 -1
- package/output/app/core/response.js +6 -6
- package/output/app/define-server.negative-cases.d.ts +8 -0
- package/output/app/define-server.negative-cases.d.ts.map +1 -1
- package/output/app/define-server.negative-cases.js +60 -1
- package/output/app/entries.d.ts +2 -0
- package/output/app/entries.d.ts.map +1 -1
- package/output/app/entries.js +2 -0
- package/output/app/error-contract.test.js +110 -8
- package/output/app/index.d.ts +1 -1
- package/output/app/index.d.ts.map +1 -1
- package/output/app/lifecycle-logging.test.js +23 -3
- package/output/app/lifecycle.d.ts +32 -6
- package/output/app/lifecycle.d.ts.map +1 -1
- package/output/app/lifecycle.js +34 -6
- package/output/app/lifecycle.test.js +4 -3
- package/output/app/middleware-order.test.js +4 -3
- package/output/app/redis.d.ts +5 -1
- package/output/app/redis.d.ts.map +1 -1
- package/output/app/redis.js +5 -1
- package/output/app/resources.test.js +45 -20
- package/output/app/shutdown.d.ts +9 -4
- package/output/app/shutdown.d.ts.map +1 -1
- package/output/app/shutdown.js +16 -4
- package/output/app/shutdown.test.js +125 -18
- package/output/app/types.d.ts +45 -5
- package/output/app/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -97,16 +97,15 @@ describe('错误契约', () => {
|
|
|
97
97
|
expect(body.message).toContain('GET /missing');
|
|
98
98
|
await app.close();
|
|
99
99
|
});
|
|
100
|
-
it('
|
|
100
|
+
it('资源注册的 `ctx.registerClassifyError` 分类器生效(→500,错误码 = 分类器返回的 SystemError.type)', async () => {
|
|
101
101
|
const server = defineServer({
|
|
102
102
|
app: { service: 'error-contract-database', title: 'Error Contract Database', version: '1.0.0' },
|
|
103
|
-
resources: () =>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}),
|
|
103
|
+
resources: (ctx) => {
|
|
104
|
+
ctx.registerClassifyError((error) => error instanceof Error && error.message === 'pg down'
|
|
105
|
+
? new SystemError('DATABASE_ERROR', { operation: 'query' }, 'pg down')
|
|
106
|
+
: null);
|
|
107
|
+
return {};
|
|
108
|
+
},
|
|
110
109
|
config: () => ({}),
|
|
111
110
|
services: () => ({}),
|
|
112
111
|
middlewares: () => [],
|
|
@@ -122,4 +121,107 @@ describe('错误契约', () => {
|
|
|
122
121
|
expect(response.json()).toEqual({ status: 'DATABASE_ERROR', message: 'pg down', data: null });
|
|
123
122
|
await app.close();
|
|
124
123
|
});
|
|
124
|
+
it('跨资源注册:每个资源各注册自己的分类器,各自认领域内错误(顺序短路),都不认领则兜底', async () => {
|
|
125
|
+
const calls = [];
|
|
126
|
+
const server = defineServer({
|
|
127
|
+
app: { service: 'error-contract-cross-resource', title: 'Error Contract Cross Resource', version: '1.0.0' },
|
|
128
|
+
resources: (ctx) => {
|
|
129
|
+
// prisma 资源:只认领自己域内的 Prisma 错误(认领不了返回 null)
|
|
130
|
+
ctx.registerClassifyError((error) => {
|
|
131
|
+
calls.push('prisma');
|
|
132
|
+
return error instanceof Error && error.name === 'PrismaError'
|
|
133
|
+
? new SystemError('DATABASE_ERROR', { operation: 'query' }, '数据库错误')
|
|
134
|
+
: null;
|
|
135
|
+
});
|
|
136
|
+
// cache 资源:只认领自己域内的缓存错误
|
|
137
|
+
ctx.registerClassifyError((error) => {
|
|
138
|
+
calls.push('cache');
|
|
139
|
+
return error instanceof Error && error.name === 'CacheError'
|
|
140
|
+
? new SystemError('CACHE_ERROR', undefined, '缓存错误')
|
|
141
|
+
: null;
|
|
142
|
+
});
|
|
143
|
+
return { prisma: {}, cache: {} };
|
|
144
|
+
},
|
|
145
|
+
config: () => ({}),
|
|
146
|
+
services: () => ({}),
|
|
147
|
+
middlewares: () => [],
|
|
148
|
+
routers: (ctx) => {
|
|
149
|
+
ctx.app.get('/prisma-error', () => {
|
|
150
|
+
throw Object.assign(new Error('db down'), { name: 'PrismaError' });
|
|
151
|
+
});
|
|
152
|
+
ctx.app.get('/cache-error', () => {
|
|
153
|
+
throw Object.assign(new Error('cache down'), { name: 'CacheError' });
|
|
154
|
+
});
|
|
155
|
+
ctx.app.get('/unclaimed-error', () => {
|
|
156
|
+
throw new Error('someone else error');
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const app = await startServer(server, { listen: false });
|
|
161
|
+
// ① prisma 域:prisma 分类器认领,错误码正确;首个命中即短路(不再问 cache)
|
|
162
|
+
calls.length = 0;
|
|
163
|
+
const prisma = await app.inject({ method: 'GET', url: '/prisma-error' });
|
|
164
|
+
expect(prisma.statusCode).toBe(500);
|
|
165
|
+
expect(prisma.json()).toEqual({ status: 'DATABASE_ERROR', message: '数据库错误', data: null });
|
|
166
|
+
expect(calls).toEqual(['prisma']);
|
|
167
|
+
// ② cache 域:prisma 返回 null → 交给 cache 认领,错误码正确
|
|
168
|
+
calls.length = 0;
|
|
169
|
+
const cache = await app.inject({ method: 'GET', url: '/cache-error' });
|
|
170
|
+
expect(cache.statusCode).toBe(500);
|
|
171
|
+
expect(cache.json()).toEqual({ status: 'CACHE_ERROR', message: '缓存错误', data: null });
|
|
172
|
+
expect(calls).toEqual(['prisma', 'cache']);
|
|
173
|
+
// ③ 都不认领 → 落 `UNKNOWN_ERROR` 兜底
|
|
174
|
+
calls.length = 0;
|
|
175
|
+
const unclaimed = await app.inject({ method: 'GET', url: '/unclaimed-error' });
|
|
176
|
+
expect(unclaimed.statusCode).toBe(500);
|
|
177
|
+
expect(unclaimed.json()).toEqual({ status: 'UNKNOWN_ERROR', message: 'someone else error', data: null });
|
|
178
|
+
expect(calls).toEqual(['prisma', 'cache']);
|
|
179
|
+
await app.close();
|
|
180
|
+
});
|
|
181
|
+
it('一个资源都不注册分类器 → 资源错误分支不生效,落 `UNKNOWN_ERROR` 兜底', async () => {
|
|
182
|
+
const server = defineServer({
|
|
183
|
+
app: { service: 'error-contract-none', title: 'Error Contract None', version: '1.0.0' },
|
|
184
|
+
resources: () => ({}),
|
|
185
|
+
config: () => ({}),
|
|
186
|
+
services: () => ({}),
|
|
187
|
+
middlewares: () => [],
|
|
188
|
+
routers: (ctx) => {
|
|
189
|
+
ctx.app.get('/boom', () => {
|
|
190
|
+
throw new Error('boom');
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const app = await startServer(server, { listen: false });
|
|
195
|
+
const response = await app.inject({ method: 'GET', url: '/boom' });
|
|
196
|
+
expect(response.statusCode).toBe(500);
|
|
197
|
+
expect(response.json()).toEqual({ status: 'UNKNOWN_ERROR', message: 'boom', data: null });
|
|
198
|
+
await app.close();
|
|
199
|
+
});
|
|
200
|
+
it('注册的分类器不影响既有分支:`UserError` 仍走 400(分类器只在资源错误分支求值)', async () => {
|
|
201
|
+
let classified = 0;
|
|
202
|
+
const server = defineServer({
|
|
203
|
+
app: { service: 'error-contract-user', title: 'Error Contract User', version: '1.0.0' },
|
|
204
|
+
resources: (ctx) => {
|
|
205
|
+
ctx.registerClassifyError((error) => {
|
|
206
|
+
classified += 1;
|
|
207
|
+
return error instanceof Error ? new SystemError('SHOULD_NOT_HAPPEN', undefined, 'x') : null;
|
|
208
|
+
});
|
|
209
|
+
return {};
|
|
210
|
+
},
|
|
211
|
+
config: () => ({}),
|
|
212
|
+
services: () => ({}),
|
|
213
|
+
middlewares: () => [],
|
|
214
|
+
routers: (ctx) => {
|
|
215
|
+
ctx.app.get('/user-error-2', () => {
|
|
216
|
+
throw new UserError('INVALID_TOKEN', undefined, 'token 无效');
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
const app = await startServer(server, { listen: false });
|
|
221
|
+
const response = await app.inject({ method: 'GET', url: '/user-error-2' });
|
|
222
|
+
expect(response.statusCode).toBe(400);
|
|
223
|
+
expect(response.json()).toEqual({ status: 'INVALID_TOKEN', message: 'token 无效', data: null });
|
|
224
|
+
expect(classified).toBe(0); // `UserError` 在分支①命中,分类器根本没被求值
|
|
225
|
+
await app.close();
|
|
226
|
+
});
|
|
125
227
|
});
|
package/output/app/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { createCorsMiddleware, createContextMiddleware, createAuthMiddleware, cr
|
|
|
8
8
|
export { REQUEST_ID_HEADER } from './core/request-id.js';
|
|
9
9
|
export { DOC_ROUTE_PREFIX, DOC_SPEC_ROUTE } from './core/openapi.js';
|
|
10
10
|
export { extractPermissionOperations } from './permission-operations.js';
|
|
11
|
-
export type { AppIdentity, AppInstance, LoggerLike, MaybePromise, RedisHandle, Live, ResourcesView, AssertNoDependencies, ServicesShape, ResourcesContext, ResourcesFactory, ConfigContext, ConfigFactory, ServiceUnit, ServiceContext, ServicesFactory, MiddlewareUnit, MiddlewareContext, MiddlewaresFactory, RouterContext, RoutersFactory, ServerConfig, Server, StartServerOptions, ShutdownContext, OpenApiDocument, } from './types.js';
|
|
11
|
+
export type { AppIdentity, AppInstance, LoggerLike, MaybePromise, RedisHandle, Live, ResourcesView, AssertNoDependencies, ServicesShape, ErrorClassifier, ResourcesContext, ResourcesFactory, ConfigContext, ConfigFactory, ServiceUnit, ServiceContext, ServicesFactory, MiddlewareUnit, MiddlewareContext, MiddlewaresFactory, RouterContext, RoutersFactory, ServerConfig, Server, StartServerOptions, ShutdownContext, OpenApiDocument, } from './types.js';
|
|
12
12
|
export type { CacheSetupParams } from './redis.js';
|
|
13
13
|
export type { PermissionOperation } from './permission-operations.js';
|
|
14
14
|
export type { CorsMiddlewareOptions, ContextMiddlewareOptions, AuthMiddlewareOptions, AuthSkip, AuthVerifyInput, AuthVerifyMetadata, AuthVerifyResult, AccountIntegrationConfig, RateLimitMiddlewareOptions } from './middleware/index.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/app/index.ts"],"names":[],"mappings":"AAEA,OAAO,mBAAmB,CAAA;AAG1B,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAGjG,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AACvI,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGpD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,+BAA+B,EAAE,MAAM,YAAY,CAAA;AAGtF,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAG3K,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAGxE,YAAY,EAEV,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,WAAW,EAEX,IAAI,EACJ,aAAa,EACb,oBAAoB,EACpB,aAAa,EAEb,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,WAAW,EACX,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,cAAc,EAEd,YAAY,EACZ,MAAM,EACN,kBAAkB,EAClB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AAC1O,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/app/index.ts"],"names":[],"mappings":"AAEA,OAAO,mBAAmB,CAAA;AAG1B,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAGjG,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AACvI,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGpD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,+BAA+B,EAAE,MAAM,YAAY,CAAA;AAGtF,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAG3K,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAGxE,YAAY,EAEV,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,WAAW,EAEX,IAAI,EACJ,aAAa,EACb,oBAAoB,EACpB,aAAa,EAEb,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,WAAW,EACX,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,cAAc,EAEd,YAAY,EACZ,MAAM,EACN,kBAAkB,EAClB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AAC1O,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Disposer } from '@taicode/common-base';
|
|
2
3
|
import { defineServer, startServer } from './entries.js';
|
|
3
|
-
import { buildDisposeOrder, initServiceUnits, LIFECYCLE_PHASE_DISPOSE, LIFECYCLE_PHASE_INIT, MIDDLEWARES_LOG_STAGE, middlewareUnitName, SERVICES_STAGE, serviceUnitName, } from './lifecycle.js';
|
|
4
|
+
import { buildDisposeOrder, disposeStage, initServiceUnits, LIFECYCLE_PHASE_DISPOSE, LIFECYCLE_PHASE_INIT, MIDDLEWARES_LOG_STAGE, middlewareUnitName, RESOURCES_STAGE, SERVICES_STAGE, serviceUnitName, } from './lifecycle.js';
|
|
4
5
|
import { createRecordingLogger } from './test-support.js';
|
|
5
6
|
/**
|
|
6
7
|
* 逐单元日志。
|
|
7
8
|
*
|
|
8
9
|
* - **注册期**:每个中间件注册后打一条 `debug`,带 `{ service, unit, stage: 'middlewares' }`;
|
|
9
10
|
* - **init / dispose**:逐单元 `debug`(开始 / 完成),失败 `error`;带 `{ service, unit, stage, phase }`;
|
|
11
|
+
* - **阶段 flush**:`disposeStage` 每阶段一条 `debug`(`unit` = 阶段名);
|
|
10
12
|
* - 中间件名字:`unit.name ?? '<service>:middleware[<index>]'`;services 用对象键。
|
|
11
13
|
*
|
|
12
14
|
* 分两层验证:
|
|
@@ -91,6 +93,19 @@ describe('逐单元日志:init / dispose', () => {
|
|
|
91
93
|
expect(middlewareUnitName('svc', {}, 3)).toBe('svc:middleware[3]');
|
|
92
94
|
expect(middlewareUnitName('svc', { name: 'named' }, 1)).toBe('named');
|
|
93
95
|
});
|
|
96
|
+
it('阶段 flush:`unit` / `stage` = 阶段名,`phase` = dispose', async () => {
|
|
97
|
+
const log = createRecordingLogger();
|
|
98
|
+
const disposer = new Disposer();
|
|
99
|
+
await disposeStage({ service: 'svc', log, stage: RESOURCES_STAGE, disposer });
|
|
100
|
+
const fields = log.entries.map((entry) => entry.obj);
|
|
101
|
+
expect(fields.length).toBeGreaterThan(0);
|
|
102
|
+
for (const field of fields) {
|
|
103
|
+
expect(field.service).toBe('svc');
|
|
104
|
+
expect(field.unit).toBe(RESOURCES_STAGE);
|
|
105
|
+
expect(field.stage).toBe(RESOURCES_STAGE);
|
|
106
|
+
expect(field.phase).toBe(LIFECYCLE_PHASE_DISPOSE);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
94
109
|
});
|
|
95
110
|
// ── 真路径:`startServer` 走完整装配,捕获框架 logger 写入 ────────
|
|
96
111
|
const captured = vi.hoisted(() => ({ lines: [] }));
|
|
@@ -148,8 +163,13 @@ describe('startServer 真路径:注册 / init / dispose 逐单元日志', () =
|
|
|
148
163
|
// init / dispose:services 逐单元 debug(带 `phase`)
|
|
149
164
|
expect(matchRecords(records, { unit: 'alpha', stage: SERVICES_STAGE, phase: LIFECYCLE_PHASE_INIT }).length).toBeGreaterThan(0);
|
|
150
165
|
expect(matchRecords(records, { unit: 'alpha', stage: SERVICES_STAGE, phase: LIFECYCLE_PHASE_DISPOSE }).length).toBeGreaterThan(0);
|
|
151
|
-
//
|
|
166
|
+
// 中间件**没有逐单元生命周期**:没有 middlewares 阶段的 init 日志;dispose 只有阶段 flush
|
|
152
167
|
expect(matchRecords(records, { stage: MIDDLEWARES_LOG_STAGE, phase: LIFECYCLE_PHASE_INIT })).toHaveLength(0);
|
|
153
|
-
|
|
168
|
+
const middlewareDispose = matchRecords(records, {
|
|
169
|
+
stage: MIDDLEWARES_LOG_STAGE,
|
|
170
|
+
phase: LIFECYCLE_PHASE_DISPOSE,
|
|
171
|
+
});
|
|
172
|
+
expect(middlewareDispose.length).toBeGreaterThan(0);
|
|
173
|
+
expect(middlewareDispose.every((record) => record.unit === MIDDLEWARES_LOG_STAGE)).toBe(true);
|
|
154
174
|
});
|
|
155
175
|
});
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
+
import type { Disposer } from '@taicode/common-base';
|
|
1
2
|
import type { LoggerLike } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* 生命周期序(N1 §4 / N2 §2.5)。
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
+
* **每个阶段一个 `Disposer`**(`ctx.disposer`,所有槽位都有):在创建某个东西的地方当场
|
|
7
|
+
* `ctx.disposer.add(() => 释放它)` —— 创建与释放同处一地。
|
|
8
|
+
*
|
|
9
|
+
* 关闭时按阶段**逆序 flush**:`routers → middlewares → services → config → resources`
|
|
10
|
+
* (后创建的先销毁;Prisma / Redis 这类资源最后才断)。
|
|
11
|
+
*
|
|
12
|
+
* **`services` 额外有逐单元生命周期**:
|
|
6
13
|
* - 单元之间**不得相互依赖**(`services` 的 ctx 没有 `services` 字段,编译期禁止);
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
14
|
+
* - `init` 按**对象声明序**,`dispose` **逆序**;
|
|
15
|
+
* - services 阶段内部**先 flush `ctx.disposer`**(构造期注册的清理),再逐单元 `dispose`。
|
|
16
|
+
*
|
|
17
|
+
* `middlewares` 按数组序注册;它只有 `disposer`,没有逐单元生命周期。
|
|
10
18
|
*/
|
|
11
19
|
export declare const SERVICES_STAGE = "services";
|
|
12
|
-
|
|
20
|
+
export declare const CONFIG_STAGE = "config";
|
|
21
|
+
export declare const MIDDLEWARES_STAGE = "middlewares";
|
|
22
|
+
export declare const ROUTERS_STAGE = "routers";
|
|
23
|
+
export declare const RESOURCES_STAGE = "resources";
|
|
24
|
+
/** `middlewares` 的**注册期日志**阶段标签(与生命周期阶段同名) */
|
|
13
25
|
export declare const MIDDLEWARES_LOG_STAGE = "middlewares";
|
|
14
26
|
/** 生命周期相位(逐单元日志字段 `phase`) */
|
|
15
27
|
export declare const LIFECYCLE_PHASE_INIT = "init";
|
|
@@ -27,7 +39,7 @@ export interface LifecycleLogContext {
|
|
|
27
39
|
/** 记录型 logger(`ctx.log`);缺省 = 不打日志 */
|
|
28
40
|
log?: LoggerLike;
|
|
29
41
|
}
|
|
30
|
-
/** 逐单元日志字段(全拼):`unit` =
|
|
42
|
+
/** 逐单元日志字段(全拼):`unit` = 有效名字(阶段 flush 时为阶段名),`stage` = 阶段名 */
|
|
31
43
|
export interface LifecycleUnitLogFields {
|
|
32
44
|
service: string;
|
|
33
45
|
unit: string;
|
|
@@ -67,4 +79,18 @@ export declare function buildDisposeOrder(params: DisposeServiceUnitsParams): Ar
|
|
|
67
79
|
* 此处仅保证单步不阻断后续。
|
|
68
80
|
*/
|
|
69
81
|
export declare function disposeServices(params: DisposeServiceUnitsParams): Promise<void>;
|
|
82
|
+
/** `disposeStage` 入参(参数签名内联对象类型已按 lint 规则外提) */
|
|
83
|
+
export interface DisposeStageParams extends LifecycleLogContext {
|
|
84
|
+
/** 阶段名(`routers` / `middlewares` / `services` / `config` / `resources`) */
|
|
85
|
+
stage: string;
|
|
86
|
+
/** 该阶段的 `Disposer`(构造期注册的清理函数,**逆注册序**执行) */
|
|
87
|
+
disposer: Disposer;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 关闭步:flush 某个阶段的 `disposer`。
|
|
91
|
+
*
|
|
92
|
+
* 单元名 = 阶段名(一个阶段一个 `Disposer`,不逐注册项打日志);`phase` 固定 `dispose`。
|
|
93
|
+
* 单个清理函数失败由 `Disposer` 自己捕获记录,不影响同阶段其它函数。
|
|
94
|
+
*/
|
|
95
|
+
export declare function disposeStage(params: DisposeStageParams): Promise<void>;
|
|
70
96
|
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../source/app/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../source/app/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,cAAc,aAAa,CAAA;AACxC,eAAO,MAAM,YAAY,WAAW,CAAA;AACpC,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAC9C,eAAO,MAAM,aAAa,YAAY,CAAA;AACtC,eAAO,MAAM,eAAe,cAAc,CAAA;AAC1C,8CAA8C;AAC9C,eAAO,MAAM,qBAAqB,gBAAoB,CAAA;AAEtD,8BAA8B;AAC9B,eAAO,MAAM,oBAAoB,SAAS,CAAA;AAC1C,eAAO,MAAM,uBAAuB,YAAY,CAAA;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,oBAAoB,GAAG,OAAO,uBAAuB,CAAA;AAEzF;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,GAAG,CAAC,EAAE,UAAU,CAAA;CACjB;AAED,8DAA8D;AAC9D,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,cAAc,CAAA;CACtB;AAMD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKxF;AAED,oDAAoD;AACpD,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAmCD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpF;AAED,6EAA6E;AAC7E,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACpE,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,yBAAyB,GAAG,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAMtG;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF;AAED,gDAAgD;AAChD,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAA;IACb,6CAA6C;IAC7C,QAAQ,EAAE,QAAQ,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5E"}
|
package/output/app/lifecycle.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 生命周期序(N1 §4 / N2 §2.5)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* **每个阶段一个 `Disposer`**(`ctx.disposer`,所有槽位都有):在创建某个东西的地方当场
|
|
5
|
+
* `ctx.disposer.add(() => 释放它)` —— 创建与释放同处一地。
|
|
6
|
+
*
|
|
7
|
+
* 关闭时按阶段**逆序 flush**:`routers → middlewares → services → config → resources`
|
|
8
|
+
* (后创建的先销毁;Prisma / Redis 这类资源最后才断)。
|
|
9
|
+
*
|
|
10
|
+
* **`services` 额外有逐单元生命周期**:
|
|
5
11
|
* - 单元之间**不得相互依赖**(`services` 的 ctx 没有 `services` 字段,编译期禁止);
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
12
|
+
* - `init` 按**对象声明序**,`dispose` **逆序**;
|
|
13
|
+
* - services 阶段内部**先 flush `ctx.disposer`**(构造期注册的清理),再逐单元 `dispose`。
|
|
14
|
+
*
|
|
15
|
+
* `middlewares` 按数组序注册;它只有 `disposer`,没有逐单元生命周期。
|
|
9
16
|
*/
|
|
10
17
|
export const SERVICES_STAGE = 'services';
|
|
11
|
-
|
|
12
|
-
export const
|
|
18
|
+
export const CONFIG_STAGE = 'config';
|
|
19
|
+
export const MIDDLEWARES_STAGE = 'middlewares';
|
|
20
|
+
export const ROUTERS_STAGE = 'routers';
|
|
21
|
+
export const RESOURCES_STAGE = 'resources';
|
|
22
|
+
/** `middlewares` 的**注册期日志**阶段标签(与生命周期阶段同名) */
|
|
23
|
+
export const MIDDLEWARES_LOG_STAGE = MIDDLEWARES_STAGE;
|
|
13
24
|
/** 生命周期相位(逐单元日志字段 `phase`) */
|
|
14
25
|
export const LIFECYCLE_PHASE_INIT = 'init';
|
|
15
26
|
export const LIFECYCLE_PHASE_DISPOSE = 'dispose';
|
|
@@ -97,3 +108,20 @@ export async function disposeServices(params) {
|
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* 关闭步:flush 某个阶段的 `disposer`。
|
|
113
|
+
*
|
|
114
|
+
* 单元名 = 阶段名(一个阶段一个 `Disposer`,不逐注册项打日志);`phase` 固定 `dispose`。
|
|
115
|
+
* 单个清理函数失败由 `Disposer` 自己捕获记录,不影响同阶段其它函数。
|
|
116
|
+
*/
|
|
117
|
+
export async function disposeStage(params) {
|
|
118
|
+
const fields = {
|
|
119
|
+
service: params.service ?? '',
|
|
120
|
+
unit: params.stage,
|
|
121
|
+
stage: params.stage,
|
|
122
|
+
phase: LIFECYCLE_PHASE_DISPOSE,
|
|
123
|
+
};
|
|
124
|
+
params.log?.debug(fields, `[lifecycle] ${LIFECYCLE_PHASE_DISPOSE} "${params.stage}" start`);
|
|
125
|
+
await params.disposer.dispose();
|
|
126
|
+
params.log?.debug(fields, `[lifecycle] ${LIFECYCLE_PHASE_DISPOSE} "${params.stage}" done`);
|
|
127
|
+
}
|
|
@@ -4,10 +4,11 @@ import { buildDisposeOrder, initServiceUnits, serviceUnitName } from './lifecycl
|
|
|
4
4
|
/**
|
|
5
5
|
* 生命周期(N1 §4)。
|
|
6
6
|
*
|
|
7
|
-
* -
|
|
7
|
+
* - **每个阶段一个 `Disposer`**(`ctx.disposer`):创建处注册释放,关闭时按阶段逆序 flush;
|
|
8
|
+
* - **`services` 额外有逐单元生命周期**:按**对象声明序** `init` / **逆序** `dispose`;
|
|
9
|
+
* - services 阶段内部:先 flush 自己的 `disposer`,再逐单元 `dispose`;
|
|
8
10
|
* - **没有 `dependsOn`**(`services` 的 ctx 没有 `services` 字段,编译期禁止互依);
|
|
9
|
-
* - 单元名 =
|
|
10
|
-
* - `middlewares` 按数组序注册,**不参与生命周期**。
|
|
11
|
+
* - 单元名 = **对象键**。
|
|
11
12
|
*/
|
|
12
13
|
describe('init / dispose 顺序:声明序 / 逆序(N1 §4 / §5)', () => {
|
|
13
14
|
it('init:services 按对象声明序', async () => {
|
|
@@ -10,20 +10,21 @@ import { defaultMiddlewares } from './middleware/index.js';
|
|
|
10
10
|
const fakeMiddlewareContext = {
|
|
11
11
|
app: {},
|
|
12
12
|
log: { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } },
|
|
13
|
+
disposer: { add: () => { }, dispose: async () => { } },
|
|
13
14
|
services: {},
|
|
14
15
|
config: {},
|
|
15
16
|
};
|
|
16
17
|
describe('ctx 可见性(N1 §2)', () => {
|
|
17
|
-
it('MiddlewareContext 有 app / log / services / config;没有 resources / env
|
|
18
|
+
it('MiddlewareContext 有 app / log / disposer / services / config;没有 resources / env', () => {
|
|
18
19
|
expectTypeOf().toHaveProperty('app');
|
|
19
20
|
expectTypeOf().toHaveProperty('services');
|
|
20
21
|
expectTypeOf().toHaveProperty('config');
|
|
22
|
+
// `disposer` 是所有槽位的标配(不是 services 专属)
|
|
23
|
+
expectTypeOf().toHaveProperty('disposer');
|
|
21
24
|
// @ts-expect-error 中间件看不到 resources(禁直连数据层)
|
|
22
25
|
void 0..resources;
|
|
23
26
|
// @ts-expect-error 中间件看不到 env
|
|
24
27
|
void 0..env;
|
|
25
|
-
// @ts-expect-error 中间件看不到 disposer(disposer 只属 services)
|
|
26
|
-
void 0..disposer;
|
|
27
28
|
});
|
|
28
29
|
});
|
|
29
30
|
describe('默认预设', () => {
|
package/output/app/redis.d.ts
CHANGED
|
@@ -4,17 +4,21 @@ import type { LoggerLike, RedisHandle } from './types.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* 引导期 Redis / cache helper(N2 §2.13)。
|
|
6
6
|
*
|
|
7
|
-
* 在 `resources(ctx)` 槽位里按需调用(框架自己**不**创建 Redis
|
|
7
|
+
* 在 `resources(ctx)` 槽位里按需调用(框架自己**不**创建 Redis);释放**在创建处**登记:
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
10
|
* resources: (ctx) => {
|
|
11
11
|
* const redis = createRedis(ctx.env, ctx.log) // 无 REDIS_URL → undefined
|
|
12
|
+
* if (redis) {
|
|
13
|
+
* ctx.disposer.add(() => redis.dispose()) // 关闭时 resources 阶段最后 flush
|
|
14
|
+
* }
|
|
12
15
|
* return { redis, cache: createCache({ env: ctx.env, prefix: 'account', redis }) }
|
|
13
16
|
* }
|
|
14
17
|
* ```
|
|
15
18
|
*
|
|
16
19
|
* `REDIS_URL` 由 helper 直接读 `env`(固定约定),**不**取自 `config`;`redis` / `cache` 一律是
|
|
17
20
|
* **惰性包装**:构造时不建连、不 ping,首次真正使用才建连 —— 「出 spec 不连库/Redis」结构性成立。
|
|
21
|
+
* 释放永远走调用方在 `ctx.disposer` 里的登记,框架不替资源持有句柄。
|
|
18
22
|
*/
|
|
19
23
|
/** 一级/二级缓存默认 TTL(秒),与 `RedisCache` 默认一致 */
|
|
20
24
|
export declare const DEFAULT_REDIS_CACHE_TTL_SECONDS = 300;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../source/app/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEzD
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../source/app/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEzD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,2CAA2C;AAC3C,eAAO,MAAM,+BAA+B,MAAM,CAAA;AAElD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CAmBrF;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"}
|
package/output/app/redis.js
CHANGED
|
@@ -2,17 +2,21 @@ import { createRedisClient, RedisCache } from '../redis-cache/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* 引导期 Redis / cache helper(N2 §2.13)。
|
|
4
4
|
*
|
|
5
|
-
* 在 `resources(ctx)` 槽位里按需调用(框架自己**不**创建 Redis
|
|
5
|
+
* 在 `resources(ctx)` 槽位里按需调用(框架自己**不**创建 Redis);释放**在创建处**登记:
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
8
|
* resources: (ctx) => {
|
|
9
9
|
* const redis = createRedis(ctx.env, ctx.log) // 无 REDIS_URL → undefined
|
|
10
|
+
* if (redis) {
|
|
11
|
+
* ctx.disposer.add(() => redis.dispose()) // 关闭时 resources 阶段最后 flush
|
|
12
|
+
* }
|
|
10
13
|
* return { redis, cache: createCache({ env: ctx.env, prefix: 'account', redis }) }
|
|
11
14
|
* }
|
|
12
15
|
* ```
|
|
13
16
|
*
|
|
14
17
|
* `REDIS_URL` 由 helper 直接读 `env`(固定约定),**不**取自 `config`;`redis` / `cache` 一律是
|
|
15
18
|
* **惰性包装**:构造时不建连、不 ping,首次真正使用才建连 —— 「出 spec 不连库/Redis」结构性成立。
|
|
19
|
+
* 释放永远走调用方在 `ctx.disposer` 里的登记,框架不替资源持有句柄。
|
|
16
20
|
*/
|
|
17
21
|
/** 一级/二级缓存默认 TTL(秒),与 `RedisCache` 默认一致 */
|
|
18
22
|
export const DEFAULT_REDIS_CACHE_TTL_SECONDS = 300;
|
|
@@ -16,7 +16,7 @@ const assertLiveKeysRemovesFalse = () => {
|
|
|
16
16
|
};
|
|
17
17
|
void assertLiveKeysRemovesFalse;
|
|
18
18
|
describe('resources 槽位(N1 §2 / N2 §2.3)', () => {
|
|
19
|
-
it('①
|
|
19
|
+
it('① 入参为 env + log + disposer + registerClassifyError,且资源嵌套在 ctx.resources 下(config / services 都能用)', async () => {
|
|
20
20
|
let captured;
|
|
21
21
|
const observed = {};
|
|
22
22
|
const server = defineServer({
|
|
@@ -43,9 +43,13 @@ describe('resources 槽位(N1 §2 / N2 §2.3)', () => {
|
|
|
43
43
|
});
|
|
44
44
|
const app = await startServer(server, { listen: false });
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
// 槽位 ctx:env + log + disposer(+ 专属的 registerClassifyError)
|
|
47
|
+
expect(Object.keys(captured ?? {}).sort()).toEqual(['disposer', 'env', 'log', 'registerClassifyError']);
|
|
47
48
|
expect(captured?.env.ENV).toBeTypeOf('string');
|
|
48
49
|
expect(typeof captured?.log.info).toBe('function');
|
|
50
|
+
expect(typeof captured?.disposer.add).toBe('function');
|
|
51
|
+
// resources 槽位专属能力:每个资源注册自己的错误分类器(函数)
|
|
52
|
+
expect(typeof captured?.registerClassifyError).toBe('function');
|
|
49
53
|
expect(observed.inConfig).toBe('rows:select 1');
|
|
50
54
|
expect(observed.inServices).toBe('domain:cached');
|
|
51
55
|
const response = await app.inject({ method: 'GET', url: '/resource' });
|
|
@@ -141,38 +145,59 @@ describe('resources 槽位(N1 §2 / N2 §2.3)', () => {
|
|
|
141
145
|
await app.close();
|
|
142
146
|
}
|
|
143
147
|
});
|
|
144
|
-
it('⑤
|
|
148
|
+
it('⑤ resources 的 disposer 在 services 阶段**之后**执行;阶段内按逆注册序;资源对象自带的 dispose 不被调用', async () => {
|
|
145
149
|
const calls = [];
|
|
146
150
|
const server = defineServer({
|
|
147
151
|
app: { service: 'resources-lifecycle', title: 'Resources Lifecycle', version: '1.0.0' },
|
|
148
|
-
resources: () =>
|
|
149
|
-
|
|
152
|
+
resources: (ctx) => {
|
|
153
|
+
// 资源对象自带的 `dispose` 不参与生命周期:释放只认 `ctx.disposer.add(...)`
|
|
154
|
+
const database = {
|
|
150
155
|
dispose: () => {
|
|
151
|
-
calls.push('dispose:resource');
|
|
156
|
+
calls.push('dispose:resource-object');
|
|
152
157
|
},
|
|
153
|
-
}
|
|
154
|
-
|
|
158
|
+
};
|
|
159
|
+
ctx.disposer.add(() => {
|
|
160
|
+
calls.push('dispose:resource-a');
|
|
161
|
+
});
|
|
162
|
+
ctx.disposer.add(() => {
|
|
163
|
+
calls.push('dispose:resource-b');
|
|
164
|
+
});
|
|
165
|
+
return { database };
|
|
166
|
+
},
|
|
155
167
|
config: () => ({}),
|
|
156
|
-
services: () =>
|
|
157
|
-
|
|
158
|
-
dispose:
|
|
159
|
-
|
|
168
|
+
services: (ctx) => {
|
|
169
|
+
ctx.disposer.add(() => {
|
|
170
|
+
calls.push('dispose:services-disposer');
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
databaseClient: {
|
|
174
|
+
dispose: () => {
|
|
175
|
+
calls.push('dispose:service-unit');
|
|
176
|
+
},
|
|
160
177
|
},
|
|
161
|
-
}
|
|
162
|
-
}
|
|
178
|
+
};
|
|
179
|
+
},
|
|
163
180
|
middlewares: () => [],
|
|
164
181
|
routers: () => undefined,
|
|
165
182
|
});
|
|
166
183
|
const app = await startServer(server, { listen: false });
|
|
167
184
|
await app.close();
|
|
168
|
-
expect(calls).toEqual([
|
|
185
|
+
expect(calls).toEqual([
|
|
186
|
+
'dispose:services-disposer',
|
|
187
|
+
'dispose:service-unit',
|
|
188
|
+
'dispose:resource-b',
|
|
189
|
+
'dispose:resource-a',
|
|
190
|
+
]);
|
|
169
191
|
});
|
|
170
|
-
it('⑥ 出 spec 路径同样跑 resources
|
|
171
|
-
|
|
192
|
+
it('⑥ 出 spec 路径同样跑 resources(只构造,不建连),但不 flush 任何 disposer', async () => {
|
|
193
|
+
const calls = [];
|
|
172
194
|
const server = defineServer({
|
|
173
195
|
app: { service: 'resources-openapi', title: 'Resources OpenApi', version: '1.0.0' },
|
|
174
|
-
resources: () => {
|
|
175
|
-
calls
|
|
196
|
+
resources: (ctx) => {
|
|
197
|
+
calls.push('resources');
|
|
198
|
+
ctx.disposer.add(() => {
|
|
199
|
+
calls.push('dispose:resources');
|
|
200
|
+
});
|
|
176
201
|
return {};
|
|
177
202
|
},
|
|
178
203
|
config: () => ({}),
|
|
@@ -181,7 +206,7 @@ describe('resources 槽位(N1 §2 / N2 §2.3)', () => {
|
|
|
181
206
|
routers: () => undefined,
|
|
182
207
|
});
|
|
183
208
|
await generateOpenApi(server);
|
|
184
|
-
expect(calls).
|
|
209
|
+
expect(calls).toEqual(['resources']);
|
|
185
210
|
});
|
|
186
211
|
});
|
|
187
212
|
/** `ResourcesView` 自证:`Awaited<>` 归一(同步 / 异步两写法等价) */
|
package/output/app/shutdown.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 固定关闭步(N1 §5)——**顺序写死在数组里**。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `disposer` 是所有槽位 ctx 的标配:每个阶段一个 `Disposer`,在**创建处**注册释放。
|
|
5
|
+
* 关闭时按阶段**逆序 flush**:`routers → middlewares → services → config → resources`
|
|
6
|
+
* (后创建的先销毁;Prisma / Redis 这类资源**最后**才断)。
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* `services` 阶段内部保持既有语义:先 flush 自己的 `disposer`(原 `stopProducers`),
|
|
9
|
+
* 再 `disposeServices`(单元**逆序** `dispose`)。配置失效通道已删除,故没有 `unsubscribeConfig` 步。
|
|
10
|
+
*
|
|
11
|
+
* 固定六步:`flushRouters` → `flushMiddlewares` → `flushServices` → `disposeServices`
|
|
12
|
+
* → `flushConfig` → `flushResources`。
|
|
8
13
|
*
|
|
9
14
|
* 每步失败记日志、不阻断后续,最后聚合上报。
|
|
10
15
|
*/
|
|
11
|
-
export declare const SHUTDOWN_ORDER: readonly ["
|
|
16
|
+
export declare const SHUTDOWN_ORDER: readonly ["flushRouters", "flushMiddlewares", "flushServices", "disposeServices", "flushConfig", "flushResources"];
|
|
12
17
|
export type ShutdownStepName = (typeof SHUTDOWN_ORDER)[number];
|
|
13
18
|
export type ShutdownHandles = Record<ShutdownStepName, () => void | Promise<void>>;
|
|
14
19
|
export interface ShutdownLogger {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../source/app/shutdown.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../source/app/shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,oHAOjB,CAAA;AAEV,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"}
|
package/output/app/shutdown.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 固定关闭步(N1 §5)——**顺序写死在数组里**。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `disposer` 是所有槽位 ctx 的标配:每个阶段一个 `Disposer`,在**创建处**注册释放。
|
|
5
|
+
* 关闭时按阶段**逆序 flush**:`routers → middlewares → services → config → resources`
|
|
6
|
+
* (后创建的先销毁;Prisma / Redis 这类资源**最后**才断)。
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* `services` 阶段内部保持既有语义:先 flush 自己的 `disposer`(原 `stopProducers`),
|
|
9
|
+
* 再 `disposeServices`(单元**逆序** `dispose`)。配置失效通道已删除,故没有 `unsubscribeConfig` 步。
|
|
10
|
+
*
|
|
11
|
+
* 固定六步:`flushRouters` → `flushMiddlewares` → `flushServices` → `disposeServices`
|
|
12
|
+
* → `flushConfig` → `flushResources`。
|
|
8
13
|
*
|
|
9
14
|
* 每步失败记日志、不阻断后续,最后聚合上报。
|
|
10
15
|
*/
|
|
11
|
-
export const SHUTDOWN_ORDER = [
|
|
16
|
+
export const SHUTDOWN_ORDER = [
|
|
17
|
+
'flushRouters',
|
|
18
|
+
'flushMiddlewares',
|
|
19
|
+
'flushServices',
|
|
20
|
+
'disposeServices',
|
|
21
|
+
'flushConfig',
|
|
22
|
+
'flushResources',
|
|
23
|
+
];
|
|
12
24
|
/**
|
|
13
25
|
* 顺序执行关闭步骤;单步抛错记录后**继续**后续步骤,最后聚合上报
|
|
14
26
|
* (避免「一步报错导致后面资源没关」)。
|