@taicode/common-server 3.7.13 → 3.8.0
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/assemble.d.ts +16 -0
- package/output/app/assemble.d.ts.map +1 -0
- package/output/app/assemble.js +124 -0
- package/output/app/database.d.ts +36 -0
- package/output/app/database.d.ts.map +1 -0
- package/output/app/database.js +40 -0
- package/output/app/database.test.d.ts +2 -0
- package/output/app/database.test.d.ts.map +1 -0
- package/output/app/database.test.js +66 -0
- package/output/app/default-modules.d.ts +23 -0
- package/output/app/default-modules.d.ts.map +1 -0
- package/output/app/default-modules.js +40 -0
- package/output/app/define-server.d.ts +12 -0
- package/output/app/define-server.d.ts.map +1 -0
- package/output/app/define-server.js +289 -0
- package/output/app/define-server.test.d.ts +2 -0
- package/output/app/define-server.test.d.ts.map +1 -0
- package/output/app/define-server.test.js +299 -0
- package/output/app/env.d.ts +24 -0
- package/output/app/env.d.ts.map +1 -0
- package/output/app/env.js +38 -0
- package/output/app/env.test.d.ts +2 -0
- package/output/app/env.test.d.ts.map +1 -0
- package/output/app/env.test.js +78 -0
- package/output/app/index.d.ts +16 -0
- package/output/app/index.d.ts.map +1 -0
- package/output/app/index.js +9 -0
- package/output/app/module.d.ts +88 -0
- package/output/app/module.d.ts.map +1 -0
- package/output/app/module.js +34 -0
- package/output/app/shutdown.d.ts +9 -0
- package/output/app/shutdown.d.ts.map +1 -0
- package/output/app/shutdown.js +15 -0
- package/output/app/shutdown.test.d.ts +2 -0
- package/output/app/shutdown.test.d.ts.map +1 -0
- package/output/app/shutdown.test.js +149 -0
- package/output/app/signal.d.ts +12 -0
- package/output/app/signal.d.ts.map +1 -0
- package/output/app/signal.js +45 -0
- package/output/app/steps.d.ts +17 -0
- package/output/app/steps.d.ts.map +1 -0
- package/output/app/steps.js +41 -0
- package/output/app/types.d.ts +149 -0
- package/output/app/types.d.ts.map +1 -0
- package/output/app/types.js +1 -0
- package/package.json +6 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DatabaseModule, ModuleAssembly, ModuleName, ServerModule } from './module.js';
|
|
2
|
+
/** 装配结果:注册顺序、按名索引、被 `false` 跳过的名字、以及数据层模块句柄 */
|
|
3
|
+
export interface ResolvedModules {
|
|
4
|
+
order: ModuleName[];
|
|
5
|
+
byName: Partial<Record<ModuleName, ServerModule>>;
|
|
6
|
+
disabled: ModuleName[];
|
|
7
|
+
database?: DatabaseModule;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 装配期校验 + 槽位解析(design §3.2)。
|
|
11
|
+
*
|
|
12
|
+
* 报错**发生在 `defineServer()` 装配阶段**(返回 promise resolve 之前),不拖到 `listen` 才炸。
|
|
13
|
+
* 校验顺序:未知名 → 重复 / 键名不符 → 依赖被跳过 / 依赖环 → 未实现模块未显式 false → database 未提供。
|
|
14
|
+
*/
|
|
15
|
+
export declare function assembleModules(assembly?: ModuleAssembly): ResolvedModules;
|
|
16
|
+
//# sourceMappingURL=assemble.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../source/app/assemble.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE3F,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAA;IACjD,QAAQ,EAAE,UAAU,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B;AA2HD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,CAAC,EAAE,cAAc,GAAG,eAAe,CAQ1E"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createDefaultModules } from './default-modules.js';
|
|
2
|
+
import { MODULE_NAMES, PENDING_MODULES } from './module.js';
|
|
3
|
+
function isModuleName(value) {
|
|
4
|
+
return MODULE_NAMES.includes(value);
|
|
5
|
+
}
|
|
6
|
+
function assertKnownNames(provided) {
|
|
7
|
+
for (const key of Object.keys(provided)) {
|
|
8
|
+
if (!isModuleName(key)) {
|
|
9
|
+
throw new Error(`Unknown module "${key}". Allowed: ${MODULE_NAMES.join(', ')}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function assertNamesMatch(provided) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const key of Object.keys(provided)) {
|
|
16
|
+
const value = provided[key];
|
|
17
|
+
if (value === undefined || value === false) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (seen.has(value.name)) {
|
|
21
|
+
throw new Error(`Module "${value.name}" registered twice`);
|
|
22
|
+
}
|
|
23
|
+
if (value.name !== key) {
|
|
24
|
+
throw new Error(`Module key "${key}" != module.name "${value.name}"`);
|
|
25
|
+
}
|
|
26
|
+
seen.add(value.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function assertNoCycles(provided) {
|
|
30
|
+
const visiting = new Set();
|
|
31
|
+
const visited = new Set();
|
|
32
|
+
function visit(name, path) {
|
|
33
|
+
if (visited.has(name)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (visiting.has(name)) {
|
|
37
|
+
throw new Error(`Module dependency cycle detected: ${[...path, name].join(' -> ')}`);
|
|
38
|
+
}
|
|
39
|
+
visiting.add(name);
|
|
40
|
+
const value = provided[name];
|
|
41
|
+
const deps = value === undefined || value === false ? [] : value.dependsOn ?? [];
|
|
42
|
+
for (const dep of deps) {
|
|
43
|
+
if (isModuleName(dep)) {
|
|
44
|
+
visit(dep, [...path, name]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
visiting.delete(name);
|
|
48
|
+
visited.add(name);
|
|
49
|
+
}
|
|
50
|
+
for (const key of Object.keys(provided)) {
|
|
51
|
+
if (isModuleName(key)) {
|
|
52
|
+
visit(key, []);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function assertDependencies(provided) {
|
|
57
|
+
for (const key of Object.keys(provided)) {
|
|
58
|
+
const value = provided[key];
|
|
59
|
+
if (value === undefined || value === false) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const dep of value.dependsOn ?? []) {
|
|
63
|
+
if (!isModuleName(dep)) {
|
|
64
|
+
throw new Error(`Module "${value.name}" depends on unknown module "${dep}"`);
|
|
65
|
+
}
|
|
66
|
+
if (provided[dep] === false) {
|
|
67
|
+
throw new Error(`Module "${value.name}" depends on "${dep}" which is disabled`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
assertNoCycles(provided);
|
|
72
|
+
}
|
|
73
|
+
function assertPendingProvided(provided) {
|
|
74
|
+
for (const [name, stage] of Object.entries(PENDING_MODULES)) {
|
|
75
|
+
if (provided[name] === undefined) {
|
|
76
|
+
throw new Error(`Module "${name}" is not implemented until ${stage}; pass false to opt out`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function assertDatabaseProvided(provided) {
|
|
81
|
+
if (provided.database === undefined) {
|
|
82
|
+
throw new Error('Module "database" requires createPrismaDatabaseModule({...}) or false');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function resolveModules(provided) {
|
|
86
|
+
const defaults = createDefaultModules();
|
|
87
|
+
const byName = {};
|
|
88
|
+
const disabled = [];
|
|
89
|
+
for (const name of MODULE_NAMES) {
|
|
90
|
+
const value = provided[name];
|
|
91
|
+
if (value === false) {
|
|
92
|
+
disabled.push(name);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (value) {
|
|
96
|
+
byName[name] = value;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const fallback = defaults[name];
|
|
100
|
+
if (fallback) {
|
|
101
|
+
byName[name] = fallback;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const order = MODULE_NAMES.filter((name) => byName[name] !== undefined);
|
|
105
|
+
const candidate = byName.database;
|
|
106
|
+
// `database` 已由 `assertDatabaseProvided` 强制显式:走到这里要么是 `DatabaseModule`,要么是 `false`
|
|
107
|
+
const database = typeof candidate?.createClient === 'function' ? candidate : undefined;
|
|
108
|
+
return { order, byName, disabled, database };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 装配期校验 + 槽位解析(design §3.2)。
|
|
112
|
+
*
|
|
113
|
+
* 报错**发生在 `defineServer()` 装配阶段**(返回 promise resolve 之前),不拖到 `listen` 才炸。
|
|
114
|
+
* 校验顺序:未知名 → 重复 / 键名不符 → 依赖被跳过 / 依赖环 → 未实现模块未显式 false → database 未提供。
|
|
115
|
+
*/
|
|
116
|
+
export function assembleModules(assembly) {
|
|
117
|
+
const provided = (assembly ?? {});
|
|
118
|
+
assertKnownNames(provided);
|
|
119
|
+
assertNamesMatch(provided);
|
|
120
|
+
assertDependencies(provided);
|
|
121
|
+
assertPendingProvided(provided);
|
|
122
|
+
assertDatabaseProvided(provided);
|
|
123
|
+
return resolveModules(provided);
|
|
124
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DatabaseClientParams, DatabaseModule, PrismaErrorConstructors, PrismaOperationLabels } from './module.js';
|
|
2
|
+
export interface PrismaDatabaseModuleParams<Ctor extends new (...args: never[]) => unknown> {
|
|
3
|
+
/**
|
|
4
|
+
* 各仓生成的 client 构造函数(`@prisma/generated/*`)——**只能由各仓经参数注入**,
|
|
5
|
+
* common 不引用任何具体生成产物(决策 #8)。
|
|
6
|
+
*/
|
|
7
|
+
PrismaClient: Ctor;
|
|
8
|
+
/**
|
|
9
|
+
* 各仓生成的命名空间(`instanceof` 判定用;跨包 `instanceof` 不可靠,决策 #9)。
|
|
10
|
+
*/
|
|
11
|
+
PrismaNamespace: PrismaErrorConstructors;
|
|
12
|
+
/** 可选:覆盖 operation 标签;不传则用 common 默认集 */
|
|
13
|
+
operationLabels?: PrismaOperationLabels;
|
|
14
|
+
/**
|
|
15
|
+
* 可选:构造 Prisma client 的参数工厂。
|
|
16
|
+
*
|
|
17
|
+
* common **不依赖** `@prisma/client` 与其驱动适配器(如 `@prisma/adapter-pg`),
|
|
18
|
+
* 由调用方注入构造参数,例如:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* createClientArgs: (params) => [
|
|
22
|
+
* { adapter: new PrismaPg(params.postgresConfig, { schema: (params.postgresConfig as { schema: string }).schema }) },
|
|
23
|
+
* ]
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* 省略时等价于 `new PrismaClient()`(要求传入的 `PrismaClient` 自身可完成连接)。
|
|
27
|
+
*/
|
|
28
|
+
createClientArgs?: (params: DatabaseClientParams) => unknown[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `database` 模块的默认实现:Prisma(design §3.3 / 调研 D)。
|
|
32
|
+
*
|
|
33
|
+
* 返回类型即 `defineServer<Db>` 的 `Db`,调用方据此**推断**类型、无需强转(决策 10-2)。
|
|
34
|
+
*/
|
|
35
|
+
export declare function createPrismaDatabaseModule<Ctor extends new (...args: never[]) => unknown>(params: PrismaDatabaseModuleParams<Ctor>): DatabaseModule<InstanceType<Ctor>>;
|
|
36
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../source/app/database.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAevH,MAAM,WAAW,0BAA0B,CAAC,IAAI,SAAS,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO;IACxF;;;OAGG;IACH,YAAY,EAAE,IAAI,CAAA;IAClB;;OAEG;IACH,eAAe,EAAE,uBAAuB,CAAA;IACxC,yCAAyC;IACzC,eAAe,CAAC,EAAE,qBAAqB,CAAA;IACvC;;;;;;;;;;;;;OAaG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,OAAO,EAAE,CAAA;CAC/D;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,SAAS,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,EACvF,MAAM,EAAE,0BAA0B,CAAC,IAAI,CAAC,GACvC,cAAc,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CA2BpC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SystemError } from '@taicode/common-base';
|
|
2
|
+
/** common 默认的统一 operation 标签(design §2.3:抽进 common 时统一口径) */
|
|
3
|
+
const DEFAULT_OPERATION_LABELS = {
|
|
4
|
+
known: '已知请求错误',
|
|
5
|
+
unknown: '未知请求错误',
|
|
6
|
+
validation: '参数校验错误',
|
|
7
|
+
initialization: '初始化错误',
|
|
8
|
+
rustPanic: '引擎崩溃',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* `database` 模块的默认实现:Prisma(design §3.3 / 调研 D)。
|
|
12
|
+
*
|
|
13
|
+
* 返回类型即 `defineServer<Db>` 的 `Db`,调用方据此**推断**类型、无需强转(决策 10-2)。
|
|
14
|
+
*/
|
|
15
|
+
export function createPrismaDatabaseModule(params) {
|
|
16
|
+
const labels = { ...DEFAULT_OPERATION_LABELS, ...params.operationLabels };
|
|
17
|
+
return {
|
|
18
|
+
name: 'database',
|
|
19
|
+
createClient: (clientParams) => {
|
|
20
|
+
const args = params.createClientArgs ? params.createClientArgs(clientParams) : [];
|
|
21
|
+
return Reflect.construct(params.PrismaClient, args);
|
|
22
|
+
},
|
|
23
|
+
classifyError: (error) => {
|
|
24
|
+
const hit = (Ctor, label) => (error instanceof Ctor ? label : null);
|
|
25
|
+
const namespace = params.PrismaNamespace;
|
|
26
|
+
const label = hit(namespace.PrismaClientKnownRequestError, labels.known) ??
|
|
27
|
+
hit(namespace.PrismaClientUnknownRequestError, labels.unknown) ??
|
|
28
|
+
hit(namespace.PrismaClientValidationError, labels.validation) ??
|
|
29
|
+
hit(namespace.PrismaClientInitializationError, labels.initialization) ??
|
|
30
|
+
hit(namespace.PrismaClientRustPanicError, labels.rustPanic);
|
|
31
|
+
return label ? new SystemError('DATABASE_ERROR', { operation: label }, label) : null;
|
|
32
|
+
},
|
|
33
|
+
dispose: async (db) => {
|
|
34
|
+
const target = db;
|
|
35
|
+
if (typeof target.$disconnect === 'function') {
|
|
36
|
+
await target.$disconnect();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.test.d.ts","sourceRoot":"","sources":["../../source/app/database.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SystemError } from '@taicode/common-base';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createPrismaDatabaseModule } from './database.js';
|
|
4
|
+
const TEST_CONFIG = { loadConfig: () => ({ debug: false, port: 0 }) };
|
|
5
|
+
const NAMESPACE = {
|
|
6
|
+
PrismaClientKnownRequestError: class extends Error {
|
|
7
|
+
},
|
|
8
|
+
PrismaClientUnknownRequestError: class extends Error {
|
|
9
|
+
},
|
|
10
|
+
PrismaClientValidationError: class extends Error {
|
|
11
|
+
},
|
|
12
|
+
PrismaClientInitializationError: class extends Error {
|
|
13
|
+
},
|
|
14
|
+
PrismaClientRustPanicError: class extends Error {
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
class Client {
|
|
18
|
+
static lastArgs = [];
|
|
19
|
+
disconnected = false;
|
|
20
|
+
constructor(...args) {
|
|
21
|
+
Client.lastArgs = args;
|
|
22
|
+
}
|
|
23
|
+
async $disconnect() {
|
|
24
|
+
this.disconnected = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
describe('createPrismaDatabaseModule', () => {
|
|
28
|
+
it('createClient 使用注入的构造函数与参数工厂(common 不依赖 @prisma/client)', async () => {
|
|
29
|
+
const database = createPrismaDatabaseModule({
|
|
30
|
+
PrismaClient: Client,
|
|
31
|
+
PrismaNamespace: NAMESPACE,
|
|
32
|
+
createClientArgs: (params) => [{ adapter: params.postgresConfig }],
|
|
33
|
+
});
|
|
34
|
+
const client = await database.createClient({ config: TEST_CONFIG, postgresConfig: { schema: 'test' } });
|
|
35
|
+
expect(client).toBeInstanceOf(Client);
|
|
36
|
+
expect(Client.lastArgs).toEqual([{ adapter: { schema: 'test' } }]);
|
|
37
|
+
});
|
|
38
|
+
it('省略 createClientArgs 时等价于 new PrismaClient()', async () => {
|
|
39
|
+
const database = createPrismaDatabaseModule({ PrismaClient: Client, PrismaNamespace: NAMESPACE });
|
|
40
|
+
await database.createClient({ config: TEST_CONFIG, postgresConfig: undefined });
|
|
41
|
+
expect(Client.lastArgs).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
it('classifyError 命中 5 类错误时返回 SystemError(DATABASE_ERROR),未命中返回 null', () => {
|
|
44
|
+
const database = createPrismaDatabaseModule({ PrismaClient: Client, PrismaNamespace: NAMESPACE });
|
|
45
|
+
const known = database.classifyError(new NAMESPACE.PrismaClientKnownRequestError());
|
|
46
|
+
expect(SystemError.is(known)).toBe(true);
|
|
47
|
+
expect(known?.type).toBe('DATABASE_ERROR');
|
|
48
|
+
expect(known?.context?.operation).toBe('已知请求错误');
|
|
49
|
+
expect(database.classifyError(new NAMESPACE.PrismaClientRustPanicError())?.context?.operation).toBe('引擎崩溃');
|
|
50
|
+
expect(database.classifyError(new Error('plain'))).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
it('operationLabels 可覆盖默认标签', () => {
|
|
53
|
+
const database = createPrismaDatabaseModule({
|
|
54
|
+
PrismaClient: Client,
|
|
55
|
+
PrismaNamespace: NAMESPACE,
|
|
56
|
+
operationLabels: { known: 'ClientKnown' },
|
|
57
|
+
});
|
|
58
|
+
expect(database.classifyError(new NAMESPACE.PrismaClientKnownRequestError())?.context?.operation).toBe('ClientKnown');
|
|
59
|
+
});
|
|
60
|
+
it('dispose 调用 db.$disconnect()', async () => {
|
|
61
|
+
const database = createPrismaDatabaseModule({ PrismaClient: Client, PrismaNamespace: NAMESPACE });
|
|
62
|
+
const client = await database.createClient({ config: TEST_CONFIG, postgresConfig: undefined });
|
|
63
|
+
await database.dispose?.(client);
|
|
64
|
+
expect(client.disconnected).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ModuleName, ServerModule } from './module.js';
|
|
2
|
+
/**
|
|
3
|
+
* 默认 `env` 模块(P1 空实现)。
|
|
4
|
+
*
|
|
5
|
+
* `loadEnvFiles()` 是独立导出、在骨架之前调用(决策 #5),因此这里只作为「封闭集合内的合法名字」存在。
|
|
6
|
+
*/
|
|
7
|
+
export declare function createEnvModule(): ServerModule;
|
|
8
|
+
/**
|
|
9
|
+
* P1 默认 `redis` 模块(透传)。
|
|
10
|
+
*
|
|
11
|
+
* 骨架只持有 `createRedis()` 返回的 `{ client, dispose }`;未传 `createRedis` 时第 4 步空转
|
|
12
|
+
* (notice 即是此形态)。
|
|
13
|
+
*/
|
|
14
|
+
export declare function createRedisModule(): ServerModule;
|
|
15
|
+
/**
|
|
16
|
+
* P1 有默认实现的模块(`env` / `redis`)。
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ 故意不含 `database`:common 无法引用各仓生成的 Prisma client(决策 #8),给个「不建连」的
|
|
19
|
+
* 空实现会埋下静默无 DB 的雷,因此 `database` 必须由调用方**显式**给出
|
|
20
|
+
* `createPrismaDatabaseModule({ ... })` 或 `false`(装配期强制,见 `assemble.ts`)。
|
|
21
|
+
*/
|
|
22
|
+
export declare function createDefaultModules(): Partial<Record<ModuleName, ServerModule>>;
|
|
23
|
+
//# sourceMappingURL=default-modules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-modules.d.ts","sourceRoot":"","sources":["../../source/app/default-modules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE3D;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,YAAY,CAYhD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAKhF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认 `env` 模块(P1 空实现)。
|
|
3
|
+
*
|
|
4
|
+
* `loadEnvFiles()` 是独立导出、在骨架之前调用(决策 #5),因此这里只作为「封闭集合内的合法名字」存在。
|
|
5
|
+
*/
|
|
6
|
+
export function createEnvModule() {
|
|
7
|
+
return { name: 'env' };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* P1 默认 `redis` 模块(透传)。
|
|
11
|
+
*
|
|
12
|
+
* 骨架只持有 `createRedis()` 返回的 `{ client, dispose }`;未传 `createRedis` 时第 4 步空转
|
|
13
|
+
* (notice 即是此形态)。
|
|
14
|
+
*/
|
|
15
|
+
export function createRedisModule() {
|
|
16
|
+
return {
|
|
17
|
+
name: 'redis',
|
|
18
|
+
steps: {
|
|
19
|
+
createRedis: (ctx) => {
|
|
20
|
+
const handle = ctx.options.createRedis?.(ctx.options.config);
|
|
21
|
+
if (handle) {
|
|
22
|
+
ctx.state.redis = handle;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* P1 有默认实现的模块(`env` / `redis`)。
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ 故意不含 `database`:common 无法引用各仓生成的 Prisma client(决策 #8),给个「不建连」的
|
|
32
|
+
* 空实现会埋下静默无 DB 的雷,因此 `database` 必须由调用方**显式**给出
|
|
33
|
+
* `createPrismaDatabaseModule({ ... })` 或 `false`(装配期强制,见 `assemble.ts`)。
|
|
34
|
+
*/
|
|
35
|
+
export function createDefaultModules() {
|
|
36
|
+
return {
|
|
37
|
+
env: createEnvModule(),
|
|
38
|
+
redis: createRedisModule(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DefineServerOptions, ServerInstance } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 模块化装配 + 15 步固定编排(design §3 / §4 / §5;提案 §11)。
|
|
4
|
+
*
|
|
5
|
+
* 返回**原生 `FastifyInstance`**(不包 `fastify-plugin`、不返回包装对象)——调用方可直接
|
|
6
|
+
* `app.swagger()` / `app.inject()` / `app.decorate()`。`mode: 'openapi-build'` 时不 listen,
|
|
7
|
+
* 由调用方读 `app.swagger()`。
|
|
8
|
+
*
|
|
9
|
+
* `Db` 由 `modules.database`(`database` 模块的 `createClient` 返回类型)推断(决策 10-2)。
|
|
10
|
+
*/
|
|
11
|
+
export declare function defineServer<Db = unknown, S = unknown>(options: DefineServerOptions<Db, S>): Promise<ServerInstance>;
|
|
12
|
+
//# sourceMappingURL=define-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-server.d.ts","sourceRoot":"","sources":["../../source/app/define-server.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,mBAAmB,EAInB,cAAc,EAIf,MAAM,YAAY,CAAA;AA4QnB;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC1D,OAAO,EAAE,mBAAmB,CAAC,EAAE,EAAE,CAAC,CAAC,GAClC,OAAO,CAAC,cAAc,CAAC,CAsBzB"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Disposer } from '@taicode/common-base';
|
|
2
|
+
import Fastify, {} from 'fastify';
|
|
3
|
+
import { assembleModules } from './assemble.js';
|
|
4
|
+
import { attachSignalHandlers } from './signal.js';
|
|
5
|
+
import { SHUTDOWN_ORDER } from './shutdown.js';
|
|
6
|
+
import { SKIP_BY_MODE, STEP_ORDER } from './steps.js';
|
|
7
|
+
const NOOP_LOGGER = {
|
|
8
|
+
debug: () => undefined,
|
|
9
|
+
info: () => undefined,
|
|
10
|
+
warn: () => undefined,
|
|
11
|
+
error: () => undefined,
|
|
12
|
+
};
|
|
13
|
+
function requireApp(state) {
|
|
14
|
+
if (!state.app) {
|
|
15
|
+
throw new Error('Fastify instance has not been created yet');
|
|
16
|
+
}
|
|
17
|
+
return state.app;
|
|
18
|
+
}
|
|
19
|
+
function resolveLog(options, state) {
|
|
20
|
+
if (options.log) {
|
|
21
|
+
return options.log;
|
|
22
|
+
}
|
|
23
|
+
if (state.app) {
|
|
24
|
+
return state.app.log;
|
|
25
|
+
}
|
|
26
|
+
return NOOP_LOGGER;
|
|
27
|
+
}
|
|
28
|
+
function collectHooks(resolved) {
|
|
29
|
+
const hooks = {};
|
|
30
|
+
for (const name of resolved.order) {
|
|
31
|
+
const module = resolved.byName[name];
|
|
32
|
+
if (module?.hooks) {
|
|
33
|
+
Object.assign(hooks, module.hooks);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return hooks;
|
|
37
|
+
}
|
|
38
|
+
function collectPlugins(resolved, options) {
|
|
39
|
+
const plugins = [];
|
|
40
|
+
for (const name of resolved.order) {
|
|
41
|
+
const module = resolved.byName[name];
|
|
42
|
+
if (module?.plugins) {
|
|
43
|
+
plugins.push(...module.plugins);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (options.extraPlugins) {
|
|
47
|
+
plugins.push(...options.extraPlugins);
|
|
48
|
+
}
|
|
49
|
+
return plugins;
|
|
50
|
+
}
|
|
51
|
+
function buildStepImplementations(resolved, options, moduleHooks) {
|
|
52
|
+
const implementations = {};
|
|
53
|
+
// 1) 固定骨架步骤的内置实现(模块可覆盖既定槽位,但无法新增 / 重排步骤)
|
|
54
|
+
implementations.createFastify = (ctx) => {
|
|
55
|
+
ctx.state.app = Fastify({ logger: ctx.options.config.loadConfig().debug, trustProxy: true });
|
|
56
|
+
};
|
|
57
|
+
implementations.registerCorePlugins = async (ctx) => {
|
|
58
|
+
const app = requireApp(ctx.state);
|
|
59
|
+
for (const plugin of collectPlugins(resolved, ctx.options)) {
|
|
60
|
+
await app.register(plugin);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
implementations.onClose = (ctx) => {
|
|
64
|
+
const shutdownImplementations = buildShutdownImplementations(resolved, moduleHooks);
|
|
65
|
+
requireApp(ctx.state).addHook('onClose', () => runShutdown(ctx, shutdownImplementations, moduleHooks));
|
|
66
|
+
};
|
|
67
|
+
implementations.ready = async (ctx) => {
|
|
68
|
+
await requireApp(ctx.state).ready();
|
|
69
|
+
};
|
|
70
|
+
implementations.listen = async (ctx) => {
|
|
71
|
+
const app = requireApp(ctx.state);
|
|
72
|
+
const host = ctx.options.serverOptions?.host ?? '0.0.0.0';
|
|
73
|
+
const port = ctx.options.serverOptions?.port ?? ctx.options.config.loadConfig().port;
|
|
74
|
+
await app.listen({ port, host });
|
|
75
|
+
if (ctx.options.signal !== false) {
|
|
76
|
+
attachSignalHandlers(app, ctx.log);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// 2) 数据层端口 → 第 3 步(database 模块的整体覆盖也在此生效)
|
|
80
|
+
const database = resolved.database;
|
|
81
|
+
if (database) {
|
|
82
|
+
implementations['database.createClient'] = async (ctx) => {
|
|
83
|
+
const loadPostgresConfig = ctx.options.config.loadPostgresConfig;
|
|
84
|
+
const postgresConfig = loadPostgresConfig ? await loadPostgresConfig() : undefined;
|
|
85
|
+
ctx.state.db = await database.createClient({ config: ctx.options.config, postgresConfig });
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// 3) 模块贡献的步骤实现(注册顺序 = MODULE_NAMES 顺序)
|
|
89
|
+
for (const name of resolved.order) {
|
|
90
|
+
const module = resolved.byName[name];
|
|
91
|
+
if (module?.steps) {
|
|
92
|
+
Object.assign(implementations, module.steps);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 4) 调用方槽位(调用方优先级最高)
|
|
96
|
+
if (options.setupCompilers) {
|
|
97
|
+
implementations.setupCompilers = (ctx) => {
|
|
98
|
+
ctx.options.setupCompilers?.(requireApp(ctx.state));
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (options.initGlobalConfig) {
|
|
102
|
+
implementations.initGlobalConfig = (ctx) => {
|
|
103
|
+
const globalConfigOptions = {
|
|
104
|
+
redisUrl: ctx.options.config.resolveRedisUrl?.(),
|
|
105
|
+
channelPrefix: ctx.options.config.resolveCachePrefix?.(),
|
|
106
|
+
};
|
|
107
|
+
const dispose = ctx.options.initGlobalConfig?.(ctx.state.db, globalConfigOptions);
|
|
108
|
+
if (typeof dispose === 'function') {
|
|
109
|
+
ctx.state.disposeConfig = dispose;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (options.initClients) {
|
|
114
|
+
implementations.initClients = async (ctx) => {
|
|
115
|
+
const unsubscribe = await ctx.options.initClients?.({
|
|
116
|
+
db: ctx.state.db,
|
|
117
|
+
redis: ctx.state.redis?.client,
|
|
118
|
+
globalConfig: ctx.state.globalConfig,
|
|
119
|
+
});
|
|
120
|
+
if (typeof unsubscribe === 'function') {
|
|
121
|
+
ctx.state.unsubscribe = unsubscribe;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
implementations.createServices = (ctx) => {
|
|
126
|
+
ctx.state.services = ctx.options.createServices({
|
|
127
|
+
db: ctx.state.db,
|
|
128
|
+
redis: ctx.state.redis?.client,
|
|
129
|
+
globalConfig: ctx.state.globalConfig,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
if (options.initServices || options.afterInit) {
|
|
133
|
+
implementations.initServices = async (ctx) => {
|
|
134
|
+
if (ctx.options.initServices) {
|
|
135
|
+
await ctx.options.initServices({ services: ctx.state.services });
|
|
136
|
+
}
|
|
137
|
+
if (ctx.options.afterInit) {
|
|
138
|
+
const disposer = await ctx.options.afterInit({
|
|
139
|
+
db: ctx.state.db,
|
|
140
|
+
redis: ctx.state.redis?.client,
|
|
141
|
+
globalConfig: ctx.state.globalConfig,
|
|
142
|
+
services: ctx.state.services,
|
|
143
|
+
});
|
|
144
|
+
if (typeof disposer === 'function') {
|
|
145
|
+
ctx.state.disposer.addDisposer(disposer);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (options.backgroundWorkers) {
|
|
151
|
+
implementations.backgroundWorkers = async (ctx) => {
|
|
152
|
+
await ctx.options.backgroundWorkers?.({
|
|
153
|
+
db: ctx.state.db,
|
|
154
|
+
redis: ctx.state.redis?.client,
|
|
155
|
+
globalConfig: ctx.state.globalConfig,
|
|
156
|
+
services: ctx.state.services,
|
|
157
|
+
disposer: ctx.state.disposer,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
implementations.registerRouters = async (ctx) => {
|
|
162
|
+
await ctx.options.registerRouters(requireApp(ctx.state), { services: ctx.state.services });
|
|
163
|
+
};
|
|
164
|
+
if (options.afterReady) {
|
|
165
|
+
implementations.afterReady = async (ctx) => {
|
|
166
|
+
await ctx.options.afterReady?.({ app: requireApp(ctx.state), services: ctx.state.services });
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return implementations;
|
|
170
|
+
}
|
|
171
|
+
async function disposeServiceMap(services) {
|
|
172
|
+
if (typeof services !== 'object' || services === null) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const record = services;
|
|
176
|
+
const pending = Object.values(record).map((service) => {
|
|
177
|
+
const candidate = service;
|
|
178
|
+
return typeof candidate.dispose === 'function' ? candidate.dispose() : undefined;
|
|
179
|
+
});
|
|
180
|
+
await Promise.allSettled(pending);
|
|
181
|
+
}
|
|
182
|
+
function buildShutdownImplementations(resolved, moduleHooks) {
|
|
183
|
+
const implementations = {
|
|
184
|
+
stopProducers: async (ctx) => {
|
|
185
|
+
await ctx.state.disposer.dispose();
|
|
186
|
+
},
|
|
187
|
+
unsubscribeConfig: async (ctx) => {
|
|
188
|
+
await ctx.state.unsubscribe?.();
|
|
189
|
+
},
|
|
190
|
+
disposeConfig: async (ctx) => {
|
|
191
|
+
await ctx.state.disposeConfig?.();
|
|
192
|
+
},
|
|
193
|
+
disposeServices: async (ctx) => {
|
|
194
|
+
const disposeServices = ctx.options.disposeServices ?? moduleHooks.disposeServices;
|
|
195
|
+
if (disposeServices) {
|
|
196
|
+
await disposeServices({ services: ctx.state.services });
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
await disposeServiceMap(ctx.state.services);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
disposeClients: async (ctx) => {
|
|
203
|
+
const disposeClients = ctx.options.disposeClients ?? moduleHooks.disposeClients;
|
|
204
|
+
await disposeClients?.();
|
|
205
|
+
},
|
|
206
|
+
disposeRedis: async (ctx) => {
|
|
207
|
+
await ctx.state.redis?.dispose();
|
|
208
|
+
},
|
|
209
|
+
disposeDb: async (ctx) => {
|
|
210
|
+
await resolved.database?.dispose?.(ctx.state.db);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
for (const name of resolved.order) {
|
|
214
|
+
const module = resolved.byName[name];
|
|
215
|
+
if (module?.shutdown) {
|
|
216
|
+
Object.assign(implementations, module.shutdown);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return implementations;
|
|
220
|
+
}
|
|
221
|
+
async function runShutdown(ctx, implementations, moduleHooks) {
|
|
222
|
+
const errors = [];
|
|
223
|
+
const options = ctx.options;
|
|
224
|
+
const shutdown = options.shutdown ?? moduleHooks.shutdown;
|
|
225
|
+
const beforeShutdown = options.beforeShutdown ?? moduleHooks.beforeShutdown;
|
|
226
|
+
if (shutdown) {
|
|
227
|
+
try {
|
|
228
|
+
await shutdown('onClose');
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
errors.push(error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
if (beforeShutdown) {
|
|
236
|
+
try {
|
|
237
|
+
await beforeShutdown();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
errors.push(error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const step of SHUTDOWN_ORDER) {
|
|
244
|
+
const implementation = implementations[step];
|
|
245
|
+
if (!implementation) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await implementation(ctx);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
errors.push(error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (errors.length > 0) {
|
|
257
|
+
ctx.log.error({ errors, count: errors.length }, '[shutdown] completed with errors');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 模块化装配 + 15 步固定编排(design §3 / §4 / §5;提案 §11)。
|
|
262
|
+
*
|
|
263
|
+
* 返回**原生 `FastifyInstance`**(不包 `fastify-plugin`、不返回包装对象)——调用方可直接
|
|
264
|
+
* `app.swagger()` / `app.inject()` / `app.decorate()`。`mode: 'openapi-build'` 时不 listen,
|
|
265
|
+
* 由调用方读 `app.swagger()`。
|
|
266
|
+
*
|
|
267
|
+
* `Db` 由 `modules.database`(`database` 模块的 `createClient` 返回类型)推断(决策 10-2)。
|
|
268
|
+
*/
|
|
269
|
+
export async function defineServer(options) {
|
|
270
|
+
const internal = options;
|
|
271
|
+
const mode = internal.mode ?? 'production';
|
|
272
|
+
const resolved = assembleModules(internal.modules);
|
|
273
|
+
const state = { disposer: new Disposer() };
|
|
274
|
+
const moduleHooks = collectHooks(resolved);
|
|
275
|
+
const implementations = buildStepImplementations(resolved, internal, moduleHooks);
|
|
276
|
+
const skipped = new Set(SKIP_BY_MODE[mode]);
|
|
277
|
+
for (const step of STEP_ORDER) {
|
|
278
|
+
if (skipped.has(step)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const ctx = { options: internal, state, log: resolveLog(internal, state) };
|
|
282
|
+
ctx.log.debug({ step }, '[boot]');
|
|
283
|
+
const implementation = implementations[step];
|
|
284
|
+
if (implementation) {
|
|
285
|
+
await implementation(ctx);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return requireApp(state);
|
|
289
|
+
}
|