@tyno/tyno 2.1.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.
Files changed (45) hide show
  1. package/README-zh.md +572 -0
  2. package/README.md +555 -0
  3. package/example/app.ts +173 -0
  4. package/example/public/index.html +1 -0
  5. package/example/public/test.json +1 -0
  6. package/package.json +63 -0
  7. package/scripts/build.mjs +97 -0
  8. package/scripts/rename-cjs.mjs +23 -0
  9. package/src/application.ts +304 -0
  10. package/src/cache/drivers/file.ts +79 -0
  11. package/src/cache/drivers/memory.ts +72 -0
  12. package/src/cache/drivers/redis.ts +72 -0
  13. package/src/cache/index.ts +5 -0
  14. package/src/cache/manager.ts +106 -0
  15. package/src/cache/types.ts +24 -0
  16. package/src/cache-facade.ts +64 -0
  17. package/src/compose.ts +139 -0
  18. package/src/context.ts +5 -0
  19. package/src/errors/app-error.ts +37 -0
  20. package/src/errors/http-error.ts +34 -0
  21. package/src/errors/index.ts +4 -0
  22. package/src/errors/runtime-error.ts +19 -0
  23. package/src/facade/index.ts +3 -0
  24. package/src/index.ts +29 -0
  25. package/src/middlewares/compress.ts +101 -0
  26. package/src/middlewares/cors.ts +57 -0
  27. package/src/middlewares/error-page.ts +89 -0
  28. package/src/middlewares/index.ts +9 -0
  29. package/src/middlewares/request-id.ts +47 -0
  30. package/src/middlewares/static.ts +138 -0
  31. package/src/mime.ts +38 -0
  32. package/src/request/body-parser.ts +61 -0
  33. package/src/request/index.ts +273 -0
  34. package/src/request/multipart-parser.ts +360 -0
  35. package/src/request-global.ts +31 -0
  36. package/src/response/sse.ts +54 -0
  37. package/src/response.ts +177 -0
  38. package/src/router/index.ts +290 -0
  39. package/src/router/node.ts +15 -0
  40. package/src/router/parse-path.ts +18 -0
  41. package/src/types.ts +109 -0
  42. package/test/functional.test.ts +614 -0
  43. package/tsconfig.build.json +13 -0
  44. package/tsconfig.cjs.json +9 -0
  45. package/tsconfig.json +21 -0
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { CacheDriver } from '../types.ts';
4
+
5
+ interface FileEntry {
6
+ value: string;
7
+ expires: number | null; // 毫秒时间戳,null 表示永不过期
8
+ }
9
+
10
+ /** 文件缓存驱动:每个 key 一个 JSON 文件 */
11
+ export class FileDriver implements CacheDriver {
12
+ private dir: string;
13
+
14
+ constructor(dir: string = './storage/cache') {
15
+ this.dir = path.resolve(dir);
16
+ fs.mkdirSync(this.dir, { recursive: true });
17
+ }
18
+
19
+ async get(key: string): Promise<string | null> {
20
+ const filePath = this._keyPath(key);
21
+ try {
22
+ const data = JSON.parse(await fs.promises.readFile(filePath, 'utf-8')) as FileEntry;
23
+ if (data.expires !== null && Date.now() > data.expires) {
24
+ await fs.promises.unlink(filePath).catch(() => {});
25
+ return null;
26
+ }
27
+ return data.value;
28
+ } catch (e: unknown) {
29
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
30
+ throw e;
31
+ }
32
+ }
33
+
34
+ async set(key: string, value: string, ttl?: number): Promise<void> {
35
+ const expires = ttl && ttl > 0 ? Date.now() + ttl * 1000 : null;
36
+ const filePath = this._keyPath(key);
37
+ await fs.promises.writeFile(filePath, JSON.stringify({ value, expires }), 'utf-8');
38
+ }
39
+
40
+ async delete(key: string): Promise<boolean> {
41
+ try {
42
+ await fs.promises.unlink(this._keyPath(key));
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ async has(key: string): Promise<boolean> {
50
+ const filePath = this._keyPath(key);
51
+ try {
52
+ const data = JSON.parse(await fs.promises.readFile(filePath, 'utf-8')) as FileEntry;
53
+ if (data.expires !== null && Date.now() > data.expires) {
54
+ await fs.promises.unlink(filePath).catch(() => {});
55
+ return false;
56
+ }
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ async clear(): Promise<void> {
64
+ try {
65
+ const files = await fs.promises.readdir(this.dir);
66
+ await Promise.all(files.map(f => fs.promises.unlink(path.join(this.dir, f)).catch(() => {})));
67
+ } catch { /* ignore */ }
68
+ }
69
+
70
+ async close(): Promise<void> {
71
+ // 文件驱动无需关闭连接
72
+ }
73
+
74
+ private _keyPath(key: string): string {
75
+ // 将 key 中的 : / 等替换为 _,防止路径问题
76
+ const safe = key.replace(/[^a-zA-Z0-9_-]/g, '_');
77
+ return path.join(this.dir, `${safe}.json`);
78
+ }
79
+ }
@@ -0,0 +1,72 @@
1
+ import type { CacheDriver } from '../types.ts';
2
+
3
+ interface CacheEntry {
4
+ value: string;
5
+ expires: number; // 毫秒时间戳,0 表示永不过期
6
+ }
7
+
8
+ /** 内存缓存驱动:基于 Map + TTL */
9
+ export class MemoryDriver implements CacheDriver {
10
+ private store = new Map<string, CacheEntry>();
11
+ private maxSize: number;
12
+
13
+ constructor(maxSize: number = 10000) {
14
+ this.maxSize = maxSize;
15
+ }
16
+
17
+ async get(key: string): Promise<string | null> {
18
+ const entry = this.store.get(key);
19
+ if (!entry) return null;
20
+ if (entry.expires !== 0 && Date.now() > entry.expires) {
21
+ this.store.delete(key);
22
+ return null;
23
+ }
24
+ return entry.value;
25
+ }
26
+
27
+ async set(key: string, value: string, ttl?: number): Promise<void> {
28
+ // 容量保护:满时清理过期条目,仍满则删除最旧条目
29
+ if (this.store.size >= this.maxSize && !this.store.has(key)) {
30
+ this._evict();
31
+ }
32
+ const expires = ttl && ttl > 0 ? Date.now() + ttl * 1000 : 0;
33
+ this.store.set(key, { value, expires });
34
+ }
35
+
36
+ async delete(key: string): Promise<boolean> {
37
+ return this.store.delete(key);
38
+ }
39
+
40
+ async has(key: string): Promise<boolean> {
41
+ const entry = this.store.get(key);
42
+ if (!entry) return false;
43
+ if (entry.expires !== 0 && Date.now() > entry.expires) {
44
+ this.store.delete(key);
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+
50
+ async clear(): Promise<void> {
51
+ this.store.clear();
52
+ }
53
+
54
+ async close(): Promise<void> {
55
+ this.store.clear();
56
+ }
57
+
58
+ /** 懒清理:删除过期条目,仍超容量则删除最旧的条目 */
59
+ private _evict(): void {
60
+ const now = Date.now();
61
+ for (const [key, entry] of this.store) {
62
+ if (entry.expires !== 0 && now > entry.expires) {
63
+ this.store.delete(key);
64
+ }
65
+ }
66
+ // 仍满则删除最旧条目
67
+ if (this.store.size >= this.maxSize) {
68
+ const first = this.store.keys().next().value;
69
+ if (first !== undefined) this.store.delete(first);
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,72 @@
1
+ import type { CacheDriver } from '../types.ts';
2
+
3
+ /** Redis 缓存驱动:基于官方 redis 包(@redis/client) */
4
+ export class RedisDriver implements CacheDriver {
5
+ private client: Record<string, (...args: unknown[]) => Promise<unknown>>;
6
+
7
+ private constructor(client: Record<string, (...args: unknown[]) => Promise<unknown>>) {
8
+ this.client = client;
9
+ }
10
+
11
+ /**
12
+ * 创建 RedisDriver 实例。
13
+ * 运行时动态加载官方 redis 包,未安装时抛出明确错误。
14
+ */
15
+ static async create(config: { host?: string; port?: number; password?: string; db?: number } = {}): Promise<RedisDriver> {
16
+ // 动态构造模块名,避免 TypeScript 静态分析
17
+ const modName = 'redis';
18
+ let createClient: (...args: unknown[]) => unknown;
19
+ try {
20
+ const mod = await (Function('m', 'return import(m)') as (m: string) => Promise<unknown>)(modName);
21
+ createClient = (mod as Record<string, unknown>).createClient as (...args: unknown[]) => unknown;
22
+ if (!createClient) throw new Error('createClient not found');
23
+ } catch {
24
+ throw new Error(
25
+ '[tyno] Redis cache driver requires the official "redis" package. Install it: npm install redis'
26
+ );
27
+ }
28
+
29
+ const socket: Record<string, unknown> = {
30
+ host: config.host ?? '127.0.0.1',
31
+ port: config.port ?? 6379,
32
+ };
33
+ const opts: Record<string, unknown> = { socket };
34
+ if (config.password) opts.password = config.password;
35
+ if (config.db != null) opts.database = config.db;
36
+
37
+ const client = createClient(opts) as Record<string, (...args: unknown[]) => Promise<unknown>>;
38
+ await client.connect();
39
+ return new RedisDriver(client);
40
+ }
41
+
42
+ async get(key: string): Promise<string | null> {
43
+ const val = await this.client.get(key) as string | null;
44
+ return val ?? null;
45
+ }
46
+
47
+ async set(key: string, value: string, ttl?: number): Promise<void> {
48
+ if (ttl && ttl > 0) {
49
+ await this.client.set(key, value, { EX: ttl });
50
+ } else {
51
+ await this.client.set(key, value);
52
+ }
53
+ }
54
+
55
+ async delete(key: string): Promise<boolean> {
56
+ const count = await this.client.del(key) as number;
57
+ return count > 0;
58
+ }
59
+
60
+ async has(key: string): Promise<boolean> {
61
+ const exists = await this.client.exists(key) as number;
62
+ return exists > 0;
63
+ }
64
+
65
+ async clear(): Promise<void> {
66
+ await this.client.flushDb();
67
+ }
68
+
69
+ async close(): Promise<void> {
70
+ await this.client.quit();
71
+ }
72
+ }
@@ -0,0 +1,5 @@
1
+ export { CacheManager } from './manager.ts';
2
+ export { MemoryDriver } from './drivers/memory.ts';
3
+ export { FileDriver } from './drivers/file.ts';
4
+ export { RedisDriver } from './drivers/redis.ts';
5
+ export type { CacheDriver, CacheConfig } from './types.ts';
@@ -0,0 +1,106 @@
1
+ import { MemoryDriver } from './drivers/memory.ts';
2
+ import { FileDriver } from './drivers/file.ts';
3
+ import { RedisDriver } from './drivers/redis.ts';
4
+ import type { CacheDriver, CacheConfig } from './types.ts';
5
+
6
+ /**
7
+ * 缓存管理器:统一 API + 自动 JSON 序列化 + 驱动委托。
8
+ *
9
+ * @example
10
+ * const cache = new CacheManager({ driver: 'memory', ttl: 60 });
11
+ * await cache.set('user:1', { name: 'Alice' });
12
+ * const user = await cache.get('user:1'); // { name: 'Alice' }
13
+ * await cache.remember('config', 3600, () => loadConfig());
14
+ */
15
+ export class CacheManager {
16
+ private driver: CacheDriver | null = null;
17
+ private config: CacheConfig;
18
+ private prefix: string;
19
+ private defaultTtl: number;
20
+
21
+ constructor(config: CacheConfig = {}) {
22
+ this.config = config;
23
+ this.prefix = config.prefix ?? 'tyno:';
24
+ this.defaultTtl = config.ttl ?? 3600;
25
+ }
26
+
27
+ /** 获取缓存值,不存在返回 null */
28
+ async get<T = unknown>(key: string): Promise<T | null> {
29
+ const raw = await (await this._driver()).get(this.prefix + key);
30
+ if (raw === null) return null;
31
+ try { return JSON.parse(raw) as T; }
32
+ catch { return raw as unknown as T; }
33
+ }
34
+
35
+ /** 设置缓存值(自动 JSON 序列化),ttl 单位秒,省略用默认值 */
36
+ async set(key: string, value: unknown, ttl?: number): Promise<void> {
37
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
38
+ await (await this._driver()).set(this.prefix + key, serialized, ttl ?? this.defaultTtl);
39
+ }
40
+
41
+ /** 检查 key 是否存在 */
42
+ async has(key: string): Promise<boolean> {
43
+ return (await this._driver()).has(this.prefix + key);
44
+ }
45
+
46
+ /** 删除缓存 */
47
+ async delete(key: string): Promise<boolean> {
48
+ return (await this._driver()).delete(this.prefix + key);
49
+ }
50
+
51
+ /** 清空所有缓存 */
52
+ async clear(): Promise<void> {
53
+ await (await this._driver()).clear();
54
+ }
55
+
56
+ /**
57
+ * 获取或设置:key 存在则返回缓存值,不存在则调用 factory 并缓存结果。
58
+ *
59
+ * @example
60
+ * const config = await cache.remember('app:config', 3600, async () => {
61
+ * return await loadConfigFromDB();
62
+ * });
63
+ */
64
+ async remember<T>(key: string, ttl: number, factory: () => T | Promise<T>): Promise<T> {
65
+ const cached = await this.get<T>(key);
66
+ if (cached !== null) return cached;
67
+ const value = await factory();
68
+ await this.set(key, value, ttl);
69
+ return value;
70
+ }
71
+
72
+ /** 关闭驱动连接 */
73
+ async close(): Promise<void> {
74
+ if (this.driver) {
75
+ await this.driver.close?.();
76
+ }
77
+ }
78
+
79
+ /** 获取底层驱动实例(高级用法,异步) */
80
+ async getDriver(): Promise<CacheDriver> {
81
+ return this._driver();
82
+ }
83
+
84
+ /** 异步懒初始化驱动 */
85
+ private async _driver(): Promise<CacheDriver> {
86
+ if (!this.driver) {
87
+ this.driver = await this._createDriver();
88
+ }
89
+ return this.driver;
90
+ }
91
+
92
+ /** 创建驱动 */
93
+ private async _createDriver(): Promise<CacheDriver> {
94
+ const driver = this.config.driver ?? 'memory';
95
+ switch (driver) {
96
+ case 'memory':
97
+ return new MemoryDriver();
98
+ case 'file':
99
+ return new FileDriver(this.config.file?.path);
100
+ case 'redis':
101
+ return RedisDriver.create(this.config.redis ?? {});
102
+ default:
103
+ throw new Error(`[tyno] Unknown cache driver: "${driver}"`);
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,24 @@
1
+ /** 缓存驱动接口 */
2
+ export interface CacheDriver {
3
+ get(key: string): Promise<string | null>;
4
+ set(key: string, value: string, ttl?: number): Promise<void>;
5
+ delete(key: string): Promise<boolean>;
6
+ has(key: string): Promise<boolean>;
7
+ clear(): Promise<void>;
8
+ /** 关闭驱动连接(如 Redis) */
9
+ close?(): Promise<void>;
10
+ }
11
+
12
+ /** 缓存配置 */
13
+ export interface CacheConfig {
14
+ /** 驱动类型,默认 'memory' */
15
+ driver?: 'memory' | 'file' | 'redis';
16
+ /** key 前缀,默认 'tyno:' */
17
+ prefix?: string;
18
+ /** 默认 TTL(秒),默认 3600 */
19
+ ttl?: number;
20
+ /** 文件驱动配置 */
21
+ file?: { path?: string };
22
+ /** Redis 驱动配置 */
23
+ redis?: { host?: string; port?: number; password?: string; db?: number };
24
+ }
@@ -0,0 +1,64 @@
1
+ import { requestStore } from './context.ts';
2
+ import type { CacheManager } from './cache/index.ts';
3
+
4
+ const CACHE_KEY = '_tyno_cache';
5
+
6
+ /**
7
+ * 全局缓存门面。
8
+ *
9
+ * 在任意中间件或深层业务函数中直接导入,无需通过 app.cache() 访问。
10
+ * 底层基于 AsyncLocalStorage,在请求生命周期内可从任意深度访问。
11
+ *
12
+ * @example
13
+ * import { cache } from 'tyno';
14
+ *
15
+ * app.use(async (req, next) => {
16
+ * await cache.set('user:1', { name: 'Alice' }, 60);
17
+ * const user = await cache.get('user:1');
18
+ * return Response.json(user);
19
+ * });
20
+ */
21
+ export const cache = {
22
+ /** 获取缓存值,不存在返回 null */
23
+ async get<T = unknown>(key: string): Promise<T | null> {
24
+ return (await _getCacheManager()).get<T>(key);
25
+ },
26
+
27
+ /** 设置缓存值,ttl 单位秒 */
28
+ async set(key: string, value: unknown, ttl?: number): Promise<void> {
29
+ await (await _getCacheManager()).set(key, value, ttl);
30
+ },
31
+
32
+ /** 检查 key 是否存在 */
33
+ async has(key: string): Promise<boolean> {
34
+ return (await _getCacheManager()).has(key);
35
+ },
36
+
37
+ /** 删除缓存 */
38
+ async delete(key: string): Promise<boolean> {
39
+ return (await _getCacheManager()).delete(key);
40
+ },
41
+
42
+ /** 清空所有缓存 */
43
+ async clear(): Promise<void> {
44
+ await (await _getCacheManager()).clear();
45
+ },
46
+
47
+ /** 获取或设置:不存在时调用 factory 并缓存 */
48
+ async remember<T>(key: string, ttl: number, factory: () => T | Promise<T>): Promise<T> {
49
+ return (await _getCacheManager()).remember<T>(key, ttl, factory);
50
+ }
51
+ };
52
+
53
+ /** @internal 从请求上下文获取 CacheManager */
54
+ async function _getCacheManager(): Promise<CacheManager> {
55
+ const req = requestStore.getStore();
56
+ if (!req) {
57
+ throw new Error('No active request context. Use cache facade only inside middleware or the request lifecycle.');
58
+ }
59
+ const mgr = (req as unknown as Record<string, unknown>)[CACHE_KEY] as CacheManager | undefined;
60
+ if (!mgr) {
61
+ throw new Error('Cache not configured. Add cache config to new Tyno({ cache: { ... } }).');
62
+ }
63
+ return mgr;
64
+ }
package/src/compose.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { Response } from './response.ts';
2
+ import { IS_ERROR_MIDDLEWARE } from './types.ts';
3
+ import type { AnyMiddleware, TynoRequest, NextFunction } from './types.ts';
4
+
5
+ /**
6
+ * 将任意中间件返回值规范化为 Response 实例
7
+ */
8
+ export function normalizeResponse(res: unknown): Response {
9
+ if (res instanceof Response) return res;
10
+
11
+ if (typeof res === 'string' || Buffer.isBuffer(res)) {
12
+ return Response.text(res);
13
+ }
14
+
15
+ if (typeof res === 'object' && res !== null) {
16
+ if (typeof (res as Record<string, unknown>)[Symbol.asyncIterator as unknown as string] === 'function') {
17
+ return new Response(200, {
18
+ 'Content-Type': 'text/event-stream',
19
+ 'Cache-Control': 'no-cache',
20
+ 'Connection': 'keep-alive'
21
+ }, res as AsyncIterable<unknown>);
22
+ }
23
+ if (typeof (res as Record<string, unknown>).pipe === 'function') {
24
+ return new Response(200, {}, res as NodeJS.ReadableStream);
25
+ }
26
+ return Response.json(res);
27
+ }
28
+
29
+ if (res == null) {
30
+ return Response.empty(204);
31
+ }
32
+
33
+ return Response.text(String(res));
34
+ }
35
+
36
+ /**
37
+ * 判断中间件是否为错误中间件。
38
+ * 优先级:Symbol 标记 > fn.length 检测
39
+ */
40
+ function isErrorMiddleware(fn: AnyMiddleware): boolean {
41
+ return (fn as unknown as Record<string | symbol, unknown>)[IS_ERROR_MIDDLEWARE] === true || fn.length >= 3;
42
+ }
43
+
44
+ /**
45
+ * 显式标记一个函数为错误中间件。
46
+ * 适用于 fn.length < 3 但仍需作为错误中间件使用的场景(如经过包装的函数)。
47
+ */
48
+ export function asErrorMiddleware(fn: AnyMiddleware): AnyMiddleware {
49
+ (fn as unknown as Record<string | symbol, unknown>)[IS_ERROR_MIDDLEWARE] = true;
50
+ return fn;
51
+ }
52
+
53
+ /** 错误中间件处理函数签名 */
54
+ type ErrorHandlerFn = (err: unknown, req: TynoRequest, next: NextFunction) => unknown;
55
+
56
+ /**
57
+ * 中间件组合函数,实现洋葱模型。
58
+ *
59
+ * 中间件签名约定:
60
+ * - 普通中间件:async (req, next?) => unknown (无 Symbol 标记,fn.length <= 2)
61
+ * - 错误中间件:async (err, req, next) => unknown (有 IS_ERROR_MIDDLEWARE 标记或 fn.length >= 3)
62
+ */
63
+ export function compose(req: TynoRequest, middlewares: AnyMiddleware[]): Promise<Response> {
64
+ let index = -1;
65
+
66
+ const dispatch = (i: number): Promise<Response> => {
67
+ if (i <= index) return Promise.reject(new Error('next() called multiple times'));
68
+ index = i;
69
+
70
+ if (i === middlewares.length) {
71
+ return Promise.resolve(new Response(404, {}, 'Not Found'));
72
+ }
73
+
74
+ const fn = middlewares[i];
75
+
76
+ // 错误中间件在正常流程中跳过
77
+ if (isErrorMiddleware(fn)) {
78
+ return dispatch(i + 1);
79
+ }
80
+
81
+ try {
82
+ const next: NextFunction = (err?: unknown): Promise<Response> => {
83
+ if (err !== undefined && err !== null) return Promise.reject(err);
84
+ return dispatch(i + 1);
85
+ };
86
+
87
+ let result: unknown;
88
+ if (fn.length > 1) {
89
+ result = (fn as (req: TynoRequest, next: NextFunction) => unknown)(req, next);
90
+ } else {
91
+ result = (fn as (req: TynoRequest) => unknown)(req);
92
+ }
93
+
94
+ return Promise.resolve(result).then((res: unknown) => normalizeResponse(res));
95
+ } catch (err) {
96
+ return Promise.reject(err);
97
+ }
98
+ };
99
+
100
+ return dispatch(0).catch((err: unknown) => runErrorHandlers(req, middlewares, err));
101
+ }
102
+
103
+ /**
104
+ * 依次执行所有错误中间件,直到某个中间件消费错误并返回响应。
105
+ * next() 真正串联下一个错误处理器,返回有意义的响应。
106
+ */
107
+ async function runErrorHandlers(req: TynoRequest, middlewares: AnyMiddleware[], err: unknown): Promise<Response> {
108
+ const errorHandlers = middlewares.filter(fn => isErrorMiddleware(fn));
109
+
110
+ let handlerIndex = 0;
111
+
112
+ const runNext = (currentErr: unknown): Promise<Response> => {
113
+ if (handlerIndex >= errorHandlers.length) {
114
+ return Promise.reject(currentErr); // 无处理器消费,向上抛出
115
+ }
116
+
117
+ const handler = errorHandlers[handlerIndex++];
118
+ let nextCalled = false;
119
+ let nextChain: Promise<Response> | null = null;
120
+
121
+ const next: NextFunction = (e?: unknown): Promise<Response> => {
122
+ nextCalled = true;
123
+ const errToForward = (e !== undefined && e !== null) ? e : currentErr;
124
+ nextChain = runNext(errToForward); // 真正串联下一个错误处理器
125
+ return nextChain;
126
+ };
127
+
128
+ return Promise.resolve()
129
+ .then(() => (handler as ErrorHandlerFn)(currentErr, req, next))
130
+ .then((result: unknown) => {
131
+ // next() 被调用 → 使用错误链的结果
132
+ // next() 未调用 → 使用 handler 自身的返回值
133
+ if (nextCalled && nextChain) return nextChain;
134
+ return normalizeResponse(result);
135
+ });
136
+ };
137
+
138
+ return runNext(err);
139
+ }
package/src/context.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { TynoRequest } from './types.ts';
3
+
4
+ /** 请求上下文存储(AsyncLocalStorage),供所有门面共享 */
5
+ export const requestStore = new AsyncLocalStorage<TynoRequest>();
@@ -0,0 +1,37 @@
1
+ export interface AppErrorOptions {
2
+ status?: number;
3
+ expose?: boolean;
4
+ code?: string | number | null;
5
+ cause?: Error | null;
6
+ [key: string]: any;
7
+ }
8
+
9
+ export class AppError extends Error {
10
+ status: number;
11
+ expose: boolean;
12
+ code: string | number | null;
13
+ cause: Error | null;
14
+
15
+ constructor(message: string, options: AppErrorOptions = {}) {
16
+ super(message);
17
+ this.name = 'AppError';
18
+ this.status = options.status ?? 500;
19
+ this.expose = options.expose ?? false;
20
+ this.code = options.code ?? null;
21
+ this.cause = options.cause ?? null;
22
+ for (const k in options) {
23
+ if (!['status', 'expose', 'code', 'cause'].includes(k)) {
24
+ (this as any)[k] = options[k];
25
+ }
26
+ }
27
+ }
28
+
29
+ toJSON({ reveal = false }: { reveal?: boolean } = {}): Record<string, any> {
30
+ const base: Record<string, any> = { error: this.message, status: this.status };
31
+ if (this.code != null) base.code = this.code;
32
+ if (reveal || this.expose) {
33
+ if (this.cause?.message) base.cause = this.cause.message;
34
+ }
35
+ return base;
36
+ }
37
+ }
@@ -0,0 +1,34 @@
1
+ import { AppError } from './app-error.ts';
2
+ import type { AppErrorOptions } from './app-error.ts';
3
+
4
+ const STATUS_TEXT: Record<number, string> = {
5
+ 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found',
6
+ 405: 'Method Not Allowed', 409: 'Conflict', 413: 'Payload Too Large',
7
+ 415: 'Unsupported Media Type', 422: 'Unprocessable Entity', 429: 'Too Many Requests',
8
+ 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway',
9
+ 503: 'Service Unavailable', 504: 'Gateway Timeout'
10
+ };
11
+
12
+ export class HttpError extends AppError {
13
+ constructor(status: number, message?: string, props: AppErrorOptions = {}) {
14
+ super(message || STATUS_TEXT[status] || 'Error', {
15
+ status,
16
+ expose: props.expose ?? (status >= 400 && status < 500),
17
+ ...props
18
+ });
19
+ this.name = 'HttpError';
20
+ }
21
+ }
22
+
23
+ export function createError(status: number, message?: string, props?: AppErrorOptions): HttpError {
24
+ return new HttpError(status, message, props);
25
+ }
26
+
27
+ export class BadRequest extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(400, msg, props); } }
28
+ export class Unauthorized extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(401, msg, props); } }
29
+ export class Forbidden extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(403, msg, props); } }
30
+ export class NotFound extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(404, msg, props); } }
31
+ export class Conflict extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(409, msg, props); } }
32
+ export class PayloadTooLarge extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(413, msg, props); } }
33
+ export class TooManyRequests extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(429, msg, props); } }
34
+ export class InternalServerError extends HttpError { constructor(msg?: string, props?: AppErrorOptions) { super(500, msg, props); } }
@@ -0,0 +1,4 @@
1
+ export { AppError } from './app-error.ts';
2
+ export type { AppErrorOptions } from './app-error.ts';
3
+ export { HttpError, createError, BadRequest, Unauthorized, Forbidden, NotFound, Conflict, PayloadTooLarge, TooManyRequests, InternalServerError } from './http-error.ts';
4
+ export { RuntimeError, isRuntimeError } from './runtime-error.ts';
@@ -0,0 +1,19 @@
1
+ import { AppError } from './app-error.ts';
2
+ import type { AppErrorOptions } from './app-error.ts';
3
+
4
+ export class RuntimeError extends AppError {
5
+ constructor(message: string, options: AppErrorOptions = {}) {
6
+ super(message, {
7
+ status: options.status ?? 500,
8
+ expose: options.expose ?? false,
9
+ code: options.code,
10
+ cause: options.cause,
11
+ ...options
12
+ });
13
+ this.name = 'RuntimeError';
14
+ }
15
+ }
16
+
17
+ export function isRuntimeError(err: any): err is RuntimeError {
18
+ return err instanceof RuntimeError;
19
+ }
@@ -0,0 +1,3 @@
1
+ // 全局门面导出
2
+ export { default as request } from '../request-global.ts';
3
+ export { cache as Cache, cache } from '../cache-facade.ts';