weifuwu 0.56.0 → 0.56.1
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 +1 -1
- package/dist/db/postgres/connection.d.ts +5 -1
- package/dist/db/postgres/protocol.d.ts +4 -1
- package/dist/db/redis/client.d.ts +7 -2
- package/dist/db/redis/connection.d.ts +11 -2
- package/dist/db/redis/pool.d.ts +2 -0
- package/dist/db/redis/resp.d.ts +12 -5
- package/dist/index.js +229 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -679,7 +679,7 @@ app.use(db)
|
|
|
679
679
|
|
|
680
680
|
## redis — Redis 客户端(自研)
|
|
681
681
|
|
|
682
|
-
> **自研 RESP2 协议**(零第三方依赖)——连接/重连(断线 pending 拒绝、指数退避)/离线队列/管道/Pub-Sub(订阅断线自动重放)+ 消除 ioredis 高频痛点(TTL 参数顺序、JSON
|
|
682
|
+
> **自研 RESP2 协议**(零第三方依赖)——连接/重连(断线 pending 拒绝、指数退避)/离线队列/管道/Pub-Sub(订阅断线自动重放)+ 消除 ioredis 高频痛点(TTL 参数顺序、JSON 手动序列化、缓存样板)。**二进制安全**:`getBuffer(key)` 原样返回字节(缓存序列化 payload 不损坏)。
|
|
683
683
|
|
|
684
684
|
```ts
|
|
685
685
|
import { redis } from 'weifuwu'
|
|
@@ -39,9 +39,9 @@ export declare class PgConnection {
|
|
|
39
39
|
private onReady;
|
|
40
40
|
private onAuthFail;
|
|
41
41
|
private expectingAuth;
|
|
42
|
-
private authStage;
|
|
43
42
|
private authCtx;
|
|
44
43
|
private prepared;
|
|
44
|
+
private static readonly PREPARED_MAX;
|
|
45
45
|
private stmtSeq;
|
|
46
46
|
private currentQuery;
|
|
47
47
|
private onData;
|
|
@@ -49,6 +49,10 @@ export declare class PgConnection {
|
|
|
49
49
|
/** SCRAM client-final 消息 */
|
|
50
50
|
private scramFinal;
|
|
51
51
|
private scramServerSignature;
|
|
52
|
+
/** LRU 读取:命中移到尾部(最近使用),超限删最旧 */
|
|
53
|
+
private getPrepared;
|
|
54
|
+
/** LRU 写入:刷新位置;超上限淘汰最旧(长运行服务防无限累积) */
|
|
55
|
+
private setPrepared;
|
|
52
56
|
/** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
|
|
53
57
|
transaction<T>(fn: (tx: {
|
|
54
58
|
query: (sql: string, params?: (string | number | boolean | object | null)[]) => Promise<Row[]>;
|
|
@@ -21,7 +21,10 @@ export declare function startupMessage(params: Record<string, string>): Uint8Arr
|
|
|
21
21
|
export declare function queryMessage(sql: string): Uint8Array;
|
|
22
22
|
/** Parse: P + statementName\0 + query\0 + paramTypeCount(2) + OIDs */
|
|
23
23
|
export declare function parseMessage(name: string, sql: string, paramTypes?: number[]): Uint8Array;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Bind: B + portal\0 + statement\0 + fmtCount + formats + paramCount + params + resultFmtCount
|
|
26
|
+
* 两遍法:先算 payload 总长 → 预分配一次写入(buffer + offset 指针,避免 number[] 累积 O(n²))。
|
|
27
|
+
*/
|
|
25
28
|
export declare function bindMessage(statement: string, params: (string | Uint8Array | null)[], paramFormats?: number[]): Uint8Array;
|
|
26
29
|
/** Execute: E + portal\0 + maxRows(4) */
|
|
27
30
|
export declare function executeMessage(portal?: string, maxRows?: number): Uint8Array;
|
|
@@ -15,9 +15,14 @@ export declare class RedisClient {
|
|
|
15
15
|
private constructor();
|
|
16
16
|
/** 建立连接并返回就绪的客户端 */
|
|
17
17
|
static connect(options?: RedisClientOptions): Promise<RedisClient>;
|
|
18
|
-
/** 底层命令透传(RESP
|
|
19
|
-
command(name: string, ...args: (string | number)[]): Promise<RespValue>;
|
|
18
|
+
/** 底层命令透传(RESP 值);Buffer 参数字节原样发送 */
|
|
19
|
+
command(name: string, ...args: (string | number | Buffer)[]): Promise<RespValue>;
|
|
20
20
|
get(key: string): Promise<string | null>;
|
|
21
|
+
/**
|
|
22
|
+
* 二进制安全读取:返回原始字节(Uint8Array),不经过字符串解码。
|
|
23
|
+
* 用于缓存二进制 payload(序列化字节、图片等)。key 不存在返回 null。
|
|
24
|
+
*/
|
|
25
|
+
getBuffer(key: string): Promise<Uint8Array | null>;
|
|
21
26
|
/**
|
|
22
27
|
* SET。ttl 秒可省略;传入即安全生效(内部转 SET key val EX ttl)。
|
|
23
28
|
*/
|
|
@@ -18,6 +18,8 @@ export interface RedisConnectionOptions {
|
|
|
18
18
|
maxRetries?: number;
|
|
19
19
|
/** 未连接时命令是否入队等待(ioredis enableOfflineQueue 语义)。默认 true。 */
|
|
20
20
|
enableOfflineQueue?: boolean;
|
|
21
|
+
/** 离线队列上限。默认 5000。超限命令立即 reject(防断线期间无限累积)。 */
|
|
22
|
+
maxOfflineQueue?: number;
|
|
21
23
|
}
|
|
22
24
|
export declare class RedisConnection {
|
|
23
25
|
readonly ready = false;
|
|
@@ -25,6 +27,8 @@ export declare class RedisConnection {
|
|
|
25
27
|
private socket;
|
|
26
28
|
private parser;
|
|
27
29
|
private pending;
|
|
30
|
+
/** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
|
|
31
|
+
private pendingHead;
|
|
28
32
|
private offlineQueue;
|
|
29
33
|
private subs;
|
|
30
34
|
private psubs;
|
|
@@ -42,8 +46,13 @@ export declare class RedisConnection {
|
|
|
42
46
|
private openSocket;
|
|
43
47
|
private handleDisconnect;
|
|
44
48
|
private onData;
|
|
45
|
-
/**
|
|
46
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
|
|
51
|
+
* opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
|
|
52
|
+
*/
|
|
53
|
+
command(name: string, ...args: (string | number | Buffer | {
|
|
54
|
+
asBuffer?: boolean;
|
|
55
|
+
})[]): Promise<RespValue>;
|
|
47
56
|
private sendNow;
|
|
48
57
|
private flushOffline;
|
|
49
58
|
/** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
|
package/dist/db/redis/pool.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export declare class RedisPool {
|
|
|
34
34
|
private next;
|
|
35
35
|
command(name: string, ...args: (string | number)[]): Promise<RespValue>;
|
|
36
36
|
get(key: string): Promise<string | null>;
|
|
37
|
+
/** 二进制安全读取(原始字节,不解码) */
|
|
38
|
+
getBuffer(key: string): Promise<Uint8Array | null>;
|
|
37
39
|
set(key: string, value: string | number, ttl?: number): Promise<'OK'>;
|
|
38
40
|
del(...keys: string[]): Promise<number>;
|
|
39
41
|
incr(key: string): Promise<number>;
|
package/dist/db/redis/resp.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 解码为增量式:连接层可喂入任意分片,累积到完整消息后取回。
|
|
8
8
|
*/
|
|
9
9
|
import { DbError } from '../errors.ts';
|
|
10
|
-
export type RespValue = string | number | null | RespError | RespValue[];
|
|
10
|
+
export type RespValue = string | number | null | RespError | RespValue[] | Uint8Array;
|
|
11
11
|
/** 服务器错误响应(-ERR ...) */
|
|
12
12
|
export declare class RespError extends DbError {
|
|
13
13
|
constructor(message: string);
|
|
@@ -16,9 +16,11 @@ export declare class RespError extends DbError {
|
|
|
16
16
|
export declare class IncompleteError extends Error {
|
|
17
17
|
constructor();
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
19
|
+
/** 递归解码字节值(bulk → string)。decodeBytes=false 时字节原样保留(二进制安全)。 */
|
|
20
|
+
export declare function decodeValue(v: RespValue, decodeBytes: boolean): RespValue;
|
|
21
|
+
/** 编码命令为 RESP 数组字节。Buffer 参数字节原样写入(二进制安全,零损坏)。 */
|
|
20
22
|
export declare function encodeCommand(args: (string | number | Buffer)[]): Uint8Array;
|
|
21
|
-
/** 从完整 buffer 解析单个 RESP
|
|
23
|
+
/** 从完整 buffer 解析单个 RESP 值(非增量——单消息场景,解码为 string 语义) */
|
|
22
24
|
export declare function parseReply(data: Uint8Array): RespValue;
|
|
23
25
|
export declare class RespParser {
|
|
24
26
|
private buf;
|
|
@@ -35,8 +37,13 @@ export declare class RespParser {
|
|
|
35
37
|
/** 全部消费后压缩(释放底层) */
|
|
36
38
|
private compact;
|
|
37
39
|
private parseValue;
|
|
40
|
+
/**
|
|
41
|
+
* 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
|
|
42
|
+
* 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
|
|
43
|
+
*/
|
|
44
|
+
private readInt;
|
|
38
45
|
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
39
46
|
private readLine;
|
|
40
|
-
/** 读 len 字节的 bulk 内容 + \r\n */
|
|
41
|
-
private
|
|
47
|
+
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
48
|
+
private readBulkBytes;
|
|
42
49
|
}
|
package/dist/index.js
CHANGED
|
@@ -1567,23 +1567,61 @@ function parseMessage(name, sql, paramTypes = []) {
|
|
|
1567
1567
|
return encodeMessage("P", concat(body, types));
|
|
1568
1568
|
}
|
|
1569
1569
|
function bindMessage(statement, params, paramFormats = []) {
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1570
|
+
const stmtBytes = utf8(statement);
|
|
1571
|
+
const lens = new Array(params.length);
|
|
1572
|
+
let total = 1 + stmtBytes.length + 1;
|
|
1573
|
+
total += 2 + paramFormats.length * 2;
|
|
1574
|
+
total += 2;
|
|
1575
|
+
for (let i = 0; i < params.length; i++) {
|
|
1576
|
+
const p = params[i];
|
|
1577
|
+
total += 4;
|
|
1578
|
+
if (p !== null) {
|
|
1579
|
+
const len = typeof p === "string" ? utf8Len(p) : p.length;
|
|
1580
|
+
lens[i] = len;
|
|
1581
|
+
total += len;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
total += 2;
|
|
1585
|
+
const body = new Uint8Array(total);
|
|
1586
|
+
let off = 0;
|
|
1587
|
+
body[off++] = 0;
|
|
1588
|
+
body.set(stmtBytes, off);
|
|
1589
|
+
off += stmtBytes.length;
|
|
1590
|
+
body[off++] = 0;
|
|
1591
|
+
body[off] = paramFormats.length >> 8 & 255;
|
|
1592
|
+
body[off + 1] = paramFormats.length & 255;
|
|
1593
|
+
off += 2;
|
|
1572
1594
|
for (const f of paramFormats) {
|
|
1573
|
-
body
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1595
|
+
body[off] = f >> 8 & 255;
|
|
1596
|
+
body[off + 1] = f & 255;
|
|
1597
|
+
off += 2;
|
|
1598
|
+
}
|
|
1599
|
+
body[off] = params.length >> 8 & 255;
|
|
1600
|
+
body[off + 1] = params.length & 255;
|
|
1601
|
+
off += 2;
|
|
1602
|
+
for (let i = 0; i < params.length; i++) {
|
|
1603
|
+
const p = params[i];
|
|
1577
1604
|
if (p === null) {
|
|
1578
|
-
body
|
|
1605
|
+
body[off] = body[off + 1] = body[off + 2] = body[off + 3] = 255;
|
|
1606
|
+
off += 4;
|
|
1579
1607
|
} else {
|
|
1580
|
-
const
|
|
1581
|
-
body
|
|
1582
|
-
|
|
1608
|
+
const len = lens[i];
|
|
1609
|
+
body[off] = len >> 24 & 255;
|
|
1610
|
+
body[off + 1] = len >> 16 & 255;
|
|
1611
|
+
body[off + 2] = len >> 8 & 255;
|
|
1612
|
+
body[off + 3] = len & 255;
|
|
1613
|
+
off += 4;
|
|
1614
|
+
if (typeof p === "string") body.set(utf8(p), off);
|
|
1615
|
+
else body.set(p, off);
|
|
1616
|
+
off += len;
|
|
1583
1617
|
}
|
|
1584
1618
|
}
|
|
1585
|
-
body
|
|
1586
|
-
|
|
1619
|
+
body[off] = 0;
|
|
1620
|
+
body[off + 1] = 0;
|
|
1621
|
+
return encodeMessage("B", body);
|
|
1622
|
+
}
|
|
1623
|
+
function utf8Len(s) {
|
|
1624
|
+
return Buffer.byteLength(s);
|
|
1587
1625
|
}
|
|
1588
1626
|
function executeMessage(portal = "", maxRows = 0) {
|
|
1589
1627
|
const body = new Uint8Array(portal.length + 1 + 4);
|
|
@@ -1675,7 +1713,7 @@ function parseRowDescription(payload) {
|
|
|
1675
1713
|
for (let c = 0; c < count; c++) {
|
|
1676
1714
|
let j = i;
|
|
1677
1715
|
while (payload[j] !== 0) j++;
|
|
1678
|
-
const name =
|
|
1716
|
+
const name = _decoder.decode(payload.subarray(i, j));
|
|
1679
1717
|
i = j + 1;
|
|
1680
1718
|
i += 6;
|
|
1681
1719
|
const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
@@ -1697,7 +1735,7 @@ function parseDataRow(payload) {
|
|
|
1697
1735
|
if (len === -1) {
|
|
1698
1736
|
values.push(null);
|
|
1699
1737
|
} else {
|
|
1700
|
-
values.push(
|
|
1738
|
+
values.push(_decoder.decode(payload.subarray(i, i + len)));
|
|
1701
1739
|
i += len;
|
|
1702
1740
|
}
|
|
1703
1741
|
}
|
|
@@ -1710,7 +1748,7 @@ function parseErrorFields(payload) {
|
|
|
1710
1748
|
const type = String.fromCharCode(payload[i]);
|
|
1711
1749
|
let j = i + 1;
|
|
1712
1750
|
while (j < payload.length && payload[j] !== 0) j++;
|
|
1713
|
-
const value =
|
|
1751
|
+
const value = _decoder.decode(payload.subarray(i + 1, j));
|
|
1714
1752
|
switch (type) {
|
|
1715
1753
|
case "S":
|
|
1716
1754
|
out.severity = value;
|
|
@@ -1735,8 +1773,10 @@ function parseErrorFields(payload) {
|
|
|
1735
1773
|
}
|
|
1736
1774
|
return out;
|
|
1737
1775
|
}
|
|
1776
|
+
var _decoder = new TextDecoder();
|
|
1777
|
+
var _encoder = new TextEncoder();
|
|
1738
1778
|
function utf8(s) {
|
|
1739
|
-
return
|
|
1779
|
+
return _encoder.encode(s);
|
|
1740
1780
|
}
|
|
1741
1781
|
function concat(...parts) {
|
|
1742
1782
|
let total = 0;
|
|
@@ -1789,7 +1829,7 @@ var ValidationError = class extends DbError {
|
|
|
1789
1829
|
};
|
|
1790
1830
|
|
|
1791
1831
|
// src/db/postgres/connection.ts
|
|
1792
|
-
var PgConnection = class {
|
|
1832
|
+
var PgConnection = class _PgConnection {
|
|
1793
1833
|
opts;
|
|
1794
1834
|
timeoutSet = false;
|
|
1795
1835
|
awaitingReady = false;
|
|
@@ -1823,11 +1863,7 @@ var PgConnection = class {
|
|
|
1823
1863
|
}, this.opts.connectTimeoutMs);
|
|
1824
1864
|
sock.once("connect", () => {
|
|
1825
1865
|
sock.setNoDelay(true);
|
|
1826
|
-
sock.write(
|
|
1827
|
-
Buffer.from(
|
|
1828
|
-
startupMessage({ user: this.opts.user, database: this.opts.database })
|
|
1829
|
-
)
|
|
1830
|
-
);
|
|
1866
|
+
sock.write(startupMessage({ user: this.opts.user, database: this.opts.database }));
|
|
1831
1867
|
});
|
|
1832
1868
|
sock.on("data", (chunk) => this.onData(chunk));
|
|
1833
1869
|
sock.on("error", (err) => {
|
|
@@ -1860,10 +1896,10 @@ var PgConnection = class {
|
|
|
1860
1896
|
onReady = null;
|
|
1861
1897
|
onAuthFail = null;
|
|
1862
1898
|
expectingAuth = true;
|
|
1863
|
-
authStage = "start";
|
|
1864
1899
|
authCtx = null;
|
|
1865
1900
|
prepared = /* @__PURE__ */ new Map();
|
|
1866
|
-
// sig → stmt +
|
|
1901
|
+
// sig → stmt + 列缓存(LRU)
|
|
1902
|
+
static PREPARED_MAX = 128;
|
|
1867
1903
|
stmtSeq = 0;
|
|
1868
1904
|
currentQuery = null;
|
|
1869
1905
|
onData(chunk) {
|
|
@@ -1885,7 +1921,6 @@ var PgConnection = class {
|
|
|
1885
1921
|
const outer = crypto2.createHash("md5").update(inner + Buffer.from(salt).toString("latin1")).digest("hex");
|
|
1886
1922
|
this.send(passwordMessage(`md5${outer}`));
|
|
1887
1923
|
} else if (code === 10) {
|
|
1888
|
-
this.authStage = "sasl-initial";
|
|
1889
1924
|
const nonce = crypto2.randomBytes(18).toString("base64url");
|
|
1890
1925
|
const clientFirstBare = `n=,r=${nonce}`;
|
|
1891
1926
|
this.authCtx = { clientNonce: nonce, clientFirstBare, serverFirst: "" };
|
|
@@ -1907,7 +1942,6 @@ var PgConnection = class {
|
|
|
1907
1942
|
}
|
|
1908
1943
|
this.authCtx.serverFirst = serverFirst;
|
|
1909
1944
|
const clientFinal = this.scramFinal(this.authCtx);
|
|
1910
|
-
this.authStage = "sasl-final";
|
|
1911
1945
|
this.send(encodeP(utf82(clientFinal)));
|
|
1912
1946
|
} else if (code === 12) {
|
|
1913
1947
|
const serverFinal = new TextDecoder().decode(msg.payload.subarray(4));
|
|
@@ -1934,9 +1968,7 @@ var PgConnection = class {
|
|
|
1934
1968
|
if (this.opts.statementTimeoutMs > 0 && !this.timeoutSet) {
|
|
1935
1969
|
this.timeoutSet = true;
|
|
1936
1970
|
this.awaitingReady = true;
|
|
1937
|
-
this.socket?.write(
|
|
1938
|
-
Buffer.from(queryMessage(`SET statement_timeout = ${this.opts.statementTimeoutMs}`))
|
|
1939
|
-
);
|
|
1971
|
+
this.socket?.write(queryMessage(`SET statement_timeout = ${this.opts.statementTimeoutMs}`));
|
|
1940
1972
|
return;
|
|
1941
1973
|
}
|
|
1942
1974
|
this.onReady?.();
|
|
@@ -1948,9 +1980,8 @@ var PgConnection = class {
|
|
|
1948
1980
|
case "T": {
|
|
1949
1981
|
if (this.currentQuery) {
|
|
1950
1982
|
this.currentQuery.columns = parseRowDescription(msg.payload);
|
|
1951
|
-
const
|
|
1952
|
-
|
|
1953
|
-
);
|
|
1983
|
+
const sig = `${this.currentQuery.sql}|${this.currentQuery.params?.length ?? 0}`;
|
|
1984
|
+
const entry = this.getPrepared(sig);
|
|
1954
1985
|
if (entry) entry.columns = this.currentQuery.columns;
|
|
1955
1986
|
}
|
|
1956
1987
|
break;
|
|
@@ -1973,7 +2004,7 @@ var PgConnection = class {
|
|
|
1973
2004
|
if (this.currentQuery?.awaitingDescribe && this.currentQuery.sql !== void 0) {
|
|
1974
2005
|
this.currentQuery.awaitingDescribe = false;
|
|
1975
2006
|
if (this.currentQuery.prepKey && this.currentQuery.prepName) {
|
|
1976
|
-
this.
|
|
2007
|
+
this.setPrepared(this.currentQuery.prepKey, {
|
|
1977
2008
|
name: this.currentQuery.prepName,
|
|
1978
2009
|
columns: this.currentQuery.columns
|
|
1979
2010
|
});
|
|
@@ -2057,6 +2088,24 @@ var PgConnection = class {
|
|
|
2057
2088
|
const authMessage = `${ctx.clientFirstBare},${ctx.serverFirst},${clientFinalWithoutProof}`;
|
|
2058
2089
|
return Buffer.from(hmac(serverKey, authMessage)).toString("base64");
|
|
2059
2090
|
}
|
|
2091
|
+
/** LRU 读取:命中移到尾部(最近使用),超限删最旧 */
|
|
2092
|
+
getPrepared(sig) {
|
|
2093
|
+
const entry = this.prepared.get(sig);
|
|
2094
|
+
if (entry) {
|
|
2095
|
+
this.prepared.delete(sig);
|
|
2096
|
+
this.prepared.set(sig, entry);
|
|
2097
|
+
}
|
|
2098
|
+
return entry;
|
|
2099
|
+
}
|
|
2100
|
+
/** LRU 写入:刷新位置;超上限淘汰最旧(长运行服务防无限累积) */
|
|
2101
|
+
setPrepared(sig, entry) {
|
|
2102
|
+
this.prepared.delete(sig);
|
|
2103
|
+
this.prepared.set(sig, entry);
|
|
2104
|
+
if (this.prepared.size > _PgConnection.PREPARED_MAX) {
|
|
2105
|
+
const oldest = this.prepared.keys().next().value;
|
|
2106
|
+
if (oldest !== void 0) this.prepared.delete(oldest);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2060
2109
|
/** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
|
|
2061
2110
|
async transaction(fn) {
|
|
2062
2111
|
await this.query("BEGIN");
|
|
@@ -2088,10 +2137,10 @@ var PgConnection = class {
|
|
|
2088
2137
|
resolve: resolve3,
|
|
2089
2138
|
reject
|
|
2090
2139
|
};
|
|
2091
|
-
this.socket.write(
|
|
2140
|
+
this.socket.write(queryMessage(sql));
|
|
2092
2141
|
} else {
|
|
2093
2142
|
const sig = `${sql}|${params.length}`;
|
|
2094
|
-
let stmtEntry = this.
|
|
2143
|
+
let stmtEntry = this.getPrepared(sig);
|
|
2095
2144
|
const encoded = encodeParams(params);
|
|
2096
2145
|
if (!stmtEntry) {
|
|
2097
2146
|
const name = `wf_s${++this.stmtSeq}`;
|
|
@@ -2106,9 +2155,9 @@ var PgConnection = class {
|
|
|
2106
2155
|
prepKey: sig,
|
|
2107
2156
|
prepName: name
|
|
2108
2157
|
};
|
|
2109
|
-
this.socket.write(
|
|
2110
|
-
this.socket.write(
|
|
2111
|
-
this.socket.write(
|
|
2158
|
+
this.socket.write(parseMessage(name, sql, params.map(() => 0)));
|
|
2159
|
+
this.socket.write(describeMessage("S", name));
|
|
2160
|
+
this.socket.write(flushMessage());
|
|
2112
2161
|
} else {
|
|
2113
2162
|
this.currentQuery = {
|
|
2114
2163
|
columns: [...stmtEntry.columns],
|
|
@@ -2119,15 +2168,15 @@ var PgConnection = class {
|
|
|
2119
2168
|
params: encoded,
|
|
2120
2169
|
awaitingDescribe: false
|
|
2121
2170
|
};
|
|
2122
|
-
this.socket.write(
|
|
2123
|
-
this.socket.write(
|
|
2124
|
-
this.socket.write(
|
|
2171
|
+
this.socket.write(bindMessage(stmtEntry.name, encoded));
|
|
2172
|
+
this.socket.write(executeMessage());
|
|
2173
|
+
this.socket.write(syncMessage());
|
|
2125
2174
|
}
|
|
2126
2175
|
}
|
|
2127
2176
|
});
|
|
2128
2177
|
}
|
|
2129
2178
|
send(data) {
|
|
2130
|
-
this.socket?.write(
|
|
2179
|
+
this.socket?.write(data);
|
|
2131
2180
|
}
|
|
2132
2181
|
notifyIdle() {
|
|
2133
2182
|
const ws = this.waiters;
|
|
@@ -2143,7 +2192,7 @@ var PgConnection = class {
|
|
|
2143
2192
|
async close() {
|
|
2144
2193
|
const sock = this.socket;
|
|
2145
2194
|
if (this.status === "ready" && sock) {
|
|
2146
|
-
sock.write(
|
|
2195
|
+
sock.write(terminateMessage());
|
|
2147
2196
|
}
|
|
2148
2197
|
this.status = "closed";
|
|
2149
2198
|
if (sock) sock.destroy();
|
|
@@ -2615,19 +2664,56 @@ var IncompleteError = class extends Error {
|
|
|
2615
2664
|
this.name = "IncompleteError";
|
|
2616
2665
|
}
|
|
2617
2666
|
};
|
|
2667
|
+
function decodeValue(v, decodeBytes) {
|
|
2668
|
+
if (v instanceof Uint8Array) return decodeBytes ? _decoder2.decode(v) : v;
|
|
2669
|
+
if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
|
|
2670
|
+
return v;
|
|
2671
|
+
}
|
|
2618
2672
|
function encodeCommand(args) {
|
|
2619
|
-
const
|
|
2620
|
-
|
|
2621
|
-
for (
|
|
2622
|
-
const
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2673
|
+
const lens = new Array(args.length);
|
|
2674
|
+
let total = headerLen(args.length);
|
|
2675
|
+
for (let i = 0; i < args.length; i++) {
|
|
2676
|
+
const arg = args[i];
|
|
2677
|
+
const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
|
|
2678
|
+
lens[i] = len;
|
|
2679
|
+
total += headerLen(len) + len + 2;
|
|
2680
|
+
}
|
|
2681
|
+
const out = new Uint8Array(total);
|
|
2682
|
+
let off = 0;
|
|
2683
|
+
const head = `*${args.length}\r
|
|
2684
|
+
`;
|
|
2685
|
+
out.set(_encoder2.encode(head), 0);
|
|
2686
|
+
off = head.length;
|
|
2687
|
+
for (let i = 0; i < args.length; i++) {
|
|
2688
|
+
const arg = args[i];
|
|
2689
|
+
const len = lens[i];
|
|
2690
|
+
const lh = `$${len}\r
|
|
2691
|
+
`;
|
|
2692
|
+
out.set(_encoder2.encode(lh), off);
|
|
2693
|
+
off += lh.length;
|
|
2694
|
+
if (typeof arg === "string") {
|
|
2695
|
+
out.set(_encoder2.encode(arg), off);
|
|
2696
|
+
} else if (arg instanceof Buffer) {
|
|
2697
|
+
out.set(arg, off);
|
|
2698
|
+
} else {
|
|
2699
|
+
out.set(_encoder2.encode(String(arg)), off);
|
|
2700
|
+
}
|
|
2701
|
+
off += len;
|
|
2702
|
+
out[off] = 13;
|
|
2703
|
+
out[off + 1] = 10;
|
|
2704
|
+
off += 2;
|
|
2626
2705
|
}
|
|
2627
|
-
return
|
|
2706
|
+
return out;
|
|
2628
2707
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2708
|
+
function headerLen(n) {
|
|
2709
|
+
if (n < 10) return 4;
|
|
2710
|
+
if (n < 100) return 5;
|
|
2711
|
+
if (n < 1e3) return 6;
|
|
2712
|
+
if (n < 1e4) return 7;
|
|
2713
|
+
return 8;
|
|
2714
|
+
}
|
|
2715
|
+
var _decoder2 = new TextDecoder();
|
|
2716
|
+
var _encoder2 = new TextEncoder();
|
|
2631
2717
|
var RespParser = class {
|
|
2632
2718
|
buf = new Uint8Array(0);
|
|
2633
2719
|
off = 0;
|
|
@@ -2701,16 +2787,15 @@ var RespParser = class {
|
|
|
2701
2787
|
return new RespError(line);
|
|
2702
2788
|
}
|
|
2703
2789
|
case ":": {
|
|
2704
|
-
|
|
2705
|
-
return parseInt(line, 10);
|
|
2790
|
+
return this.readInt();
|
|
2706
2791
|
}
|
|
2707
2792
|
case "$": {
|
|
2708
|
-
const len =
|
|
2793
|
+
const len = this.readInt();
|
|
2709
2794
|
if (len === -1) return null;
|
|
2710
|
-
return this.
|
|
2795
|
+
return this.readBulkBytes(len);
|
|
2711
2796
|
}
|
|
2712
2797
|
case "*": {
|
|
2713
|
-
const count =
|
|
2798
|
+
const count = this.readInt();
|
|
2714
2799
|
if (count === -1) return null;
|
|
2715
2800
|
const items = [];
|
|
2716
2801
|
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
@@ -2720,25 +2805,50 @@ var RespParser = class {
|
|
|
2720
2805
|
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
2721
2806
|
}
|
|
2722
2807
|
}
|
|
2808
|
+
/**
|
|
2809
|
+
* 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
|
|
2810
|
+
* 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
|
|
2811
|
+
*/
|
|
2812
|
+
readInt() {
|
|
2813
|
+
const buf = this.buf;
|
|
2814
|
+
let i = this.off;
|
|
2815
|
+
let neg = false;
|
|
2816
|
+
if (i < buf.length && buf[i] === 45) {
|
|
2817
|
+
neg = true;
|
|
2818
|
+
i++;
|
|
2819
|
+
}
|
|
2820
|
+
let n = 0;
|
|
2821
|
+
while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
|
|
2822
|
+
n = n * 10 + (buf[i] - 48);
|
|
2823
|
+
i++;
|
|
2824
|
+
}
|
|
2825
|
+
if (i + 1 < buf.length && buf[i] === 13 && buf[i + 1] === 10) {
|
|
2826
|
+
this.off = i + 2;
|
|
2827
|
+
return neg ? -n : n;
|
|
2828
|
+
}
|
|
2829
|
+
throw new IncompleteError();
|
|
2830
|
+
}
|
|
2723
2831
|
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
2724
2832
|
readLine() {
|
|
2725
2833
|
const idx = indexOfCRLF(this.buf, this.off);
|
|
2726
2834
|
if (idx === -1) throw new IncompleteError();
|
|
2727
|
-
const line =
|
|
2835
|
+
const line = _decoder2.decode(this.buf.subarray(this.off, idx));
|
|
2728
2836
|
this.off = idx + 2;
|
|
2729
2837
|
return line;
|
|
2730
2838
|
}
|
|
2731
|
-
/** 读 len 字节的 bulk 内容 + \r\n */
|
|
2732
|
-
|
|
2839
|
+
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
2840
|
+
readBulkBytes(len) {
|
|
2733
2841
|
if (this.buf.length - this.off < len + 2) throw new IncompleteError();
|
|
2734
|
-
const value =
|
|
2842
|
+
const value = this.buf.subarray(this.off, this.off + len);
|
|
2735
2843
|
this.off += len + 2;
|
|
2736
2844
|
return value;
|
|
2737
2845
|
}
|
|
2738
2846
|
};
|
|
2739
2847
|
function indexOfCRLF(buf, from = 0) {
|
|
2740
|
-
|
|
2741
|
-
|
|
2848
|
+
let i = buf.indexOf(13, from);
|
|
2849
|
+
while (i !== -1) {
|
|
2850
|
+
if (i + 1 < buf.length && buf[i + 1] === 10) return i;
|
|
2851
|
+
i = buf.indexOf(13, i + 1);
|
|
2742
2852
|
}
|
|
2743
2853
|
return -1;
|
|
2744
2854
|
}
|
|
@@ -2751,6 +2861,8 @@ var RedisConnection = class {
|
|
|
2751
2861
|
socket = null;
|
|
2752
2862
|
parser = new RespParser();
|
|
2753
2863
|
pending = [];
|
|
2864
|
+
/** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
|
|
2865
|
+
pendingHead = 0;
|
|
2754
2866
|
offlineQueue = [];
|
|
2755
2867
|
subs = /* @__PURE__ */ new Map();
|
|
2756
2868
|
psubs = /* @__PURE__ */ new Map();
|
|
@@ -2766,7 +2878,8 @@ var RedisConnection = class {
|
|
|
2766
2878
|
port: options.port ?? 6379,
|
|
2767
2879
|
retryDelayMs: options.retryDelayMs ?? 100,
|
|
2768
2880
|
maxRetries: options.maxRetries ?? 10,
|
|
2769
|
-
enableOfflineQueue: options.enableOfflineQueue ?? true
|
|
2881
|
+
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
2882
|
+
maxOfflineQueue: options.maxOfflineQueue ?? 5e3
|
|
2770
2883
|
};
|
|
2771
2884
|
}
|
|
2772
2885
|
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
@@ -2816,10 +2929,10 @@ var RedisConnection = class {
|
|
|
2816
2929
|
});
|
|
2817
2930
|
}
|
|
2818
2931
|
handleDisconnect(err) {
|
|
2819
|
-
const queue = this.pending;
|
|
2932
|
+
const queue = this.pending.slice(this.pendingHead);
|
|
2820
2933
|
this.pending = [];
|
|
2821
|
-
|
|
2822
|
-
for (const p of queue) p.reject(
|
|
2934
|
+
this.pendingHead = 0;
|
|
2935
|
+
for (const p of queue) p.reject(err);
|
|
2823
2936
|
if (this.closed || this.status === "closed") return;
|
|
2824
2937
|
this.retries++;
|
|
2825
2938
|
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
@@ -2837,38 +2950,52 @@ var RedisConnection = class {
|
|
|
2837
2950
|
onData(chunk) {
|
|
2838
2951
|
try {
|
|
2839
2952
|
const values = this.parser.pushAll(chunk);
|
|
2840
|
-
for (const
|
|
2841
|
-
|
|
2842
|
-
|
|
2953
|
+
for (const raw of values) {
|
|
2954
|
+
const first = Array.isArray(raw) && raw.length > 0 ? decodeValue(raw[0], true) : void 0;
|
|
2955
|
+
if (typeof first === "string" && PUSH_TYPES.has(first)) {
|
|
2956
|
+
this.dispatchSubscribe(decodeValue(raw, true));
|
|
2843
2957
|
continue;
|
|
2844
2958
|
}
|
|
2845
|
-
const p = this.pending.
|
|
2959
|
+
const p = this.pending[this.pendingHead++];
|
|
2846
2960
|
if (!p) break;
|
|
2847
|
-
if (
|
|
2848
|
-
else p.resolve(
|
|
2961
|
+
if (raw instanceof RespError) p.reject(raw);
|
|
2962
|
+
else p.resolve(decodeValue(raw, !p.asBuffer));
|
|
2963
|
+
}
|
|
2964
|
+
if (this.pendingHead > 64 && this.pendingHead * 2 > this.pending.length) {
|
|
2965
|
+
this.pending = this.pending.slice(this.pendingHead);
|
|
2966
|
+
this.pendingHead = 0;
|
|
2849
2967
|
}
|
|
2850
2968
|
} catch (e) {
|
|
2851
|
-
const queue = this.pending;
|
|
2969
|
+
const queue = this.pending.slice(this.pendingHead);
|
|
2852
2970
|
this.pending = [];
|
|
2971
|
+
this.pendingHead = 0;
|
|
2853
2972
|
for (const q of queue) q.reject(e);
|
|
2854
2973
|
this.socket?.destroy();
|
|
2855
2974
|
}
|
|
2856
2975
|
}
|
|
2857
|
-
/**
|
|
2976
|
+
/**
|
|
2977
|
+
* 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
|
|
2978
|
+
* opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
|
|
2979
|
+
*/
|
|
2858
2980
|
command(name, ...args) {
|
|
2981
|
+
const last = args[args.length - 1];
|
|
2982
|
+
const opts = typeof last === "object" && last !== null && !(last instanceof Uint8Array) ? args.pop() : void 0;
|
|
2859
2983
|
if (this.status === "ready" && this.socket) {
|
|
2860
|
-
return this.sendNow(name, args);
|
|
2984
|
+
return this.sendNow(name, args, opts?.asBuffer);
|
|
2861
2985
|
}
|
|
2862
2986
|
if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
|
|
2863
2987
|
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
2864
2988
|
}
|
|
2989
|
+
if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
|
|
2990
|
+
return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
|
|
2991
|
+
}
|
|
2865
2992
|
return new Promise((resolve3, reject) => {
|
|
2866
|
-
this.offlineQueue.push({ name, args, resolve: resolve3, reject });
|
|
2993
|
+
this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
|
|
2867
2994
|
});
|
|
2868
2995
|
}
|
|
2869
|
-
sendNow(name, args) {
|
|
2996
|
+
sendNow(name, args, asBuffer) {
|
|
2870
2997
|
return new Promise((resolve3, reject) => {
|
|
2871
|
-
this.pending.push({ resolve: resolve3, reject });
|
|
2998
|
+
this.pending.push({ resolve: resolve3, reject, asBuffer });
|
|
2872
2999
|
this.socket.write(encodeCommand([name, ...args]));
|
|
2873
3000
|
});
|
|
2874
3001
|
}
|
|
@@ -2876,7 +3003,7 @@ var RedisConnection = class {
|
|
|
2876
3003
|
const queue = this.offlineQueue;
|
|
2877
3004
|
this.offlineQueue = [];
|
|
2878
3005
|
for (const q of queue) {
|
|
2879
|
-
this.sendNow(q.name, q.args).then(q.resolve, q.reject);
|
|
3006
|
+
this.sendNow(q.name, q.args, q.asBuffer).then(q.resolve, q.reject);
|
|
2880
3007
|
}
|
|
2881
3008
|
}
|
|
2882
3009
|
/** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
|
|
@@ -2934,15 +3061,16 @@ var RedisConnection = class {
|
|
|
2934
3061
|
clearTimeout(this.reconnectTimer);
|
|
2935
3062
|
this.reconnectTimer = null;
|
|
2936
3063
|
}
|
|
2937
|
-
const queue = this.pending;
|
|
3064
|
+
const queue = this.pending.slice(this.pendingHead);
|
|
2938
3065
|
this.pending = [];
|
|
3066
|
+
this.pendingHead = 0;
|
|
2939
3067
|
for (const p of queue) p.reject(new ConnectionError("redis: connection closed"));
|
|
3068
|
+
const oq = this.offlineQueue;
|
|
3069
|
+
this.offlineQueue = [];
|
|
3070
|
+
for (const q of oq) q.reject(new ConnectionError("redis: connection closed"));
|
|
2940
3071
|
const sock = this.socket;
|
|
2941
3072
|
this.socket = null;
|
|
2942
|
-
|
|
2943
|
-
sock.destroy();
|
|
2944
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
2945
|
-
}
|
|
3073
|
+
sock?.destroy();
|
|
2946
3074
|
}
|
|
2947
3075
|
get connected() {
|
|
2948
3076
|
return this.status === "ready";
|
|
@@ -2961,7 +3089,7 @@ var RedisClient = class _RedisClient {
|
|
|
2961
3089
|
await conn.connect();
|
|
2962
3090
|
return new _RedisClient(conn);
|
|
2963
3091
|
}
|
|
2964
|
-
/** 底层命令透传(RESP
|
|
3092
|
+
/** 底层命令透传(RESP 值);Buffer 参数字节原样发送 */
|
|
2965
3093
|
command(name, ...args) {
|
|
2966
3094
|
return this.conn.command(name, ...args);
|
|
2967
3095
|
}
|
|
@@ -2970,6 +3098,15 @@ var RedisClient = class _RedisClient {
|
|
|
2970
3098
|
const v = await this.conn.command("GET", key);
|
|
2971
3099
|
return v === null ? null : String(v);
|
|
2972
3100
|
}
|
|
3101
|
+
/**
|
|
3102
|
+
* 二进制安全读取:返回原始字节(Uint8Array),不经过字符串解码。
|
|
3103
|
+
* 用于缓存二进制 payload(序列化字节、图片等)。key 不存在返回 null。
|
|
3104
|
+
*/
|
|
3105
|
+
async getBuffer(key) {
|
|
3106
|
+
const v = await this.conn.command("GET", key, { asBuffer: true });
|
|
3107
|
+
if (v === null) return null;
|
|
3108
|
+
return v instanceof Uint8Array ? v : new TextEncoder().encode(String(v));
|
|
3109
|
+
}
|
|
2973
3110
|
/**
|
|
2974
3111
|
* SET。ttl 秒可省略;传入即安全生效(内部转 SET key val EX ttl)。
|
|
2975
3112
|
*/
|
|
@@ -3106,6 +3243,11 @@ var RedisPool = class _RedisPool {
|
|
|
3106
3243
|
await this.ensure();
|
|
3107
3244
|
return this.next().get(this.k(key));
|
|
3108
3245
|
}
|
|
3246
|
+
/** 二进制安全读取(原始字节,不解码) */
|
|
3247
|
+
async getBuffer(key) {
|
|
3248
|
+
await this.ensure();
|
|
3249
|
+
return this.next().getBuffer(this.k(key));
|
|
3250
|
+
}
|
|
3109
3251
|
async set(key, value, ttl) {
|
|
3110
3252
|
await this.ensure();
|
|
3111
3253
|
return this.next().set(this.k(key), value, ttl);
|