weifuwu 0.55.3 → 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 +222 -18
- package/dist/client/app.d.ts +3 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +337 -87
- package/dist/client/reactive.d.ts +6 -0
- package/dist/client/render.d.ts +11 -0
- package/dist/client/route-match.d.ts +22 -0
- package/dist/client/types.d.ts +17 -0
- package/dist/client/vnode.d.ts +30 -1
- package/dist/components/index.js +56 -18
- 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/dev/index.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +529 -89
- package/dist/ui/html-safe.d.ts +11 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/ssr-page.d.ts +41 -0
- package/dist/ui/ssr.d.ts +35 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1083,23 +1083,219 @@ import { build } from "esbuild";
|
|
|
1083
1083
|
import { readFile, stat } from "node:fs/promises";
|
|
1084
1084
|
import { resolve as resolve2 } from "node:path";
|
|
1085
1085
|
import { fileURLToPath } from "node:url";
|
|
1086
|
+
|
|
1087
|
+
// src/ui/html-safe.ts
|
|
1086
1088
|
var HtmlSafe = class {
|
|
1089
|
+
value;
|
|
1087
1090
|
constructor(value) {
|
|
1088
1091
|
this.value = value;
|
|
1089
1092
|
}
|
|
1090
|
-
value;
|
|
1091
1093
|
toString() {
|
|
1092
1094
|
return this.value;
|
|
1093
1095
|
}
|
|
1094
1096
|
};
|
|
1097
|
+
|
|
1098
|
+
// src/client/vnode.ts
|
|
1099
|
+
var ASYNC_MARK = "__wfAsyncComponent";
|
|
1100
|
+
function isAsyncComponent(type) {
|
|
1101
|
+
return typeof type === "function" && type?.[ASYNC_MARK] === true;
|
|
1102
|
+
}
|
|
1103
|
+
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
1104
|
+
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
1105
|
+
|
|
1106
|
+
// src/client/reactive.ts
|
|
1107
|
+
function createReactiveState(dirty) {
|
|
1108
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
1109
|
+
const reactive = (target) => {
|
|
1110
|
+
if (target === null || typeof target !== "object") return target;
|
|
1111
|
+
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
1112
|
+
const proxy = new Proxy(target, {
|
|
1113
|
+
set(target2, key, value) {
|
|
1114
|
+
const old = Reflect.get(target2, key);
|
|
1115
|
+
if (old === value) return true;
|
|
1116
|
+
Reflect.set(target2, key, value);
|
|
1117
|
+
dirty();
|
|
1118
|
+
return true;
|
|
1119
|
+
},
|
|
1120
|
+
get(target2, key) {
|
|
1121
|
+
const value = Reflect.get(target2, key);
|
|
1122
|
+
if (typeof value === "object" && value !== null) return reactive(value);
|
|
1123
|
+
return value;
|
|
1124
|
+
},
|
|
1125
|
+
deleteProperty(target2, key) {
|
|
1126
|
+
if (Reflect.has(target2, key)) {
|
|
1127
|
+
Reflect.deleteProperty(target2, key);
|
|
1128
|
+
dirty();
|
|
1129
|
+
}
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
proxyCache.set(target, proxy);
|
|
1134
|
+
return proxy;
|
|
1135
|
+
};
|
|
1136
|
+
return reactive({});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/ui/ssr.ts
|
|
1095
1140
|
function escape(s) {
|
|
1096
1141
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1097
1142
|
}
|
|
1143
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
1144
|
+
"area",
|
|
1145
|
+
"base",
|
|
1146
|
+
"br",
|
|
1147
|
+
"col",
|
|
1148
|
+
"embed",
|
|
1149
|
+
"hr",
|
|
1150
|
+
"img",
|
|
1151
|
+
"input",
|
|
1152
|
+
"link",
|
|
1153
|
+
"meta",
|
|
1154
|
+
"param",
|
|
1155
|
+
"source",
|
|
1156
|
+
"track",
|
|
1157
|
+
"wbr"
|
|
1158
|
+
]);
|
|
1159
|
+
function classToString(v) {
|
|
1160
|
+
if (typeof v === "string") return v;
|
|
1161
|
+
if (Array.isArray(v)) return v.filter(Boolean).join(" ");
|
|
1162
|
+
if (v && typeof v === "object") {
|
|
1163
|
+
return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
|
|
1164
|
+
}
|
|
1165
|
+
return "";
|
|
1166
|
+
}
|
|
1167
|
+
function styleToString(v) {
|
|
1168
|
+
if (typeof v === "string") return v;
|
|
1169
|
+
if (v && typeof v === "object") {
|
|
1170
|
+
return Object.entries(v).filter(([, x]) => x != null).map(([k, x]) => `${k}:${typeof x === "number" ? x + "px" : String(x)}`).join(";");
|
|
1171
|
+
}
|
|
1172
|
+
return "";
|
|
1173
|
+
}
|
|
1174
|
+
function createSsrContext(serverCtx, dataStore) {
|
|
1175
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
1176
|
+
const ui2 = {
|
|
1177
|
+
$: () => createReactiveState(() => {
|
|
1178
|
+
}),
|
|
1179
|
+
dirty: () => {
|
|
1180
|
+
},
|
|
1181
|
+
render: () => {
|
|
1182
|
+
},
|
|
1183
|
+
selfId: (name) => {
|
|
1184
|
+
if (usedIds.has(name)) throw new Error(`[weifuwu] Duplicate component ID: "${name}"`);
|
|
1185
|
+
usedIds.add(name);
|
|
1186
|
+
},
|
|
1187
|
+
useMedia: () => {
|
|
1188
|
+
},
|
|
1189
|
+
useBreakpoint: () => {
|
|
1190
|
+
},
|
|
1191
|
+
usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
|
|
1192
|
+
} })
|
|
1193
|
+
};
|
|
1194
|
+
const ctx = Object.create(serverCtx ?? {});
|
|
1195
|
+
ctx.ui = ui2;
|
|
1196
|
+
ctx.data = {
|
|
1197
|
+
async get(key, fetcher) {
|
|
1198
|
+
if (dataStore.has(key)) return dataStore.get(key);
|
|
1199
|
+
if (!fetcher) return void 0;
|
|
1200
|
+
const val = await fetcher();
|
|
1201
|
+
dataStore.set(key, val);
|
|
1202
|
+
return val;
|
|
1203
|
+
},
|
|
1204
|
+
set(key, val) {
|
|
1205
|
+
dataStore.set(key, val);
|
|
1206
|
+
},
|
|
1207
|
+
has(key) {
|
|
1208
|
+
return dataStore.has(key);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
return ctx;
|
|
1212
|
+
}
|
|
1213
|
+
async function renderSsr(input, ctx) {
|
|
1214
|
+
if (input == null || typeof input === "boolean") return "";
|
|
1215
|
+
if (typeof input === "string" || typeof input === "number") return escape(String(input));
|
|
1216
|
+
if (Array.isArray(input)) {
|
|
1217
|
+
let s = "";
|
|
1218
|
+
for (const c of input) s += await renderSsr(c, ctx);
|
|
1219
|
+
return s;
|
|
1220
|
+
}
|
|
1221
|
+
const vnode = input;
|
|
1222
|
+
if (vnode.type === Portal) return renderSsr(vnode.props?.children, ctx);
|
|
1223
|
+
if (vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
|
|
1224
|
+
if (typeof vnode.type === "function") {
|
|
1225
|
+
let def;
|
|
1226
|
+
if (isAsyncComponent(vnode.type)) {
|
|
1227
|
+
def = await vnode.type(ctx);
|
|
1228
|
+
if (typeof def !== "function") {
|
|
1229
|
+
throw new Error(
|
|
1230
|
+
`asyncComponent factory <${vnode.type.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
} else {
|
|
1234
|
+
def = vnode.type;
|
|
1235
|
+
}
|
|
1236
|
+
const childCtx = Object.create(ctx);
|
|
1237
|
+
const renderFn = def(vnode.props ?? {}, childCtx);
|
|
1238
|
+
if (typeof renderFn !== "function") {
|
|
1239
|
+
throw new Error(
|
|
1240
|
+
`Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
return renderSsr(renderFn(vnode.props ?? {}), childCtx);
|
|
1244
|
+
}
|
|
1245
|
+
const tag = vnode.type;
|
|
1246
|
+
const props = vnode.props ?? {};
|
|
1247
|
+
const attrs = [];
|
|
1248
|
+
let innerHTML;
|
|
1249
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1250
|
+
if (key === "children" || key === "key") continue;
|
|
1251
|
+
if (key === "ref") continue;
|
|
1252
|
+
if (key.startsWith("on") && typeof value === "function") continue;
|
|
1253
|
+
if (key === "innerHTML") {
|
|
1254
|
+
innerHTML = String(value ?? "");
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (key === "class" || key === "className") {
|
|
1258
|
+
const cls = classToString(value);
|
|
1259
|
+
if (cls) attrs.push(` class="${escape(cls)}"`);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (key === "style" && value && typeof value === "object") {
|
|
1263
|
+
attrs.push(` style="${escape(styleToString(value))}"`);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (value === true) {
|
|
1267
|
+
attrs.push(` ${key}`);
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
if (value === false || value == null) continue;
|
|
1271
|
+
attrs.push(` ${key}="${escape(String(value))}"`);
|
|
1272
|
+
}
|
|
1273
|
+
const attrStr = attrs.join("");
|
|
1274
|
+
if (VOID_TAGS.has(tag)) return `<${tag}${attrStr}>`;
|
|
1275
|
+
if (innerHTML !== void 0) return `<${tag}${attrStr}>${innerHTML}</${tag}>`;
|
|
1276
|
+
const children = await renderSsr(props.children, ctx);
|
|
1277
|
+
return `<${tag}${attrStr}>${children}</${tag}>`;
|
|
1278
|
+
}
|
|
1279
|
+
async function ssrToString(Comp, props, serverCtx, opts = {}) {
|
|
1280
|
+
const dataStore = opts.data ?? /* @__PURE__ */ new Map();
|
|
1281
|
+
const ctx = createSsrContext(serverCtx, dataStore);
|
|
1282
|
+
const html = await renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
|
|
1283
|
+
return new HtmlSafe(html);
|
|
1284
|
+
}
|
|
1285
|
+
function serializeData(data) {
|
|
1286
|
+
const json = JSON.stringify(Object.fromEntries(data)).replace(/</g, "\\u003c");
|
|
1287
|
+
return `<script>window.__DATA__=${json};</script>`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/ui/index.ts
|
|
1291
|
+
function escape2(s) {
|
|
1292
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1293
|
+
}
|
|
1098
1294
|
function stringify(v) {
|
|
1099
1295
|
if (v == null || v === false || v === true) return "";
|
|
1100
1296
|
if (Array.isArray(v)) return v.map(stringify).join("");
|
|
1101
1297
|
if (v instanceof HtmlSafe) return v.value;
|
|
1102
|
-
return
|
|
1298
|
+
return escape2(String(v));
|
|
1103
1299
|
}
|
|
1104
1300
|
function unsafe(s) {
|
|
1105
1301
|
return new HtmlSafe(s);
|
|
@@ -1153,6 +1349,15 @@ function ui() {
|
|
|
1153
1349
|
}
|
|
1154
1350
|
ctx.ui = {
|
|
1155
1351
|
html: Object.assign(htmlTag, { unsafe }),
|
|
1352
|
+
/** SSR 渲染组件 → HTML 片段(HtmlSafe:内联进 ctx.ui.html 不二次转义) */
|
|
1353
|
+
async ssr(Comp, props, opts) {
|
|
1354
|
+
const safe = await ssrToString(Comp, props ?? {}, ctx, opts);
|
|
1355
|
+
return safe;
|
|
1356
|
+
},
|
|
1357
|
+
/** 序列化 SSR 数据存储 → window.__DATA__ 脚本(HtmlSafe) */
|
|
1358
|
+
ssrData(data) {
|
|
1359
|
+
return new HtmlSafe(serializeData(data));
|
|
1360
|
+
},
|
|
1156
1361
|
async js(entryPath) {
|
|
1157
1362
|
const absPath = resolveEntry(entryPath);
|
|
1158
1363
|
const cached = jsCache.get(absPath);
|
|
@@ -1217,6 +1422,98 @@ function ui() {
|
|
|
1217
1422
|
};
|
|
1218
1423
|
}
|
|
1219
1424
|
|
|
1425
|
+
// src/client/route-match.ts
|
|
1426
|
+
function joinPaths(a, b) {
|
|
1427
|
+
if (!b || b === "/") return a || "/";
|
|
1428
|
+
const left = a.endsWith("/") ? a.slice(0, -1) : a;
|
|
1429
|
+
const right = b.startsWith("/") ? b : "/" + b;
|
|
1430
|
+
return left + right;
|
|
1431
|
+
}
|
|
1432
|
+
function compilePath(path) {
|
|
1433
|
+
const keys = [];
|
|
1434
|
+
const reStr = path.replace(/:(\w+)/g, (_, key) => {
|
|
1435
|
+
keys.push(key);
|
|
1436
|
+
return "([^/]+)";
|
|
1437
|
+
}).replace(/\*/g, ".*");
|
|
1438
|
+
return { re: new RegExp(`^${reStr}$`), keys };
|
|
1439
|
+
}
|
|
1440
|
+
function flattenRoutes(routes, basePath = "", chain = []) {
|
|
1441
|
+
const result = [];
|
|
1442
|
+
for (const route of routes) {
|
|
1443
|
+
const fullPath = joinPaths(basePath, route.path);
|
|
1444
|
+
const { re, keys } = compilePath(fullPath);
|
|
1445
|
+
const fullChain = [...chain, route];
|
|
1446
|
+
result.push({ re, keys, def: route, chain: fullChain });
|
|
1447
|
+
if (route.children) {
|
|
1448
|
+
result.push(...flattenRoutes(route.children, fullPath, fullChain));
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return result;
|
|
1452
|
+
}
|
|
1453
|
+
function matchRoute(path, routes) {
|
|
1454
|
+
let best = null;
|
|
1455
|
+
for (const fr of routes) {
|
|
1456
|
+
if (path.match(fr.re)) {
|
|
1457
|
+
if (!best || fr.chain.length > best.chain.length) best = fr;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return best;
|
|
1461
|
+
}
|
|
1462
|
+
function extractParams(path, match) {
|
|
1463
|
+
const params = {};
|
|
1464
|
+
const m = path.match(match.re);
|
|
1465
|
+
if (m) {
|
|
1466
|
+
for (let i = 0; i < match.keys.length; i++) {
|
|
1467
|
+
params[match.keys[i]] = decodeURIComponent(m[i + 1]);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return params;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/ui/ssr-page.ts
|
|
1474
|
+
var DEFAULT_TEMPLATE = (p) => `<!DOCTYPE html>
|
|
1475
|
+
<html lang="zh-CN">
|
|
1476
|
+
<head>
|
|
1477
|
+
<meta charset="UTF-8">
|
|
1478
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1479
|
+
<title>${escapeHtml(p.title)}</title>
|
|
1480
|
+
${(p.styles ?? []).map((s) => `<link rel="stylesheet" href="${escapeHtml(s)}">`).join("\n ")}
|
|
1481
|
+
</head>
|
|
1482
|
+
<body>
|
|
1483
|
+
<div id="root">${p.html}</div>
|
|
1484
|
+
${p.dataScript}
|
|
1485
|
+
${p.bundle ? `<script src="${escapeHtml(p.bundle)}"></script>` : ""}
|
|
1486
|
+
</body>
|
|
1487
|
+
</html>`;
|
|
1488
|
+
function escapeHtml(s) {
|
|
1489
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1490
|
+
}
|
|
1491
|
+
function uiSsr(opts) {
|
|
1492
|
+
const flatRoutes = flattenRoutes(opts.routes);
|
|
1493
|
+
const template = opts.template ?? DEFAULT_TEMPLATE;
|
|
1494
|
+
return async (req, ctx, next) => {
|
|
1495
|
+
if (req.method !== "GET") return next(req, ctx);
|
|
1496
|
+
const url = new URL(req.url);
|
|
1497
|
+
const match = matchRoute(url.pathname, flatRoutes);
|
|
1498
|
+
if (!match) return next(req, ctx);
|
|
1499
|
+
const params = extractParams(url.pathname, match);
|
|
1500
|
+
const def = match.chain[match.chain.length - 1] ?? match.def;
|
|
1501
|
+
const pageCtx = Object.assign(Object.create(ctx), {
|
|
1502
|
+
params,
|
|
1503
|
+
route: { path: url.pathname, params, query: Object.fromEntries(url.searchParams) }
|
|
1504
|
+
});
|
|
1505
|
+
if (!def.component) return next(req, ctx);
|
|
1506
|
+
const data = /* @__PURE__ */ new Map();
|
|
1507
|
+
const html = await ssrToString(def.component, {}, pageCtx, { data });
|
|
1508
|
+
const title = opts.title ? opts.title({ params, def }) : def.title || "weifuwu";
|
|
1509
|
+
const dataScript = new HtmlSafe(serializeData(data)).toString();
|
|
1510
|
+
const body = template({ html: html.toString(), dataScript, title, bundle: opts.bundle, styles: opts.styles });
|
|
1511
|
+
return new Response(body, {
|
|
1512
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1513
|
+
});
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1220
1517
|
// src/db/postgres/connection.ts
|
|
1221
1518
|
import net from "node:net";
|
|
1222
1519
|
import crypto2 from "node:crypto";
|
|
@@ -1270,23 +1567,61 @@ function parseMessage(name, sql, paramTypes = []) {
|
|
|
1270
1567
|
return encodeMessage("P", concat(body, types));
|
|
1271
1568
|
}
|
|
1272
1569
|
function bindMessage(statement, params, paramFormats = []) {
|
|
1273
|
-
const
|
|
1274
|
-
|
|
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;
|
|
1275
1594
|
for (const f of paramFormats) {
|
|
1276
|
-
body
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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];
|
|
1280
1604
|
if (p === null) {
|
|
1281
|
-
body
|
|
1605
|
+
body[off] = body[off + 1] = body[off + 2] = body[off + 3] = 255;
|
|
1606
|
+
off += 4;
|
|
1282
1607
|
} else {
|
|
1283
|
-
const
|
|
1284
|
-
body
|
|
1285
|
-
|
|
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;
|
|
1286
1617
|
}
|
|
1287
1618
|
}
|
|
1288
|
-
body
|
|
1289
|
-
|
|
1619
|
+
body[off] = 0;
|
|
1620
|
+
body[off + 1] = 0;
|
|
1621
|
+
return encodeMessage("B", body);
|
|
1622
|
+
}
|
|
1623
|
+
function utf8Len(s) {
|
|
1624
|
+
return Buffer.byteLength(s);
|
|
1290
1625
|
}
|
|
1291
1626
|
function executeMessage(portal = "", maxRows = 0) {
|
|
1292
1627
|
const body = new Uint8Array(portal.length + 1 + 4);
|
|
@@ -1378,7 +1713,7 @@ function parseRowDescription(payload) {
|
|
|
1378
1713
|
for (let c = 0; c < count; c++) {
|
|
1379
1714
|
let j = i;
|
|
1380
1715
|
while (payload[j] !== 0) j++;
|
|
1381
|
-
const name =
|
|
1716
|
+
const name = _decoder.decode(payload.subarray(i, j));
|
|
1382
1717
|
i = j + 1;
|
|
1383
1718
|
i += 6;
|
|
1384
1719
|
const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
@@ -1400,7 +1735,7 @@ function parseDataRow(payload) {
|
|
|
1400
1735
|
if (len === -1) {
|
|
1401
1736
|
values.push(null);
|
|
1402
1737
|
} else {
|
|
1403
|
-
values.push(
|
|
1738
|
+
values.push(_decoder.decode(payload.subarray(i, i + len)));
|
|
1404
1739
|
i += len;
|
|
1405
1740
|
}
|
|
1406
1741
|
}
|
|
@@ -1413,7 +1748,7 @@ function parseErrorFields(payload) {
|
|
|
1413
1748
|
const type = String.fromCharCode(payload[i]);
|
|
1414
1749
|
let j = i + 1;
|
|
1415
1750
|
while (j < payload.length && payload[j] !== 0) j++;
|
|
1416
|
-
const value =
|
|
1751
|
+
const value = _decoder.decode(payload.subarray(i + 1, j));
|
|
1417
1752
|
switch (type) {
|
|
1418
1753
|
case "S":
|
|
1419
1754
|
out.severity = value;
|
|
@@ -1438,8 +1773,10 @@ function parseErrorFields(payload) {
|
|
|
1438
1773
|
}
|
|
1439
1774
|
return out;
|
|
1440
1775
|
}
|
|
1776
|
+
var _decoder = new TextDecoder();
|
|
1777
|
+
var _encoder = new TextEncoder();
|
|
1441
1778
|
function utf8(s) {
|
|
1442
|
-
return
|
|
1779
|
+
return _encoder.encode(s);
|
|
1443
1780
|
}
|
|
1444
1781
|
function concat(...parts) {
|
|
1445
1782
|
let total = 0;
|
|
@@ -1492,7 +1829,7 @@ var ValidationError = class extends DbError {
|
|
|
1492
1829
|
};
|
|
1493
1830
|
|
|
1494
1831
|
// src/db/postgres/connection.ts
|
|
1495
|
-
var PgConnection = class {
|
|
1832
|
+
var PgConnection = class _PgConnection {
|
|
1496
1833
|
opts;
|
|
1497
1834
|
timeoutSet = false;
|
|
1498
1835
|
awaitingReady = false;
|
|
@@ -1526,11 +1863,7 @@ var PgConnection = class {
|
|
|
1526
1863
|
}, this.opts.connectTimeoutMs);
|
|
1527
1864
|
sock.once("connect", () => {
|
|
1528
1865
|
sock.setNoDelay(true);
|
|
1529
|
-
sock.write(
|
|
1530
|
-
Buffer.from(
|
|
1531
|
-
startupMessage({ user: this.opts.user, database: this.opts.database })
|
|
1532
|
-
)
|
|
1533
|
-
);
|
|
1866
|
+
sock.write(startupMessage({ user: this.opts.user, database: this.opts.database }));
|
|
1534
1867
|
});
|
|
1535
1868
|
sock.on("data", (chunk) => this.onData(chunk));
|
|
1536
1869
|
sock.on("error", (err) => {
|
|
@@ -1563,10 +1896,10 @@ var PgConnection = class {
|
|
|
1563
1896
|
onReady = null;
|
|
1564
1897
|
onAuthFail = null;
|
|
1565
1898
|
expectingAuth = true;
|
|
1566
|
-
authStage = "start";
|
|
1567
1899
|
authCtx = null;
|
|
1568
1900
|
prepared = /* @__PURE__ */ new Map();
|
|
1569
|
-
// sig → stmt +
|
|
1901
|
+
// sig → stmt + 列缓存(LRU)
|
|
1902
|
+
static PREPARED_MAX = 128;
|
|
1570
1903
|
stmtSeq = 0;
|
|
1571
1904
|
currentQuery = null;
|
|
1572
1905
|
onData(chunk) {
|
|
@@ -1588,7 +1921,6 @@ var PgConnection = class {
|
|
|
1588
1921
|
const outer = crypto2.createHash("md5").update(inner + Buffer.from(salt).toString("latin1")).digest("hex");
|
|
1589
1922
|
this.send(passwordMessage(`md5${outer}`));
|
|
1590
1923
|
} else if (code === 10) {
|
|
1591
|
-
this.authStage = "sasl-initial";
|
|
1592
1924
|
const nonce = crypto2.randomBytes(18).toString("base64url");
|
|
1593
1925
|
const clientFirstBare = `n=,r=${nonce}`;
|
|
1594
1926
|
this.authCtx = { clientNonce: nonce, clientFirstBare, serverFirst: "" };
|
|
@@ -1610,7 +1942,6 @@ var PgConnection = class {
|
|
|
1610
1942
|
}
|
|
1611
1943
|
this.authCtx.serverFirst = serverFirst;
|
|
1612
1944
|
const clientFinal = this.scramFinal(this.authCtx);
|
|
1613
|
-
this.authStage = "sasl-final";
|
|
1614
1945
|
this.send(encodeP(utf82(clientFinal)));
|
|
1615
1946
|
} else if (code === 12) {
|
|
1616
1947
|
const serverFinal = new TextDecoder().decode(msg.payload.subarray(4));
|
|
@@ -1637,9 +1968,7 @@ var PgConnection = class {
|
|
|
1637
1968
|
if (this.opts.statementTimeoutMs > 0 && !this.timeoutSet) {
|
|
1638
1969
|
this.timeoutSet = true;
|
|
1639
1970
|
this.awaitingReady = true;
|
|
1640
|
-
this.socket?.write(
|
|
1641
|
-
Buffer.from(queryMessage(`SET statement_timeout = ${this.opts.statementTimeoutMs}`))
|
|
1642
|
-
);
|
|
1971
|
+
this.socket?.write(queryMessage(`SET statement_timeout = ${this.opts.statementTimeoutMs}`));
|
|
1643
1972
|
return;
|
|
1644
1973
|
}
|
|
1645
1974
|
this.onReady?.();
|
|
@@ -1651,9 +1980,8 @@ var PgConnection = class {
|
|
|
1651
1980
|
case "T": {
|
|
1652
1981
|
if (this.currentQuery) {
|
|
1653
1982
|
this.currentQuery.columns = parseRowDescription(msg.payload);
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1656
|
-
);
|
|
1983
|
+
const sig = `${this.currentQuery.sql}|${this.currentQuery.params?.length ?? 0}`;
|
|
1984
|
+
const entry = this.getPrepared(sig);
|
|
1657
1985
|
if (entry) entry.columns = this.currentQuery.columns;
|
|
1658
1986
|
}
|
|
1659
1987
|
break;
|
|
@@ -1676,7 +2004,7 @@ var PgConnection = class {
|
|
|
1676
2004
|
if (this.currentQuery?.awaitingDescribe && this.currentQuery.sql !== void 0) {
|
|
1677
2005
|
this.currentQuery.awaitingDescribe = false;
|
|
1678
2006
|
if (this.currentQuery.prepKey && this.currentQuery.prepName) {
|
|
1679
|
-
this.
|
|
2007
|
+
this.setPrepared(this.currentQuery.prepKey, {
|
|
1680
2008
|
name: this.currentQuery.prepName,
|
|
1681
2009
|
columns: this.currentQuery.columns
|
|
1682
2010
|
});
|
|
@@ -1760,6 +2088,24 @@ var PgConnection = class {
|
|
|
1760
2088
|
const authMessage = `${ctx.clientFirstBare},${ctx.serverFirst},${clientFinalWithoutProof}`;
|
|
1761
2089
|
return Buffer.from(hmac(serverKey, authMessage)).toString("base64");
|
|
1762
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
|
+
}
|
|
1763
2109
|
/** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
|
|
1764
2110
|
async transaction(fn) {
|
|
1765
2111
|
await this.query("BEGIN");
|
|
@@ -1791,10 +2137,10 @@ var PgConnection = class {
|
|
|
1791
2137
|
resolve: resolve3,
|
|
1792
2138
|
reject
|
|
1793
2139
|
};
|
|
1794
|
-
this.socket.write(
|
|
2140
|
+
this.socket.write(queryMessage(sql));
|
|
1795
2141
|
} else {
|
|
1796
2142
|
const sig = `${sql}|${params.length}`;
|
|
1797
|
-
let stmtEntry = this.
|
|
2143
|
+
let stmtEntry = this.getPrepared(sig);
|
|
1798
2144
|
const encoded = encodeParams(params);
|
|
1799
2145
|
if (!stmtEntry) {
|
|
1800
2146
|
const name = `wf_s${++this.stmtSeq}`;
|
|
@@ -1809,9 +2155,9 @@ var PgConnection = class {
|
|
|
1809
2155
|
prepKey: sig,
|
|
1810
2156
|
prepName: name
|
|
1811
2157
|
};
|
|
1812
|
-
this.socket.write(
|
|
1813
|
-
this.socket.write(
|
|
1814
|
-
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());
|
|
1815
2161
|
} else {
|
|
1816
2162
|
this.currentQuery = {
|
|
1817
2163
|
columns: [...stmtEntry.columns],
|
|
@@ -1822,15 +2168,15 @@ var PgConnection = class {
|
|
|
1822
2168
|
params: encoded,
|
|
1823
2169
|
awaitingDescribe: false
|
|
1824
2170
|
};
|
|
1825
|
-
this.socket.write(
|
|
1826
|
-
this.socket.write(
|
|
1827
|
-
this.socket.write(
|
|
2171
|
+
this.socket.write(bindMessage(stmtEntry.name, encoded));
|
|
2172
|
+
this.socket.write(executeMessage());
|
|
2173
|
+
this.socket.write(syncMessage());
|
|
1828
2174
|
}
|
|
1829
2175
|
}
|
|
1830
2176
|
});
|
|
1831
2177
|
}
|
|
1832
2178
|
send(data) {
|
|
1833
|
-
this.socket?.write(
|
|
2179
|
+
this.socket?.write(data);
|
|
1834
2180
|
}
|
|
1835
2181
|
notifyIdle() {
|
|
1836
2182
|
const ws = this.waiters;
|
|
@@ -1846,7 +2192,7 @@ var PgConnection = class {
|
|
|
1846
2192
|
async close() {
|
|
1847
2193
|
const sock = this.socket;
|
|
1848
2194
|
if (this.status === "ready" && sock) {
|
|
1849
|
-
sock.write(
|
|
2195
|
+
sock.write(terminateMessage());
|
|
1850
2196
|
}
|
|
1851
2197
|
this.status = "closed";
|
|
1852
2198
|
if (sock) sock.destroy();
|
|
@@ -2318,19 +2664,56 @@ var IncompleteError = class extends Error {
|
|
|
2318
2664
|
this.name = "IncompleteError";
|
|
2319
2665
|
}
|
|
2320
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
|
+
}
|
|
2321
2672
|
function encodeCommand(args) {
|
|
2322
|
-
const
|
|
2323
|
-
|
|
2324
|
-
for (
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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;
|
|
2329
2680
|
}
|
|
2330
|
-
|
|
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;
|
|
2705
|
+
}
|
|
2706
|
+
return out;
|
|
2331
2707
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
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();
|
|
2334
2717
|
var RespParser = class {
|
|
2335
2718
|
buf = new Uint8Array(0);
|
|
2336
2719
|
off = 0;
|
|
@@ -2404,16 +2787,15 @@ var RespParser = class {
|
|
|
2404
2787
|
return new RespError(line);
|
|
2405
2788
|
}
|
|
2406
2789
|
case ":": {
|
|
2407
|
-
|
|
2408
|
-
return parseInt(line, 10);
|
|
2790
|
+
return this.readInt();
|
|
2409
2791
|
}
|
|
2410
2792
|
case "$": {
|
|
2411
|
-
const len =
|
|
2793
|
+
const len = this.readInt();
|
|
2412
2794
|
if (len === -1) return null;
|
|
2413
|
-
return this.
|
|
2795
|
+
return this.readBulkBytes(len);
|
|
2414
2796
|
}
|
|
2415
2797
|
case "*": {
|
|
2416
|
-
const count =
|
|
2798
|
+
const count = this.readInt();
|
|
2417
2799
|
if (count === -1) return null;
|
|
2418
2800
|
const items = [];
|
|
2419
2801
|
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
@@ -2423,25 +2805,50 @@ var RespParser = class {
|
|
|
2423
2805
|
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
2424
2806
|
}
|
|
2425
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
|
+
}
|
|
2426
2831
|
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
2427
2832
|
readLine() {
|
|
2428
2833
|
const idx = indexOfCRLF(this.buf, this.off);
|
|
2429
2834
|
if (idx === -1) throw new IncompleteError();
|
|
2430
|
-
const line =
|
|
2835
|
+
const line = _decoder2.decode(this.buf.subarray(this.off, idx));
|
|
2431
2836
|
this.off = idx + 2;
|
|
2432
2837
|
return line;
|
|
2433
2838
|
}
|
|
2434
|
-
/** 读 len 字节的 bulk 内容 + \r\n */
|
|
2435
|
-
|
|
2839
|
+
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
2840
|
+
readBulkBytes(len) {
|
|
2436
2841
|
if (this.buf.length - this.off < len + 2) throw new IncompleteError();
|
|
2437
|
-
const value =
|
|
2842
|
+
const value = this.buf.subarray(this.off, this.off + len);
|
|
2438
2843
|
this.off += len + 2;
|
|
2439
2844
|
return value;
|
|
2440
2845
|
}
|
|
2441
2846
|
};
|
|
2442
2847
|
function indexOfCRLF(buf, from = 0) {
|
|
2443
|
-
|
|
2444
|
-
|
|
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);
|
|
2445
2852
|
}
|
|
2446
2853
|
return -1;
|
|
2447
2854
|
}
|
|
@@ -2454,6 +2861,8 @@ var RedisConnection = class {
|
|
|
2454
2861
|
socket = null;
|
|
2455
2862
|
parser = new RespParser();
|
|
2456
2863
|
pending = [];
|
|
2864
|
+
/** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
|
|
2865
|
+
pendingHead = 0;
|
|
2457
2866
|
offlineQueue = [];
|
|
2458
2867
|
subs = /* @__PURE__ */ new Map();
|
|
2459
2868
|
psubs = /* @__PURE__ */ new Map();
|
|
@@ -2469,7 +2878,8 @@ var RedisConnection = class {
|
|
|
2469
2878
|
port: options.port ?? 6379,
|
|
2470
2879
|
retryDelayMs: options.retryDelayMs ?? 100,
|
|
2471
2880
|
maxRetries: options.maxRetries ?? 10,
|
|
2472
|
-
enableOfflineQueue: options.enableOfflineQueue ?? true
|
|
2881
|
+
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
2882
|
+
maxOfflineQueue: options.maxOfflineQueue ?? 5e3
|
|
2473
2883
|
};
|
|
2474
2884
|
}
|
|
2475
2885
|
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
@@ -2519,10 +2929,10 @@ var RedisConnection = class {
|
|
|
2519
2929
|
});
|
|
2520
2930
|
}
|
|
2521
2931
|
handleDisconnect(err) {
|
|
2522
|
-
const queue = this.pending;
|
|
2932
|
+
const queue = this.pending.slice(this.pendingHead);
|
|
2523
2933
|
this.pending = [];
|
|
2524
|
-
|
|
2525
|
-
for (const p of queue) p.reject(
|
|
2934
|
+
this.pendingHead = 0;
|
|
2935
|
+
for (const p of queue) p.reject(err);
|
|
2526
2936
|
if (this.closed || this.status === "closed") return;
|
|
2527
2937
|
this.retries++;
|
|
2528
2938
|
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
@@ -2540,38 +2950,52 @@ var RedisConnection = class {
|
|
|
2540
2950
|
onData(chunk) {
|
|
2541
2951
|
try {
|
|
2542
2952
|
const values = this.parser.pushAll(chunk);
|
|
2543
|
-
for (const
|
|
2544
|
-
|
|
2545
|
-
|
|
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));
|
|
2546
2957
|
continue;
|
|
2547
2958
|
}
|
|
2548
|
-
const p = this.pending.
|
|
2959
|
+
const p = this.pending[this.pendingHead++];
|
|
2549
2960
|
if (!p) break;
|
|
2550
|
-
if (
|
|
2551
|
-
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;
|
|
2552
2967
|
}
|
|
2553
2968
|
} catch (e) {
|
|
2554
|
-
const queue = this.pending;
|
|
2969
|
+
const queue = this.pending.slice(this.pendingHead);
|
|
2555
2970
|
this.pending = [];
|
|
2971
|
+
this.pendingHead = 0;
|
|
2556
2972
|
for (const q of queue) q.reject(e);
|
|
2557
2973
|
this.socket?.destroy();
|
|
2558
2974
|
}
|
|
2559
2975
|
}
|
|
2560
|
-
/**
|
|
2976
|
+
/**
|
|
2977
|
+
* 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
|
|
2978
|
+
* opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
|
|
2979
|
+
*/
|
|
2561
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;
|
|
2562
2983
|
if (this.status === "ready" && this.socket) {
|
|
2563
|
-
return this.sendNow(name, args);
|
|
2984
|
+
return this.sendNow(name, args, opts?.asBuffer);
|
|
2564
2985
|
}
|
|
2565
2986
|
if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
|
|
2566
2987
|
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
2567
2988
|
}
|
|
2989
|
+
if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
|
|
2990
|
+
return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
|
|
2991
|
+
}
|
|
2568
2992
|
return new Promise((resolve3, reject) => {
|
|
2569
|
-
this.offlineQueue.push({ name, args, resolve: resolve3, reject });
|
|
2993
|
+
this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
|
|
2570
2994
|
});
|
|
2571
2995
|
}
|
|
2572
|
-
sendNow(name, args) {
|
|
2996
|
+
sendNow(name, args, asBuffer) {
|
|
2573
2997
|
return new Promise((resolve3, reject) => {
|
|
2574
|
-
this.pending.push({ resolve: resolve3, reject });
|
|
2998
|
+
this.pending.push({ resolve: resolve3, reject, asBuffer });
|
|
2575
2999
|
this.socket.write(encodeCommand([name, ...args]));
|
|
2576
3000
|
});
|
|
2577
3001
|
}
|
|
@@ -2579,7 +3003,7 @@ var RedisConnection = class {
|
|
|
2579
3003
|
const queue = this.offlineQueue;
|
|
2580
3004
|
this.offlineQueue = [];
|
|
2581
3005
|
for (const q of queue) {
|
|
2582
|
-
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);
|
|
2583
3007
|
}
|
|
2584
3008
|
}
|
|
2585
3009
|
/** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
|
|
@@ -2637,15 +3061,16 @@ var RedisConnection = class {
|
|
|
2637
3061
|
clearTimeout(this.reconnectTimer);
|
|
2638
3062
|
this.reconnectTimer = null;
|
|
2639
3063
|
}
|
|
2640
|
-
const queue = this.pending;
|
|
3064
|
+
const queue = this.pending.slice(this.pendingHead);
|
|
2641
3065
|
this.pending = [];
|
|
3066
|
+
this.pendingHead = 0;
|
|
2642
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"));
|
|
2643
3071
|
const sock = this.socket;
|
|
2644
3072
|
this.socket = null;
|
|
2645
|
-
|
|
2646
|
-
sock.destroy();
|
|
2647
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
2648
|
-
}
|
|
3073
|
+
sock?.destroy();
|
|
2649
3074
|
}
|
|
2650
3075
|
get connected() {
|
|
2651
3076
|
return this.status === "ready";
|
|
@@ -2664,7 +3089,7 @@ var RedisClient = class _RedisClient {
|
|
|
2664
3089
|
await conn.connect();
|
|
2665
3090
|
return new _RedisClient(conn);
|
|
2666
3091
|
}
|
|
2667
|
-
/** 底层命令透传(RESP
|
|
3092
|
+
/** 底层命令透传(RESP 值);Buffer 参数字节原样发送 */
|
|
2668
3093
|
command(name, ...args) {
|
|
2669
3094
|
return this.conn.command(name, ...args);
|
|
2670
3095
|
}
|
|
@@ -2673,6 +3098,15 @@ var RedisClient = class _RedisClient {
|
|
|
2673
3098
|
const v = await this.conn.command("GET", key);
|
|
2674
3099
|
return v === null ? null : String(v);
|
|
2675
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
|
+
}
|
|
2676
3110
|
/**
|
|
2677
3111
|
* SET。ttl 秒可省略;传入即安全生效(内部转 SET key val EX ttl)。
|
|
2678
3112
|
*/
|
|
@@ -2809,6 +3243,11 @@ var RedisPool = class _RedisPool {
|
|
|
2809
3243
|
await this.ensure();
|
|
2810
3244
|
return this.next().get(this.k(key));
|
|
2811
3245
|
}
|
|
3246
|
+
/** 二进制安全读取(原始字节,不解码) */
|
|
3247
|
+
async getBuffer(key) {
|
|
3248
|
+
await this.ensure();
|
|
3249
|
+
return this.next().getBuffer(this.k(key));
|
|
3250
|
+
}
|
|
2812
3251
|
async set(key, value, ttl) {
|
|
2813
3252
|
await this.ensure();
|
|
2814
3253
|
return this.next().set(this.k(key), value, ttl);
|
|
@@ -2962,6 +3401,7 @@ export {
|
|
|
2962
3401
|
serverError,
|
|
2963
3402
|
tooManyRequests,
|
|
2964
3403
|
ui,
|
|
3404
|
+
uiSsr,
|
|
2965
3405
|
unauthorized,
|
|
2966
3406
|
unprocessable
|
|
2967
3407
|
};
|