weifuwu 0.54.1 → 0.55.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/README.md +114 -40
- package/dist/db/errors.d.ts +44 -0
- package/dist/db/postgres/connection.d.ts +64 -0
- package/dist/db/postgres/pool.d.ts +76 -0
- package/dist/db/postgres/protocol.d.ts +71 -0
- package/dist/db/postgres/schema.d.ts +19 -0
- package/dist/db/redis/client.d.ts +44 -0
- package/dist/db/redis/connection.d.ts +60 -0
- package/dist/db/redis/pool.d.ts +54 -0
- package/dist/db/redis/resp.d.ts +42 -0
- package/dist/db/redis/subscriber.d.ts +17 -0
- package/dist/index.js +1645 -115
- package/dist/make-executable-schema.d.ts +20 -0
- package/dist/make-executable-schema.test.d.ts +1 -0
- package/dist/postgres/client.d.ts +10 -1
- package/dist/postgres/types.d.ts +23 -10
- package/dist/redis/client.d.ts +6 -0
- package/dist/redis/types.d.ts +13 -6
- package/dist/types.d.ts +3 -5
- package/package.json +25 -10
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install weifuwu
|
|
|
17
17
|
|
|
18
18
|
## 设计理念
|
|
19
19
|
|
|
20
|
-
**零运行时依赖** — 前端无 npm
|
|
20
|
+
**零运行时依赖** — 前端无 npm 运行时依赖(自研 VDOM,不引入 Virtual DOM 库、rxjs、immer 等)。后端仅依赖 `graphql` + `ws`(语言/协议本身)——**数据库客户端(PostgreSQL/Redis 协议)、GraphQL schema 工具全部自研**。esbuild 编译 TSX 的结果即可直接运行。
|
|
21
21
|
|
|
22
22
|
**两阶段组件模型** — 组件 = `(initProps, ctx) => (props) => VNode`。外层函数只执行一次(mount),内层函数每次状态/props 变化时执行(render)。无 class、无 `this`、无 Hook。
|
|
23
23
|
|
|
@@ -25,6 +25,8 @@ npm install weifuwu
|
|
|
25
25
|
|
|
26
26
|
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` 等),Handler/组件从 `ctx` 读取。
|
|
27
27
|
|
|
28
|
+
**自研数据层** — `ctx.sql`(PG v3 协议)与 `ctx.redis`(RESP2 协议)为**自研客户端**:确定性输出、行为可预测、统一错误模型。jsonb 自动解码、TTL 安全 API、schema 写前校验——高频痛点(双重编码/parseRow 样板/`'EX'` 参数顺序)从根上消除。
|
|
29
|
+
|
|
28
30
|
**SSR + 动态编译** — 后端 `ctx.ui.js()` 用 esbuild 实时编译 TSX,开发时改代码即刷即用,零构建步骤。
|
|
29
31
|
|
|
30
32
|
---
|
|
@@ -162,8 +164,8 @@ createApp()
|
|
|
162
164
|
| `weifuwu` | **serve** | HTTP 服务器 | Router |
|
|
163
165
|
| `weifuwu` | **cors** | CORS 跨域中间件 | Router |
|
|
164
166
|
| `weifuwu` | **serveStatic** | 静态文件服务(ETag/304/目录索引) | Router |
|
|
165
|
-
| `weifuwu` | **postgres** | PostgreSQL
|
|
166
|
-
| `weifuwu` | **redis** | Redis
|
|
167
|
+
| `weifuwu` | **postgres** | PostgreSQL 客户端(自研 PG v3 协议)→ `ctx.sql` | Router, DATABASE_URL |
|
|
168
|
+
| `weifuwu` | **redis** | Redis 客户端(自研 RESP2 协议)→ `ctx.redis` | Router, REDIS_URL |
|
|
167
169
|
| `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
|
|
168
170
|
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
169
171
|
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
@@ -461,82 +463,152 @@ app.get('/assets/*', serveStatic('./assets', {
|
|
|
461
463
|
|
|
462
464
|
---
|
|
463
465
|
|
|
464
|
-
## postgres — PostgreSQL
|
|
466
|
+
## postgres — PostgreSQL 客户端(自研)
|
|
467
|
+
|
|
468
|
+
> **自研 PG v3 协议**(零第三方依赖)——支持 SCRAM-SHA-256 认证、扩展查询(参数化)、类型映射、事务、连接池、schema 写前校验。
|
|
465
469
|
|
|
466
470
|
```ts
|
|
467
|
-
import { postgres
|
|
471
|
+
import { postgres } from 'weifuwu'
|
|
468
472
|
|
|
469
|
-
// 注入 ctx.sql
|
|
473
|
+
// 注入 ctx.sql(懒连接池)
|
|
470
474
|
app.use(postgres())
|
|
471
475
|
|
|
472
|
-
//
|
|
476
|
+
// ① tagged template —— 插值自动参数化(防注入)
|
|
473
477
|
app.get('/users', async (req, ctx) => {
|
|
474
|
-
const users = await ctx.sql`SELECT * FROM users WHERE
|
|
478
|
+
const users = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
475
479
|
return Response.json(users)
|
|
476
480
|
})
|
|
477
481
|
|
|
478
|
-
//
|
|
482
|
+
// ② jsonb 对象直传——自动序列化,不再有双重编码/parseRow 样板
|
|
483
|
+
app.post('/decks', async (req, ctx) => {
|
|
484
|
+
const deck = await req.json()
|
|
485
|
+
await ctx.sql`INSERT INTO decks (title, deck_json) VALUES (${deck.title}, ${deck})`
|
|
486
|
+
// 读回来自动是对象:rows[0].deck_json === { slides: [...] }(不是字符串)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// ③ 事务(postgres.js 兼容 begin)
|
|
479
490
|
app.post('/transfer', async (req, ctx) => {
|
|
480
|
-
|
|
491
|
+
await ctx.sql.begin(async sql => {
|
|
481
492
|
await sql`UPDATE accounts SET balance = balance - 100 WHERE id = 1`
|
|
482
493
|
await sql`UPDATE accounts SET balance = balance + 100 WHERE id = 2`
|
|
483
494
|
})
|
|
484
|
-
return Response.json({ ok: true })
|
|
485
495
|
})
|
|
486
496
|
```
|
|
487
497
|
|
|
488
|
-
|
|
489
|
-
|------|------|--------|------|
|
|
490
|
-
| `url` | `string` | `DATABASE_URL` 环境变量 | 连接字符串 |
|
|
491
|
-
| `max` | `number` | `10` | 连接池大小 |
|
|
492
|
-
| `idleTimeout` | `number` | `30` | 空闲连接超时(秒)|
|
|
493
|
-
| `maxLifetime` | `number` | `3600` | 连接最大生存时间(秒)|
|
|
498
|
+
### 类型映射(自动)
|
|
494
499
|
|
|
495
|
-
|
|
|
496
|
-
|
|
497
|
-
|
|
|
498
|
-
|
|
|
500
|
+
| 数据库类型 | 返回 JS 类型 |
|
|
501
|
+
|-----------|-------------|
|
|
502
|
+
| json / jsonb | `object`(自动 JSON.parse) |
|
|
503
|
+
| int2 / int4 / int8(安全范围内) | `number` |
|
|
504
|
+
| **int8(超出安全范围)** | **`string`**(防静默丢精度,金额/ID 关键) |
|
|
505
|
+
| float / numeric | `number` |
|
|
506
|
+
| boolean | `boolean` |
|
|
507
|
+
| text / varchar / uuid / date | `string` |
|
|
508
|
+
| NULL | `null` |
|
|
509
|
+
|
|
510
|
+
### 类型层(查询泛型 + schema 写前校验)
|
|
499
511
|
|
|
500
512
|
```ts
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
513
|
+
// ① 查询结果泛型(编译期类型,无需手写 interface + 断言)
|
|
514
|
+
interface Deck { id: number; title: string; deck_json: { slides: unknown[] } }
|
|
515
|
+
const decks = await ctx.sql.query<Deck>('SELECT id, title, deck_json FROM decks')
|
|
516
|
+
|
|
517
|
+
// ② schema 注册 → insert 写前校验(脏数据源头拦截)
|
|
518
|
+
ctx.sql.register('decks', {
|
|
519
|
+
title: { type: 'text', required: true },
|
|
520
|
+
status: { type: 'enum', values: ['outline', 'ready'] },
|
|
521
|
+
deck_json: { type: 'jsonb' },
|
|
522
|
+
})
|
|
523
|
+
await ctx.sql.insert('decks', { title: 'x', status: 'INVALID' }) // → ValidationError
|
|
505
524
|
```
|
|
506
525
|
|
|
526
|
+
### 方法面
|
|
527
|
+
|
|
528
|
+
| 方法 | 说明 |
|
|
529
|
+
|------|------|
|
|
530
|
+
| `ctx.sql\`...\`` | tagged template → 参数化查询(插值=参数,表名需硬编码) |
|
|
531
|
+
| `ctx.sql.query<T>(sql, params?)` | 参数化查询 + 泛型 |
|
|
532
|
+
| `ctx.sql.unsafe(sql, params?)` | 原生 SQL(DDL / 动态表名) |
|
|
533
|
+
| `ctx.sql.begin(fn)` | 事务(回调收到 tagged template sql) |
|
|
534
|
+
| `ctx.sql.transaction(fn)` | 事务(回调收到 `{ query }`) |
|
|
535
|
+
| `ctx.sql.register(table, schema)` | 注册表结构(写前校验) |
|
|
536
|
+
| `ctx.sql.insert(table, row)` | schema 校验 + 参数化插入 |
|
|
537
|
+
| `ctx.sql.close()` | 关闭连接池 |
|
|
538
|
+
|
|
539
|
+
### 选项
|
|
540
|
+
|
|
541
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
542
|
+
|------|------|--------|------|
|
|
543
|
+
| `poolSize` | `number` | `10` | 连接池大小 |
|
|
544
|
+
| `acquireTimeoutMs` | `number` | `30000` | 池全忙时 acquire 超时(防饿死,0=无限) |
|
|
545
|
+
| `statementTimeoutMs` | `number` | `0` | 语句超时(慢查询保护,0=禁用) |
|
|
546
|
+
|
|
547
|
+
### 错误映射(自动)
|
|
548
|
+
|
|
549
|
+
`ctx.sql` 查询错误自动映射为 `HttpError`,业务无需手写 catch:
|
|
550
|
+
|
|
551
|
+
| 错误码 | 含义 | HTTP |
|
|
552
|
+
|--------|------|------|
|
|
553
|
+
| `23505` | 唯一约束冲突 | **409** |
|
|
554
|
+
| `23503` / `23502` / `23514` | 外键 / 非空 / 检查约束 | **400** |
|
|
555
|
+
| `22P02` / `22003` | 类型 / 数值错误 | **400** |
|
|
556
|
+
|
|
557
|
+
> 未映射的错误码原样抛出(带 `code` 属性,如 `42P01` 表不存在)。
|
|
558
|
+
|
|
559
|
+
> **裁剪声明**:逻辑复制 / 大对象 / 显式游标 / 二进制 COPY 不支持(明确抛 `ProtocolError('unsupported')`,而非静默出错)。
|
|
560
|
+
|
|
507
561
|
---
|
|
508
562
|
|
|
509
|
-
## redis — Redis
|
|
563
|
+
## redis — Redis 客户端(自研)
|
|
564
|
+
|
|
565
|
+
> **自研 RESP2 协议**(零第三方依赖)——连接/重连/离线队列/管道/Pub-Sub + 消除 ioredis 高频痛点(TTL 参数顺序、JSON 手动序列化、缓存样板)。
|
|
510
566
|
|
|
511
567
|
```ts
|
|
512
568
|
import { redis } from 'weifuwu'
|
|
513
569
|
|
|
514
570
|
app.use(redis())
|
|
515
571
|
|
|
572
|
+
// ① TTL 安全 —— 直接传秒,不会写错
|
|
573
|
+
app.post('/cache/:key', async (req, ctx) => {
|
|
574
|
+
const { value } = await req.json()
|
|
575
|
+
await ctx.redis.set(ctx.params.key, value, 3600) // ioredis 要 set(k, v, 'EX', 3600)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
// ② JSON 零样板 —— 自动序列化(AI 缓存场景)
|
|
516
579
|
app.get('/cache/:key', async (req, ctx) => {
|
|
517
|
-
const val = await ctx.redis.
|
|
518
|
-
|
|
519
|
-
return Response.json({ value: val })
|
|
580
|
+
const val = await ctx.redis.jsonGet(ctx.params.key) // 自动 JSON.parse
|
|
581
|
+
return Response.json(val ?? { miss: true })
|
|
520
582
|
})
|
|
521
583
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
await ctx.redis.
|
|
525
|
-
|
|
584
|
+
// ③ 缓存便捷 —— 读-算-写一体,null 不缓存(防穿透)
|
|
585
|
+
app.get('/llm/:id', async (req, ctx) => {
|
|
586
|
+
const result = await ctx.redis.cache(`llm:${ctx.params.id}`, async () => {
|
|
587
|
+
return await generateLLM(ctx.params.id) // miss 才执行
|
|
588
|
+
}, 3600)
|
|
589
|
+
return Response.json(result)
|
|
526
590
|
})
|
|
527
591
|
```
|
|
528
592
|
|
|
593
|
+
### 方法面
|
|
594
|
+
|
|
595
|
+
| 方法 | 说明 |
|
|
596
|
+
|------|------|
|
|
597
|
+
| `get / set(key, val, ttl?) / del / incr / expire / ttl` | 基础命令(set 直接传秒) |
|
|
598
|
+
| `jsonGet / jsonSet(key, val, ttl?)` | JSON 自动序列化 |
|
|
599
|
+
| `cache(key, fn, ttl)` | 缓存读-算-写(null 不缓存防穿透) |
|
|
600
|
+
| `publish(channel, msg)` | Pub-Sub 发布 |
|
|
601
|
+
| `createSubscriber()` | 独立订阅连接(`subscribe`/`psubscribe` 回调式) |
|
|
602
|
+
| `command(name, ...args)` | 底层命令透传 |
|
|
603
|
+
| `close()` | 关闭连接池 |
|
|
604
|
+
|
|
529
605
|
| 选项 | 类型 | 默认值 | 说明 |
|
|
530
606
|
|------|------|--------|------|
|
|
531
607
|
| `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
|
|
532
|
-
| `
|
|
608
|
+
| `poolSize` | `number` | `5` | 连接池大小 |
|
|
609
|
+
| `keyPrefix` | `string` | `''` | 所有 key 自动加前缀(多应用隔离) |
|
|
533
610
|
|
|
534
|
-
|
|
535
|
-
|----------|------|------|
|
|
536
|
-
| `ctx.redis` | `ioredis.Redis` | ioredis 实例 |
|
|
537
|
-
| `ctx.redis.close()` | `() => Promise<void>` | 关闭连接 |
|
|
538
|
-
|
|
539
|
-
支持全部 ioredis API:`get`, `set`, `del`, `hget`, `hset`, `lpush`, `publish` 等。
|
|
611
|
+
> **裁剪声明**:集群(MOVED 路由)/ 哨兵 / 自动管道不支持(standalone 优先)。
|
|
540
612
|
|
|
541
613
|
---
|
|
542
614
|
|
|
@@ -594,6 +666,8 @@ app.get('/style.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css'))
|
|
|
594
666
|
|
|
595
667
|
## graphql — GraphQL 端点
|
|
596
668
|
|
|
669
|
+
> **SDL + resolvers 绑定为自研实现**(`makeExecutableSchema`,56 行替代 @graphql-tools/schema)——支持根类型与嵌套类型字段 resolver、默认属性查找。
|
|
670
|
+
|
|
597
671
|
```ts
|
|
598
672
|
import type { GraphQLHandler } from 'weifuwu'
|
|
599
673
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db — 统一错误模型
|
|
3
|
+
*
|
|
4
|
+
* 自研 postgres/redis 客户端共享的错误类型体系。
|
|
5
|
+
* 目标:错误语义统一 → 业务层 catch 可编程(按 kind/code 决策)。
|
|
6
|
+
*
|
|
7
|
+
* kind: connection | protocol | timeout | validation | retryable
|
|
8
|
+
* code: PG 错误码(如 23505 唯一冲突)或协议码
|
|
9
|
+
*/
|
|
10
|
+
export type DbErrorKind = 'connection' | 'protocol' | 'timeout' | 'validation' | 'retryable';
|
|
11
|
+
export declare class DbError extends Error {
|
|
12
|
+
readonly kind: DbErrorKind;
|
|
13
|
+
readonly code?: string;
|
|
14
|
+
readonly cause?: unknown;
|
|
15
|
+
constructor(kind: DbErrorKind, message: string, options?: {
|
|
16
|
+
code?: string;
|
|
17
|
+
cause?: unknown;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** 明确不支持的协议能力(诚实裁剪):COPY 二进制、逻辑复制、集群、哨兵等 */
|
|
21
|
+
export declare class ProtocolError extends DbError {
|
|
22
|
+
constructor(feature: string, message?: string);
|
|
23
|
+
}
|
|
24
|
+
/** 连接失败(含重连尝试次数) */
|
|
25
|
+
export declare class ConnectionError extends DbError {
|
|
26
|
+
readonly attempts: number;
|
|
27
|
+
constructor(message: string, attempts?: number, cause?: unknown);
|
|
28
|
+
}
|
|
29
|
+
/** 可重试错误:序列化失败/死锁(PG 40P01/40001)等 */
|
|
30
|
+
export declare class RetryableError extends DbError {
|
|
31
|
+
constructor(message: string, code?: string, cause?: unknown);
|
|
32
|
+
}
|
|
33
|
+
/** 超时:statement_timeout / connect_timeout / idle 等 */
|
|
34
|
+
export declare class TimeoutError extends DbError {
|
|
35
|
+
readonly operation: string;
|
|
36
|
+
readonly ms: number;
|
|
37
|
+
constructor(operation: string, ms: number);
|
|
38
|
+
}
|
|
39
|
+
/** 写前校验失败(schema 注册 → 脏数据拦截) */
|
|
40
|
+
export declare class ValidationError extends DbError {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
43
|
+
/** 判断错误是否可安全重试(事务层用) */
|
|
44
|
+
export declare function isRetryable(err: unknown): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — PostgreSQL 连接(v3 协议)
|
|
3
|
+
*
|
|
4
|
+
* 连接流程: startup → 认证(SCRAM-SHA-256 / md5 / cleartext)→ 参数/就绪
|
|
5
|
+
* 查询流程: Query(Q) → RowDescription/DataRow/CommandComplete → ReadyForQuery
|
|
6
|
+
*
|
|
7
|
+
* 认证支持:
|
|
8
|
+
* R=0 OK
|
|
9
|
+
* R=3 cleartext
|
|
10
|
+
* R=5 md5
|
|
11
|
+
* R=10/11/12 SCRAM-SHA-256(PG15+ 默认)
|
|
12
|
+
*/
|
|
13
|
+
export interface PgConnectionOptions {
|
|
14
|
+
host?: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
user?: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
database?: string;
|
|
19
|
+
/** 连接超时 ms。默认 10_000。 */
|
|
20
|
+
connectTimeoutMs?: number;
|
|
21
|
+
/** 语句超时 ms(慢查询保护,会话级 SET statement_timeout)。默认 0 = 禁用。 */
|
|
22
|
+
statementTimeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface Row {
|
|
25
|
+
[col: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
export declare class PgConnection {
|
|
28
|
+
private opts;
|
|
29
|
+
private timeoutSet;
|
|
30
|
+
private awaitingReady;
|
|
31
|
+
private pendingErrorZ;
|
|
32
|
+
private socket;
|
|
33
|
+
private stream;
|
|
34
|
+
private status;
|
|
35
|
+
private waiters;
|
|
36
|
+
constructor(options?: PgConnectionOptions);
|
|
37
|
+
get connected(): boolean;
|
|
38
|
+
connect(): Promise<void>;
|
|
39
|
+
private onReady;
|
|
40
|
+
private onAuthFail;
|
|
41
|
+
private expectingAuth;
|
|
42
|
+
private authStage;
|
|
43
|
+
private authCtx;
|
|
44
|
+
private prepared;
|
|
45
|
+
private stmtSeq;
|
|
46
|
+
private currentQuery;
|
|
47
|
+
private onData;
|
|
48
|
+
private handle;
|
|
49
|
+
/** SCRAM client-final 消息 */
|
|
50
|
+
private scramFinal;
|
|
51
|
+
private scramServerSignature;
|
|
52
|
+
/** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
|
|
53
|
+
transaction<T>(fn: (tx: {
|
|
54
|
+
query: (sql: string, params?: (string | number | boolean | object | null)[]) => Promise<Row[]>;
|
|
55
|
+
}) => Promise<T>): Promise<T>;
|
|
56
|
+
/** 查询:无参数走简单协议(Q),有参数走扩展查询(Parse/Bind/Execute/Sync) */
|
|
57
|
+
query(sql: string, params?: (string | number | boolean | object | null)[]): Promise<Row[]>;
|
|
58
|
+
private send;
|
|
59
|
+
private notifyIdle;
|
|
60
|
+
/** 等待连接空闲(事务/多语句流程用) */
|
|
61
|
+
waitIdle(): Promise<void>;
|
|
62
|
+
/** 终止连接 */
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — PostgreSQL 连接池(借贷模型)
|
|
3
|
+
*
|
|
4
|
+
* 连接空闲队列 + 等待者队列:
|
|
5
|
+
* query → acquire(空闲连接 or 等待)→ 执行 → release
|
|
6
|
+
* 事务 → acquire 单个连接执行整个 BEGIN→fn→COMMIT/ROLLBACK
|
|
7
|
+
*
|
|
8
|
+
* 解决 PgConnection 单连接串行(一个 currentQuery)的限制:
|
|
9
|
+
* 并发查询路由到不同连接,全忙时排队等待而非 reject。
|
|
10
|
+
*/
|
|
11
|
+
import { type PgConnectionOptions, type Row } from './connection.ts';
|
|
12
|
+
import { type Schema } from './schema.ts';
|
|
13
|
+
export interface PgPoolOptions extends PgConnectionOptions {
|
|
14
|
+
/** 池大小(连接数)。默认 5。 */
|
|
15
|
+
poolSize?: number;
|
|
16
|
+
/** acquire 超时 ms(池全忙时等待上限,防饿死)。默认 30_000。0 = 无限。 */
|
|
17
|
+
acquireTimeoutMs?: number;
|
|
18
|
+
/** 查询观测钩子(慢查询日志/审计) */
|
|
19
|
+
onQuery?: (sql: string, durationMs: number, rowCount: number) => void;
|
|
20
|
+
}
|
|
21
|
+
type QueryParams = (string | number | boolean | object | null)[];
|
|
22
|
+
export declare class PgPool {
|
|
23
|
+
private all;
|
|
24
|
+
private available;
|
|
25
|
+
private waiters;
|
|
26
|
+
private closed;
|
|
27
|
+
private opts;
|
|
28
|
+
private initPromise;
|
|
29
|
+
private schemas;
|
|
30
|
+
/** 懒连接:构造不连接,ensure() 首次初始化(中间件注入场景) */
|
|
31
|
+
constructor(options?: PgPoolOptions);
|
|
32
|
+
static create(options?: PgPoolOptions): Promise<PgPool>;
|
|
33
|
+
private readyPromise;
|
|
34
|
+
private ensure;
|
|
35
|
+
private init;
|
|
36
|
+
/** 获取一个空闲连接(全忙则排队等待) */
|
|
37
|
+
private acquire;
|
|
38
|
+
private release;
|
|
39
|
+
/** 连接可用事件统一入口:优先唤醒等待者,否则回空闲池(release 与 replenish 共用) */
|
|
40
|
+
private dispatchAvailable;
|
|
41
|
+
/** 坏连接剔除后异步重建(池容量保持)——就绪后走统一分发(唤醒 waiter) */
|
|
42
|
+
private replenish;
|
|
43
|
+
query<T = Row>(sql: string, params?: QueryParams): Promise<T[]>;
|
|
44
|
+
/** 注册表结构(元数据闭环:校验/类型推断的起点) */
|
|
45
|
+
register(table: string, schema: Schema): void;
|
|
46
|
+
/** 写前校验 + 参数化插入(schema 驱动,脏数据源头拦截) */
|
|
47
|
+
insert<T = Row>(table: string, row: Record<string, unknown>): Promise<T[]>;
|
|
48
|
+
/**
|
|
49
|
+
* tagged template: sql\`SELECT * FROM t WHERE id = \${id}\`
|
|
50
|
+
* 插值 = 参数(postgres.js 语义,防注入);表名必须硬编码(插值会被当参数)。
|
|
51
|
+
* 对象插值自动 JSON.stringify → jsonb。
|
|
52
|
+
*/
|
|
53
|
+
tag(strings: TemplateStringsArray, ...values: unknown[]): Promise<Row[]>;
|
|
54
|
+
/** postgres.js 兼容事务 API: begin(fn)——fn 收到 tagged template 事务 sql */
|
|
55
|
+
begin<T>(fn: (txSql: TaggedSql) => Promise<T>): Promise<T>;
|
|
56
|
+
/** 片段:可嵌套的 SQL 片段(postgres.js fragment 语义,条件过滤模式) */
|
|
57
|
+
frag(strings: TemplateStringsArray, ...values: unknown[]): SqlFragment;
|
|
58
|
+
/** 原生 SQL(DDL / 动态表名场景);$1 占位符 + 参数数组 */
|
|
59
|
+
unsafe(sql: string, params?: QueryParams): Promise<Row[]>;
|
|
60
|
+
/** 事务:固定在单个连接上执行整个 BEGIN→fn→COMMIT/ROLLBACK */
|
|
61
|
+
transaction<T>(fn: (tx: {
|
|
62
|
+
query: (sql: string, params?: QueryParams) => Promise<Row[]>;
|
|
63
|
+
}) => Promise<T>): Promise<T>;
|
|
64
|
+
close(): Promise<void>;
|
|
65
|
+
get size(): number;
|
|
66
|
+
}
|
|
67
|
+
/** 片段对象:嵌套 SQL 片段(内部含已解析的 sql + params) */
|
|
68
|
+
export interface SqlFragment {
|
|
69
|
+
__fragment: {
|
|
70
|
+
sql: string;
|
|
71
|
+
params: QueryParams;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** tagged template 事务 sql(begin 回调参数) */
|
|
75
|
+
export type TaggedSql = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<Row[]>;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — PostgreSQL v3 协议消息编解码
|
|
3
|
+
*
|
|
4
|
+
* 消息帧: type(1) + length(4, 含自身) + payload
|
|
5
|
+
* StartupMessage 特殊: length(4) + version(4) + 参数(键\0值\0...\0)
|
|
6
|
+
*
|
|
7
|
+
* 响应解析辅助: authCode / parseRowDescription / parseDataRow / readyStatus / parseErrorFields
|
|
8
|
+
*/
|
|
9
|
+
/** 客户端消息类型 */
|
|
10
|
+
export type ClientMessageType = 'Q' | 'P' | 'B' | 'E' | 'S' | 'X' | 'p' | 'd' | 'H' | 'C' | 'D' | 'c' | 'f';
|
|
11
|
+
/** 解析后的消息 */
|
|
12
|
+
export interface Message {
|
|
13
|
+
type: string;
|
|
14
|
+
payload: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/** 编码消息帧: type + length(4) + payload */
|
|
17
|
+
export declare function encodeMessage(type: string, payload: Uint8Array): Uint8Array;
|
|
18
|
+
/** StartupMessage: length(4) + version(4=196608) + 参数键值对 + \0 */
|
|
19
|
+
export declare function startupMessage(params: Record<string, string>): Uint8Array;
|
|
20
|
+
/** 简单查询消息: Q + SQL + \0 终止符 */
|
|
21
|
+
export declare function queryMessage(sql: string): Uint8Array;
|
|
22
|
+
/** Parse: P + statementName\0 + query\0 + paramTypeCount(2) + OIDs */
|
|
23
|
+
export declare function parseMessage(name: string, sql: string, paramTypes?: number[]): Uint8Array;
|
|
24
|
+
/** Bind: B + portal\0 + statement\0 + fmtCount + formats + paramCount + params + resultFmtCount */
|
|
25
|
+
export declare function bindMessage(statement: string, params: (string | Uint8Array | null)[], paramFormats?: number[]): Uint8Array;
|
|
26
|
+
/** Execute: E + portal\0 + maxRows(4) */
|
|
27
|
+
export declare function executeMessage(portal?: string, maxRows?: number): Uint8Array;
|
|
28
|
+
/** Sync / Terminate / PasswordMessage */
|
|
29
|
+
export declare function syncMessage(): Uint8Array;
|
|
30
|
+
/** Flush: 强制服务器处理已缓冲的扩展查询消息(Parse/Bind/Execute 需 Flush 或 Sync 才执行) */
|
|
31
|
+
export declare function flushMessage(): Uint8Array;
|
|
32
|
+
/** Describe: D + 目标类型(S/P) + name\0——服务器返回 ParameterDescription(t) / RowDescription(T) */
|
|
33
|
+
export declare function describeMessage(kind: 'S' | 'P', name?: string): Uint8Array;
|
|
34
|
+
export declare function terminateMessage(): Uint8Array;
|
|
35
|
+
export declare function passwordMessage(password: string): Uint8Array;
|
|
36
|
+
/** 增量消息流解析:零拷贝(buffer + offset 指针),喂入任意分片 */
|
|
37
|
+
export declare class MessageStream {
|
|
38
|
+
private buf;
|
|
39
|
+
private off;
|
|
40
|
+
push(chunk: Uint8Array): Message[];
|
|
41
|
+
/** 追加分片:已消费部分先行压缩(一次拷贝),避免 O(n²) 累积 */
|
|
42
|
+
private append;
|
|
43
|
+
private compact;
|
|
44
|
+
/** 尝试读一条完整消息;不完整返回 null(不消费) */
|
|
45
|
+
private tryRead;
|
|
46
|
+
}
|
|
47
|
+
/** 便捷:解析 buffer 中所有完整消息 */
|
|
48
|
+
export declare function parseMessageStream(data: Uint8Array): Message[];
|
|
49
|
+
/** Authentication (R) 的认证码 */
|
|
50
|
+
export declare function authCode(msg: Message): number;
|
|
51
|
+
export interface ColumnInfo {
|
|
52
|
+
name: string;
|
|
53
|
+
typeOid: number;
|
|
54
|
+
typeLen: number;
|
|
55
|
+
}
|
|
56
|
+
/** RowDescription (T): 列信息 */
|
|
57
|
+
export declare function parseRowDescription(payload: Uint8Array): ColumnInfo[];
|
|
58
|
+
/** DataRow (D): 值列表(null 为 null,其余为文本字节) */
|
|
59
|
+
export declare function parseDataRow(payload: Uint8Array): (string | null)[];
|
|
60
|
+
/** ReadyForQuery (Z) 状态 */
|
|
61
|
+
export declare function readyStatus(payload: Uint8Array): 'idle' | 'tx' | 'error';
|
|
62
|
+
export interface ErrorFields {
|
|
63
|
+
severity?: string;
|
|
64
|
+
code?: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
detail?: string;
|
|
67
|
+
hint?: string;
|
|
68
|
+
position?: string;
|
|
69
|
+
}
|
|
70
|
+
/** ErrorResponse (E) 字段 */
|
|
71
|
+
export declare function parseErrorFields(payload: Uint8Array): ErrorFields;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — Schema 注册与写前校验
|
|
3
|
+
*
|
|
4
|
+
* 注册表结构后,insert 在写入前校验字段类型/必填/枚举——脏数据在源头拦截。
|
|
5
|
+
* 这是元数据闭环的第一环(迁移/类型推断/缓存失效共享 schema 的起点)。
|
|
6
|
+
*/
|
|
7
|
+
export type ColumnType = 'text' | 'int' | 'jsonb' | 'enum';
|
|
8
|
+
export interface ColumnDef {
|
|
9
|
+
type: ColumnType;
|
|
10
|
+
/** 必填(INSERT 时缺失报错) */
|
|
11
|
+
required?: boolean;
|
|
12
|
+
/** enum 类型允许的值 */
|
|
13
|
+
values?: string[];
|
|
14
|
+
}
|
|
15
|
+
export type Schema = Record<string, ColumnDef>;
|
|
16
|
+
/** 校验单值;失败抛 ValidationError */
|
|
17
|
+
export declare function validateValue(def: ColumnDef, value: unknown, column: string): void;
|
|
18
|
+
/** 校验整个 row(必填 + 各列类型) */
|
|
19
|
+
export declare function validateRow(schema: Schema, row: Record<string, unknown>): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/redis — Redis 客户端高层 API
|
|
3
|
+
*
|
|
4
|
+
* 在 RedisConnection 之上提供:
|
|
5
|
+
* - TTL 安全: set(key, val, ttl) 直接生效(无需记 'EX' 前缀顺序)
|
|
6
|
+
* - JSON 存取: jsonGet/jsonSet 自动序列化(AI 缓存场景零样板)
|
|
7
|
+
* - 缓存便捷: cache(key, fn, ttl) 读缓存 → miss 执行 fn → 回填
|
|
8
|
+
*/
|
|
9
|
+
import { type RedisConnectionOptions } from './connection.ts';
|
|
10
|
+
import type { RespValue } from './resp.ts';
|
|
11
|
+
export interface RedisClientOptions extends RedisConnectionOptions {
|
|
12
|
+
}
|
|
13
|
+
export declare class RedisClient {
|
|
14
|
+
private conn;
|
|
15
|
+
private constructor();
|
|
16
|
+
/** 建立连接并返回就绪的客户端 */
|
|
17
|
+
static connect(options?: RedisClientOptions): Promise<RedisClient>;
|
|
18
|
+
/** 底层命令透传(RESP 值) */
|
|
19
|
+
command(name: string, ...args: (string | number)[]): Promise<RespValue>;
|
|
20
|
+
get(key: string): Promise<string | null>;
|
|
21
|
+
/**
|
|
22
|
+
* SET。ttl 秒可省略;传入即安全生效(内部转 SET key val EX ttl)。
|
|
23
|
+
*/
|
|
24
|
+
set(key: string, value: string | number, ttl?: number): Promise<'OK'>;
|
|
25
|
+
/** DEL 多 key,返回删除数量 */
|
|
26
|
+
del(...keys: string[]): Promise<number>;
|
|
27
|
+
/** INCR,返回自增后的值 */
|
|
28
|
+
incr(key: string): Promise<number>;
|
|
29
|
+
/** EXPIRE,返回 1=设置成功 0=key 不存在 */
|
|
30
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
31
|
+
/** TTL 剩余秒数;-1=无 TTL -2=key 不存在 */
|
|
32
|
+
ttl(key: string): Promise<number>;
|
|
33
|
+
/** 读取并 JSON.parse;key 不存在返回 null */
|
|
34
|
+
jsonGet(key: string): Promise<unknown | null>;
|
|
35
|
+
/** JSON.stringify 后写入,可选 TTL */
|
|
36
|
+
jsonSet(key: string, value: unknown, ttl?: number): Promise<'OK'>;
|
|
37
|
+
/**
|
|
38
|
+
* 缓存读-算-写。命中返回缓存值;miss 执行 fn 并回填。
|
|
39
|
+
* fn 返回 null 时不缓存(防穿透)。
|
|
40
|
+
*/
|
|
41
|
+
cache<T>(key: string, fn: () => Promise<T | null>, ttl: number): Promise<T | null>;
|
|
42
|
+
/** 主动关闭连接 */
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/redis — Redis 连接(TCP + RESP2)
|
|
3
|
+
*
|
|
4
|
+
* 连接状态机: idle → connecting → ready → closed
|
|
5
|
+
* └────── reconnect(退避重试)────────┘
|
|
6
|
+
*
|
|
7
|
+
* - 命令响应按发送顺序路由(Redis 单连接严格有序)
|
|
8
|
+
* - 服务器错误响应(-ERR)→ RespError
|
|
9
|
+
* - 断线重连:pending 命令拒绝(可重试),重连后继续服务
|
|
10
|
+
*/
|
|
11
|
+
import { type RespValue } from './resp.ts';
|
|
12
|
+
export interface RedisConnectionOptions {
|
|
13
|
+
host?: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
/** 重连退避初始延迟 ms,指数增长。默认 100。 */
|
|
16
|
+
retryDelayMs?: number;
|
|
17
|
+
/** 最大重连尝试次数。默认 10。0 = 无限。 */
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
/** 未连接时命令是否入队等待(ioredis enableOfflineQueue 语义)。默认 true。 */
|
|
20
|
+
enableOfflineQueue?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class RedisConnection {
|
|
23
|
+
readonly ready = false;
|
|
24
|
+
private opts;
|
|
25
|
+
private socket;
|
|
26
|
+
private parser;
|
|
27
|
+
private pending;
|
|
28
|
+
private offlineQueue;
|
|
29
|
+
private subs;
|
|
30
|
+
private psubs;
|
|
31
|
+
private status;
|
|
32
|
+
private retries;
|
|
33
|
+
private reconnectTimer;
|
|
34
|
+
private connectPromise;
|
|
35
|
+
private closed;
|
|
36
|
+
private connectedOnce;
|
|
37
|
+
constructor(options?: RedisConnectionOptions);
|
|
38
|
+
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
39
|
+
connect(): Promise<void>;
|
|
40
|
+
private onceReady;
|
|
41
|
+
private onceFailed;
|
|
42
|
+
private openSocket;
|
|
43
|
+
private handleDisconnect;
|
|
44
|
+
private onData;
|
|
45
|
+
/** 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。 */
|
|
46
|
+
command(name: string, ...args: (string | number)[]): Promise<RespValue>;
|
|
47
|
+
private sendNow;
|
|
48
|
+
private flushOffline;
|
|
49
|
+
/** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
|
|
50
|
+
batch(payload: Uint8Array, count: number): Promise<RespValue[]>;
|
|
51
|
+
/** 订阅频道:回调式(channel, message) */
|
|
52
|
+
subscribe(channel: string, fn: (channel: string, message: string) => void): Promise<void>;
|
|
53
|
+
/** 订阅模式:回调式(channel, message) */
|
|
54
|
+
psubscribe(pattern: string, fn: (channel: string, message: string) => void): Promise<void>;
|
|
55
|
+
/** 旁路分发订阅消息(RESP 数组路由到回调) */
|
|
56
|
+
private dispatchSubscribe;
|
|
57
|
+
/** 主动关闭——不再重连 */
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
get connected(): boolean;
|
|
60
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/redis — Redis 连接池
|
|
3
|
+
*
|
|
4
|
+
* 固定大小连接池 + round-robin 分发(无状态、无跨连接串扰)。
|
|
5
|
+
* 暴露与 RedisClient 相同的方法签名,上层可无缝替换。
|
|
6
|
+
*/
|
|
7
|
+
import { type RedisClientOptions } from './client.ts';
|
|
8
|
+
import { RedisSubscriber } from './subscriber.ts';
|
|
9
|
+
import type { RespValue } from './resp.ts';
|
|
10
|
+
export interface RedisPoolOptions extends RedisClientOptions {
|
|
11
|
+
/** 池大小(连接数)。默认 5。 */
|
|
12
|
+
poolSize?: number;
|
|
13
|
+
/** 所有 key 自动加前缀(多应用共享 Redis 时隔离命名空间) */
|
|
14
|
+
keyPrefix?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class RedisPool {
|
|
17
|
+
private clients;
|
|
18
|
+
private rr;
|
|
19
|
+
private closed;
|
|
20
|
+
private keyPrefix;
|
|
21
|
+
private opts;
|
|
22
|
+
private initPromise;
|
|
23
|
+
/** 懒连接模式:构造不连接,首命令时初始化(中间件注入场景) */
|
|
24
|
+
constructor(opts?: RedisPoolOptions);
|
|
25
|
+
/** 建立池:创建 poolSize 个连接(eager) */
|
|
26
|
+
static create(options?: RedisPoolOptions): Promise<RedisPool>;
|
|
27
|
+
/** 懒连接:首次使用时初始化连接(中间件场景:构造即注入,首命令才连) */
|
|
28
|
+
private readyPromise;
|
|
29
|
+
private ensure;
|
|
30
|
+
private init;
|
|
31
|
+
/** 应用 key 前缀(command 透传不加) */
|
|
32
|
+
private k;
|
|
33
|
+
/** round-robin 选择连接 */
|
|
34
|
+
private next;
|
|
35
|
+
command(name: string, ...args: (string | number)[]): Promise<RespValue>;
|
|
36
|
+
get(key: string): Promise<string | null>;
|
|
37
|
+
set(key: string, value: string | number, ttl?: number): Promise<'OK'>;
|
|
38
|
+
del(...keys: string[]): Promise<number>;
|
|
39
|
+
incr(key: string): Promise<number>;
|
|
40
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
41
|
+
ttl(key: string): Promise<number>;
|
|
42
|
+
jsonGet(key: string): Promise<unknown | null>;
|
|
43
|
+
jsonSet(key: string, value: unknown, ttl?: number): Promise<'OK'>;
|
|
44
|
+
cache<T>(key: string, fn: () => Promise<T | null>, ttl: number): Promise<T | null>;
|
|
45
|
+
/** PUBLISH 消息到频道,返回收到消息的订阅者数 */
|
|
46
|
+
publish(channel: string, message: string | number): Promise<number>;
|
|
47
|
+
/** 创建独立订阅者连接(Pub/Sub 场景) */
|
|
48
|
+
createSubscriber(): RedisSubscriber;
|
|
49
|
+
/** 清空当前库(测试/重置场景) */
|
|
50
|
+
flushdb(): Promise<'OK'>;
|
|
51
|
+
/** 关闭所有池内连接 */
|
|
52
|
+
close(): Promise<void>;
|
|
53
|
+
get size(): number;
|
|
54
|
+
}
|