weifuwu 0.56.0 → 0.57.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 +128 -1
- package/dist/core/router.d.ts +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/email/index.d.ts +72 -0
- package/dist/email/smtp.d.ts +34 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2759 -1847
- package/dist/middleware/rate-limit.d.ts +70 -0
- package/dist/queue/index.d.ts +90 -0
- package/dist/types.d.ts +2 -2
- package/dist/user/index.d.ts +97 -0
- package/dist/user/password.d.ts +8 -0
- package/dist/user/token.d.ts +15 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -93,7 +93,7 @@ function serve(router, options) {
|
|
|
93
93
|
const ws = router.websocketHandler();
|
|
94
94
|
const handler = router.handler();
|
|
95
95
|
const port = options?.port ?? 0;
|
|
96
|
-
const
|
|
96
|
+
const hostname2 = options?.hostname ?? "0.0.0.0";
|
|
97
97
|
const server = http.createServer(async (req, res) => {
|
|
98
98
|
const traceId = req.headers["x-trace-id"] || req.headers["traceparent"]?.split("-")[1] || crypto.randomUUID();
|
|
99
99
|
try {
|
|
@@ -102,16 +102,15 @@ function serve(router, options) {
|
|
|
102
102
|
const response = await handler(request, { params: {}, query });
|
|
103
103
|
await sendResponse(res, response, { traceId });
|
|
104
104
|
} catch (err) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
res.
|
|
105
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
106
|
+
if (e instanceof HttpError) {
|
|
107
|
+
res.writeHead(e.status, { "Content-Type": "application/json" });
|
|
108
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
console.error(`[serve] ${method} ${url}:`, err instanceof Error ? err.stack || err.message : err);
|
|
114
|
-
}
|
|
111
|
+
const url = req.url ?? "/";
|
|
112
|
+
const method = req.method ?? "GET";
|
|
113
|
+
console.error(`[serve] ${method} ${url}:`, e.stack || e.message);
|
|
115
114
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
116
115
|
res.end("Internal Server Error");
|
|
117
116
|
}
|
|
@@ -157,7 +156,7 @@ function serve(router, options) {
|
|
|
157
156
|
return 0;
|
|
158
157
|
},
|
|
159
158
|
get hostname() {
|
|
160
|
-
return
|
|
159
|
+
return hostname2;
|
|
161
160
|
}
|
|
162
161
|
};
|
|
163
162
|
}
|
|
@@ -175,7 +174,7 @@ function serve(router, options) {
|
|
|
175
174
|
_cachedPort = 0;
|
|
176
175
|
resolveReady();
|
|
177
176
|
});
|
|
178
|
-
server.listen(port,
|
|
177
|
+
server.listen(port, hostname2, () => {
|
|
179
178
|
const addr = server.address();
|
|
180
179
|
if (addr && typeof addr !== "string") {
|
|
181
180
|
_cachedPort = addr.port;
|
|
@@ -204,8 +203,8 @@ function serve(router, options) {
|
|
|
204
203
|
return _cachedPort;
|
|
205
204
|
},
|
|
206
205
|
get hostname() {
|
|
207
|
-
if (!server.listening) return
|
|
208
|
-
return _cachedHostname ||
|
|
206
|
+
if (!server.listening) return hostname2;
|
|
207
|
+
return _cachedHostname || hostname2;
|
|
209
208
|
}
|
|
210
209
|
};
|
|
211
210
|
}
|
|
@@ -812,8 +811,15 @@ var Router = class _Router {
|
|
|
812
811
|
}
|
|
813
812
|
async handleError(e, req, ctx) {
|
|
814
813
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
814
|
+
if (this.errorHandler) return this.errorHandler(err, req, ctx);
|
|
815
|
+
if (err instanceof HttpError) {
|
|
816
|
+
return new Response(JSON.stringify({ error: err.message }), {
|
|
817
|
+
status: err.status,
|
|
818
|
+
headers: { "Content-Type": "application/json" }
|
|
819
|
+
});
|
|
820
|
+
}
|
|
815
821
|
console.error(`[router] ${req.method} ${new URL(req.url).pathname}:`, err.stack || err.message || err);
|
|
816
|
-
return
|
|
822
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
817
823
|
}
|
|
818
824
|
// ── Private: Middleware chain ───────────────────────────────
|
|
819
825
|
async runChain(mws, finalHandler, req, ctx) {
|
|
@@ -1078,553 +1084,723 @@ var MIME_TYPES = {
|
|
|
1078
1084
|
".wav": "audio/wav"
|
|
1079
1085
|
};
|
|
1080
1086
|
|
|
1081
|
-
// src/
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1087
|
+
// src/middleware/rate-limit.ts
|
|
1088
|
+
var PREFIX = "rl:";
|
|
1089
|
+
function rateLimit(options) {
|
|
1090
|
+
const opts = {
|
|
1091
|
+
windowMs: options?.windowMs ?? 6e4,
|
|
1092
|
+
max: options?.max ?? 100,
|
|
1093
|
+
key: options?.key ?? ((req) => req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown"),
|
|
1094
|
+
algorithm: options?.algorithm ?? "fixed",
|
|
1095
|
+
store: options?.store ?? "redis",
|
|
1096
|
+
statusCode: options?.statusCode ?? 429,
|
|
1097
|
+
message: options?.message ?? "Too Many Requests",
|
|
1098
|
+
headers: options?.headers ?? true
|
|
1099
|
+
};
|
|
1100
|
+
if (opts.store === "memory" && opts.algorithm === "sliding") {
|
|
1101
|
+
throw new Error('rateLimit: algorithm "sliding" requires store "redis" (memory store \u4EC5\u652F\u6301 fixed)');
|
|
1102
|
+
}
|
|
1103
|
+
let redisPool = null;
|
|
1104
|
+
const memoryStore = /* @__PURE__ */ new Map();
|
|
1105
|
+
function requireRedis(ctx) {
|
|
1106
|
+
if (redisPool) return;
|
|
1107
|
+
const pool = ctx.redis;
|
|
1108
|
+
if (!pool) {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
'rateLimit: store "redis" requires redis() middleware first \u2014 app.use(redis()) \u5FC5\u987B\u5728 rateLimit \u4E4B\u524D\u6CE8\u518C'
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
redisPool = pool;
|
|
1114
|
+
}
|
|
1115
|
+
async function check(key, max, windowMs) {
|
|
1116
|
+
if (opts.store === "memory") {
|
|
1117
|
+
const now2 = Date.now();
|
|
1118
|
+
const state = memoryStore.get(key);
|
|
1119
|
+
if (!state || state.resetAt <= now2) {
|
|
1120
|
+
memoryStore.set(key, { count: 1, resetAt: now2 + windowMs });
|
|
1121
|
+
return { allowed: true, remaining: Math.max(0, max - 1), resetSeconds: Math.ceil(windowMs / 1e3) };
|
|
1122
|
+
}
|
|
1123
|
+
if (state.count >= max) {
|
|
1124
|
+
return { allowed: false, remaining: 0, resetSeconds: Math.ceil((state.resetAt - now2) / 1e3) };
|
|
1125
|
+
}
|
|
1126
|
+
state.count++;
|
|
1127
|
+
return { allowed: true, remaining: Math.max(0, max - state.count), resetSeconds: Math.ceil((state.resetAt - now2) / 1e3) };
|
|
1128
|
+
}
|
|
1129
|
+
if (opts.algorithm === "fixed") {
|
|
1130
|
+
const count2 = await redisPool.incr(PREFIX + key);
|
|
1131
|
+
if (count2 === 1) {
|
|
1132
|
+
await redisPool.expire(PREFIX + key, Math.ceil(windowMs / 1e3));
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
allowed: count2 <= max,
|
|
1136
|
+
remaining: Math.max(0, max - count2),
|
|
1137
|
+
resetSeconds: Math.max(1, Math.ceil(await redisPool.ttl(PREFIX + key)))
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
const fullKey = PREFIX + key;
|
|
1141
|
+
const now = Date.now();
|
|
1142
|
+
const min = now - windowMs;
|
|
1143
|
+
await redisPool.command("ZREMRANGEBYSCORE", fullKey, 0, min);
|
|
1144
|
+
const count = Number(await redisPool.command("ZCARD", fullKey));
|
|
1145
|
+
if (count >= max) {
|
|
1146
|
+
const ttl = Number(await redisPool.command("TTL", fullKey));
|
|
1147
|
+
return { allowed: false, remaining: 0, resetSeconds: Math.max(1, ttl) };
|
|
1148
|
+
}
|
|
1149
|
+
const added = Number(
|
|
1150
|
+
await redisPool.command("ZADD", fullKey, now, `${now}-${Math.random().toString(36).slice(2)}`)
|
|
1151
|
+
);
|
|
1152
|
+
if (added === 1) {
|
|
1153
|
+
await redisPool.command("EXPIRE", fullKey, Math.ceil(windowMs / 1e3) * 2);
|
|
1154
|
+
}
|
|
1155
|
+
return { allowed: true, remaining: Math.max(0, max - count - 1), resetSeconds: Math.ceil(windowMs / 1e3) };
|
|
1156
|
+
}
|
|
1157
|
+
function tooMany(resetSeconds) {
|
|
1158
|
+
const headers = { "Content-Type": "application/json" };
|
|
1159
|
+
if (opts.headers) {
|
|
1160
|
+
headers["Retry-After"] = String(resetSeconds);
|
|
1161
|
+
headers["RateLimit-Limit"] = String(opts.max);
|
|
1162
|
+
headers["RateLimit-Remaining"] = "0";
|
|
1163
|
+
headers["RateLimit-Reset"] = String(resetSeconds);
|
|
1164
|
+
}
|
|
1165
|
+
return new Response(JSON.stringify({ error: opts.message, retryAfter: resetSeconds }), {
|
|
1166
|
+
status: opts.statusCode,
|
|
1167
|
+
headers
|
|
1168
|
+
});
|
|
1095
1169
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1170
|
+
const mw = (async (req, ctx, next) => {
|
|
1171
|
+
if (opts.store === "redis") requireRedis(ctx);
|
|
1172
|
+
ctx.limit = async (name, manual) => {
|
|
1173
|
+
const key = `${PREFIX}${name}`;
|
|
1174
|
+
const max = manual?.max ?? opts.max;
|
|
1175
|
+
const windowMs = manual?.windowMs ?? opts.windowMs;
|
|
1176
|
+
const r2 = await check(key, max, windowMs);
|
|
1177
|
+
if (!r2.allowed) {
|
|
1178
|
+
const err = new HttpError(opts.message, opts.statusCode);
|
|
1179
|
+
err.retryAfter = r2.resetSeconds;
|
|
1180
|
+
throw err;
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
const r = await check(PREFIX + "global:" + opts.key(req), opts.max, opts.windowMs);
|
|
1184
|
+
if (!r.allowed) return tooMany(r.resetSeconds);
|
|
1185
|
+
const res = await next(req, ctx);
|
|
1186
|
+
if (opts.headers) {
|
|
1187
|
+
res.headers.set("RateLimit-Limit", String(opts.max));
|
|
1188
|
+
res.headers.set("RateLimit-Remaining", String(r.remaining));
|
|
1189
|
+
res.headers.set("RateLimit-Reset", String(r.resetSeconds));
|
|
1190
|
+
}
|
|
1191
|
+
return res;
|
|
1192
|
+
});
|
|
1193
|
+
mw.__meta = { injects: ["limit"], depends: opts.store === "redis" ? ["redis"] : [] };
|
|
1194
|
+
return mw;
|
|
1102
1195
|
}
|
|
1103
|
-
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
1104
|
-
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
1105
1196
|
|
|
1106
|
-
// src/
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1197
|
+
// src/email/smtp.ts
|
|
1198
|
+
import net from "node:net";
|
|
1199
|
+
import tls from "node:tls";
|
|
1200
|
+
function encodeWord(s) {
|
|
1201
|
+
if (/^[\x20-\x7e]*$/.test(s)) return s;
|
|
1202
|
+
return `=?UTF-8?B?${Buffer.from(s, "utf8").toString("base64")}?=`;
|
|
1203
|
+
}
|
|
1204
|
+
function buildMessage(msg) {
|
|
1205
|
+
const lines = [];
|
|
1206
|
+
lines.push(`From: ${msg.from}`);
|
|
1207
|
+
lines.push(`To: ${msg.to.join(", ")}`);
|
|
1208
|
+
lines.push(`Subject: ${encodeWord(msg.subject)}`);
|
|
1209
|
+
lines.push("MIME-Version: 1.0");
|
|
1210
|
+
lines.push(
|
|
1211
|
+
msg.html ? "Content-Type: text/html; charset=utf-8" : "Content-Type: text/plain; charset=utf-8"
|
|
1212
|
+
);
|
|
1213
|
+
lines.push("");
|
|
1214
|
+
lines.push((msg.html ?? msg.text ?? "").replace(/\r?\n/g, "\r\n"));
|
|
1215
|
+
return lines.join("\r\n");
|
|
1216
|
+
}
|
|
1217
|
+
function sendSmtp(cfg, msg) {
|
|
1218
|
+
return new Promise((resolve3, reject) => {
|
|
1219
|
+
const port = cfg.port ?? (cfg.secure ? 465 : 587);
|
|
1220
|
+
const timeoutMs = cfg.timeoutMs ?? 3e4;
|
|
1221
|
+
let socket = null;
|
|
1222
|
+
let buf = "";
|
|
1223
|
+
let pending = null;
|
|
1224
|
+
const timer = setTimeout(() => {
|
|
1225
|
+
fail(new Error(`smtp: timeout after ${timeoutMs}ms (${cfg.host}:${port})`));
|
|
1226
|
+
}, timeoutMs);
|
|
1227
|
+
function fail(err) {
|
|
1228
|
+
clearTimeout(timer);
|
|
1229
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
1230
|
+
reject(err);
|
|
1231
|
+
}
|
|
1232
|
+
function onData(chunk) {
|
|
1233
|
+
buf += chunk.toString("utf8");
|
|
1234
|
+
while (true) {
|
|
1235
|
+
const nl = buf.indexOf("\r\n");
|
|
1236
|
+
if (nl === -1) return;
|
|
1237
|
+
const line = buf.slice(0, nl);
|
|
1238
|
+
buf = buf.slice(nl + 2);
|
|
1239
|
+
if (!pending) continue;
|
|
1240
|
+
if (line.length >= 4 && line[3] === " ") {
|
|
1241
|
+
const p = pending;
|
|
1242
|
+
pending = null;
|
|
1243
|
+
p.resolve({ code: parseInt(line.slice(0, 3), 10), lines: p.lines });
|
|
1244
|
+
return;
|
|
1129
1245
|
}
|
|
1130
|
-
|
|
1246
|
+
pending.lines.push(line);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function exchange(cmd) {
|
|
1250
|
+
return new Promise((resolveResp, rejectResp) => {
|
|
1251
|
+
if (pending) {
|
|
1252
|
+
rejectResp(new Error("smtp: concurrent exchange"));
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
pending = {
|
|
1256
|
+
resolve: (r) => r.code >= 400 ? rejectResp(new Error(`smtp: ${cmd} \u2192 ${r.code} ${r.lines[0]}`)) : resolveResp(r),
|
|
1257
|
+
lines: []
|
|
1258
|
+
};
|
|
1259
|
+
socket?.write(cmd + "\r\n");
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
function upgradeTls() {
|
|
1263
|
+
return new Promise((resolveUp, rejectUp) => {
|
|
1264
|
+
const tlsSocket = tls.connect({
|
|
1265
|
+
socket,
|
|
1266
|
+
rejectUnauthorized: cfg.rejectUnauthorized ?? false
|
|
1267
|
+
});
|
|
1268
|
+
tlsSocket.once("secureConnect", () => {
|
|
1269
|
+
socket = tlsSocket;
|
|
1270
|
+
tlsSocket.on("data", onData);
|
|
1271
|
+
resolveUp();
|
|
1272
|
+
});
|
|
1273
|
+
tlsSocket.once("error", rejectUp);
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
const run = async () => {
|
|
1277
|
+
try {
|
|
1278
|
+
const raw = cfg.secure ? tls.connect(port, cfg.host, { rejectUnauthorized: cfg.rejectUnauthorized ?? false }) : net.connect(port, cfg.host);
|
|
1279
|
+
socket = raw;
|
|
1280
|
+
raw.on("data", onData);
|
|
1281
|
+
raw.on("error", (e) => fail(e));
|
|
1282
|
+
await new Promise((r) => raw.once("connect", r));
|
|
1283
|
+
const greeting = await new Promise((r) => {
|
|
1284
|
+
pending = { resolve: r, lines: [] };
|
|
1285
|
+
});
|
|
1286
|
+
if (greeting.code !== 220) throw new Error(`smtp: greeting ${greeting.code} ${greeting.lines[0]}`);
|
|
1287
|
+
const ehlo = await exchange(`EHLO ${cfg.host}`);
|
|
1288
|
+
const features = ehlo.lines.join("\n");
|
|
1289
|
+
if (!cfg.secure && /STARTTLS/i.test(features)) {
|
|
1290
|
+
try {
|
|
1291
|
+
const stls = await exchange("STARTTLS");
|
|
1292
|
+
if (stls.code === 220) {
|
|
1293
|
+
await upgradeTls();
|
|
1294
|
+
await exchange(`EHLO ${cfg.host}`);
|
|
1295
|
+
}
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
if (cfg.requireTls) throw e;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (cfg.user) {
|
|
1301
|
+
const plain = Buffer.from(`\0${cfg.user}\0${cfg.pass ?? ""}`, "utf8").toString("base64");
|
|
1302
|
+
const auth = await exchange(`AUTH PLAIN ${plain}`);
|
|
1303
|
+
if (auth.code !== 235 && auth.code !== 250 && auth.code !== 334) {
|
|
1304
|
+
throw new Error(`smtp: auth failed ${auth.code}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
await exchange(`MAIL FROM:<${msg.from}>`);
|
|
1308
|
+
for (const to of msg.to) {
|
|
1309
|
+
await exchange(`RCPT TO:<${to}>`);
|
|
1310
|
+
}
|
|
1311
|
+
const data = await exchange("DATA");
|
|
1312
|
+
if (data.code !== 354) throw new Error(`smtp: DATA rejected ${data.code}`);
|
|
1313
|
+
const body = buildMessage(msg);
|
|
1314
|
+
const stuffed = body.split("\r\n").map((l) => l.startsWith(".") ? "." + l : l).join("\r\n");
|
|
1315
|
+
await new Promise((r, j) => {
|
|
1316
|
+
socket?.write(stuffed + "\r\n.\r\n", (err) => err ? j(err) : r());
|
|
1317
|
+
});
|
|
1318
|
+
const done = await new Promise((r) => {
|
|
1319
|
+
pending = { resolve: r, lines: [] };
|
|
1320
|
+
});
|
|
1321
|
+
if (done.code !== 250) throw new Error(`smtp: message rejected ${done.code} ${done.lines[0]}`);
|
|
1322
|
+
await exchange("QUIT").catch(() => {
|
|
1323
|
+
});
|
|
1324
|
+
clearTimeout(timer);
|
|
1325
|
+
socket.end();
|
|
1326
|
+
resolve3();
|
|
1327
|
+
} catch (e) {
|
|
1328
|
+
fail(e instanceof Error ? e : new Error(String(e)));
|
|
1131
1329
|
}
|
|
1330
|
+
};
|
|
1331
|
+
run();
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/email/index.ts
|
|
1336
|
+
function resendAdapter(opts, from) {
|
|
1337
|
+
const apiKey = opts.apiKey ?? process.env.RESEND_API_KEY;
|
|
1338
|
+
if (!apiKey) {
|
|
1339
|
+
throw new Error('email: adapter "resend" requires resend.apiKey or RESEND_API_KEY');
|
|
1340
|
+
}
|
|
1341
|
+
const baseUrl = opts.baseUrl ?? "https://api.resend.com";
|
|
1342
|
+
return async (msg) => {
|
|
1343
|
+
const res = await fetch(`${baseUrl}/emails`, {
|
|
1344
|
+
method: "POST",
|
|
1345
|
+
headers: {
|
|
1346
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1347
|
+
"Content-Type": "application/json"
|
|
1348
|
+
},
|
|
1349
|
+
body: JSON.stringify({
|
|
1350
|
+
from: msg.from ?? from,
|
|
1351
|
+
to: Array.isArray(msg.to) ? msg.to : [msg.to],
|
|
1352
|
+
subject: msg.subject,
|
|
1353
|
+
text: msg.text,
|
|
1354
|
+
html: msg.html
|
|
1355
|
+
})
|
|
1132
1356
|
});
|
|
1133
|
-
|
|
1134
|
-
|
|
1357
|
+
const data = await res.json().catch(() => ({}));
|
|
1358
|
+
if (!res.ok) {
|
|
1359
|
+
const detail = data?.message;
|
|
1360
|
+
throw new HttpError(`email: resend ${res.status}${detail ? ` ${detail}` : ""}`, 502);
|
|
1361
|
+
}
|
|
1362
|
+
return { id: data?.id, accepted: true };
|
|
1135
1363
|
};
|
|
1136
|
-
|
|
1364
|
+
}
|
|
1365
|
+
function smtpAdapter(opts, from) {
|
|
1366
|
+
if (!opts.host) throw new Error('email: adapter "smtp" requires smtp.host');
|
|
1367
|
+
return async (msg) => {
|
|
1368
|
+
await sendSmtp(opts, {
|
|
1369
|
+
from: msg.from ?? from,
|
|
1370
|
+
to: Array.isArray(msg.to) ? msg.to : [msg.to],
|
|
1371
|
+
subject: msg.subject,
|
|
1372
|
+
text: msg.text,
|
|
1373
|
+
html: msg.html
|
|
1374
|
+
});
|
|
1375
|
+
return { accepted: true };
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function email(options) {
|
|
1379
|
+
if (!options.from) throw new Error("email: options.from is required");
|
|
1380
|
+
const adapter = typeof options.adapter === "function" ? options.adapter : options.adapter === "smtp" ? smtpAdapter(options.smtp, options.from) : resendAdapter(options.resend ?? {}, options.from);
|
|
1381
|
+
const mw = (async (req, ctx, next) => {
|
|
1382
|
+
ctx.email = {
|
|
1383
|
+
send: (msg) => adapter(msg)
|
|
1384
|
+
};
|
|
1385
|
+
return next(req, ctx);
|
|
1386
|
+
});
|
|
1387
|
+
mw.__meta = { injects: ["email"], depends: [] };
|
|
1388
|
+
return mw;
|
|
1137
1389
|
}
|
|
1138
1390
|
|
|
1139
|
-
// src/
|
|
1140
|
-
|
|
1141
|
-
|
|
1391
|
+
// src/user/password.ts
|
|
1392
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
1393
|
+
import { promisify } from "node:util";
|
|
1394
|
+
var scryptAsync = promisify(scrypt);
|
|
1395
|
+
var KEYLEN = 64;
|
|
1396
|
+
var PARAMS = { N: 16384, r: 8, p: 1 };
|
|
1397
|
+
async function hashPassword(password) {
|
|
1398
|
+
const salt = randomBytes(16);
|
|
1399
|
+
const hash = await scryptAsync(password, salt, KEYLEN, PARAMS);
|
|
1400
|
+
return `scrypt$${PARAMS.N}$${PARAMS.r}$${PARAMS.p}$${salt.toString("hex")}$${hash.toString("hex")}`;
|
|
1142
1401
|
}
|
|
1143
|
-
|
|
1144
|
-
"
|
|
1145
|
-
"
|
|
1146
|
-
|
|
1147
|
-
"
|
|
1148
|
-
"
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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 "";
|
|
1402
|
+
async function verifyPassword(password, stored) {
|
|
1403
|
+
const parts = stored.split("$");
|
|
1404
|
+
if (parts.length !== 6 || parts[0] !== "scrypt") return false;
|
|
1405
|
+
const [, N, r, p, saltHex, hashHex] = parts;
|
|
1406
|
+
const expected = Buffer.from(hashHex, "hex");
|
|
1407
|
+
const actual = await scryptAsync(password, Buffer.from(saltHex, "hex"), expected.length, {
|
|
1408
|
+
N: Number(N),
|
|
1409
|
+
r: Number(r),
|
|
1410
|
+
p: Number(p)
|
|
1411
|
+
});
|
|
1412
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
1166
1413
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1414
|
+
|
|
1415
|
+
// src/user/token.ts
|
|
1416
|
+
import { createHash, createHmac, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
1417
|
+
var b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
|
|
1418
|
+
function signToken(payload, secret, ttlSeconds) {
|
|
1419
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1420
|
+
const data = `${b64({ alg: "HS256", typ: "JWT" })}.${b64({ ...payload, iat: now, exp: now + ttlSeconds })}`;
|
|
1421
|
+
const sig = createHmac("sha256", secret).update(data).digest("base64url");
|
|
1422
|
+
return `${data}.${sig}`;
|
|
1423
|
+
}
|
|
1424
|
+
function verifyToken(token, secret) {
|
|
1425
|
+
const parts = token.split(".");
|
|
1426
|
+
if (parts.length !== 3) return null;
|
|
1427
|
+
const [h, p, s] = parts;
|
|
1428
|
+
const data = `${h}.${p}`;
|
|
1429
|
+
const expected = createHmac("sha256", secret).update(data).digest("base64url");
|
|
1430
|
+
const sigBuf = Buffer.from(s);
|
|
1431
|
+
const expBuf = Buffer.from(expected);
|
|
1432
|
+
if (sigBuf.length !== expBuf.length || !timingSafeEqual2(sigBuf, expBuf)) return null;
|
|
1433
|
+
try {
|
|
1434
|
+
const payload = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
|
|
1435
|
+
if (typeof payload.exp === "number" && payload.exp < Date.now() / 1e3) return null;
|
|
1436
|
+
return payload;
|
|
1437
|
+
} catch {
|
|
1438
|
+
return null;
|
|
1171
1439
|
}
|
|
1172
|
-
return "";
|
|
1173
1440
|
}
|
|
1174
|
-
function
|
|
1175
|
-
|
|
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;
|
|
1441
|
+
function generateRefreshToken() {
|
|
1442
|
+
return randomBytes2(32).toString("hex");
|
|
1212
1443
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1444
|
+
function hashRefreshToken(token) {
|
|
1445
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/response.ts
|
|
1449
|
+
function ok(data, init) {
|
|
1450
|
+
return Response.json(data, { status: 200, ...init });
|
|
1451
|
+
}
|
|
1452
|
+
function created(data, init) {
|
|
1453
|
+
return Response.json(data, { status: 201, ...init });
|
|
1454
|
+
}
|
|
1455
|
+
function noContent(init) {
|
|
1456
|
+
return new Response(null, { status: 204, ...init });
|
|
1457
|
+
}
|
|
1458
|
+
function badRequest(message) {
|
|
1459
|
+
return Response.json({ error: message ?? "Bad Request" }, { status: 400 });
|
|
1460
|
+
}
|
|
1461
|
+
function unauthorized(message) {
|
|
1462
|
+
return Response.json({ error: message ?? "Unauthorized" }, { status: 401 });
|
|
1463
|
+
}
|
|
1464
|
+
function forbidden(message) {
|
|
1465
|
+
return Response.json({ error: message ?? "Forbidden" }, { status: 403 });
|
|
1466
|
+
}
|
|
1467
|
+
function notFound(message) {
|
|
1468
|
+
return Response.json({ error: message ?? "Not Found" }, { status: 404 });
|
|
1469
|
+
}
|
|
1470
|
+
function conflict(message) {
|
|
1471
|
+
return Response.json({ error: message ?? "Conflict" }, { status: 409 });
|
|
1472
|
+
}
|
|
1473
|
+
function unprocessable(message) {
|
|
1474
|
+
return Response.json({ error: message ?? "Unprocessable Entity" }, { status: 422 });
|
|
1475
|
+
}
|
|
1476
|
+
function tooManyRequests(message) {
|
|
1477
|
+
return Response.json({ error: message ?? "Too Many Requests" }, { status: 429 });
|
|
1478
|
+
}
|
|
1479
|
+
function serverError(message) {
|
|
1480
|
+
return Response.json({ error: message ?? "Internal Server Error" }, { status: 500 });
|
|
1481
|
+
}
|
|
1482
|
+
function redirect(url, status = 302) {
|
|
1483
|
+
return new Response(null, { status, headers: { Location: url } });
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/user/index.ts
|
|
1487
|
+
var USERS_TABLE = "_weifuwu_users";
|
|
1488
|
+
var SESSIONS_TABLE = "_weifuwu_sessions";
|
|
1489
|
+
function normalizeEmail(email2) {
|
|
1490
|
+
return email2.trim().toLowerCase();
|
|
1491
|
+
}
|
|
1492
|
+
function userSystem(options) {
|
|
1493
|
+
const sql = options.sql;
|
|
1494
|
+
const secretRaw = options.secret ?? process.env.AUTH_SECRET;
|
|
1495
|
+
if (!secretRaw) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
"userSystem: secret is required \u2014 pass options.secret or set AUTH_SECRET (at least 32 chars)"
|
|
1498
|
+
);
|
|
1220
1499
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
)
|
|
1242
|
-
|
|
1243
|
-
|
|
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))}"`);
|
|
1500
|
+
if (secretRaw.length < 16) {
|
|
1501
|
+
throw new Error("userSystem: secret must be at least 16 characters");
|
|
1502
|
+
}
|
|
1503
|
+
const secret = secretRaw;
|
|
1504
|
+
const accessTtlSeconds = options.accessTtlSeconds ?? 3600;
|
|
1505
|
+
const refreshTtlDays = options.refreshTtlDays ?? 30;
|
|
1506
|
+
const prefix = options.prefix ?? "/api/auth";
|
|
1507
|
+
let currentUser = null;
|
|
1508
|
+
async function findUserById(id) {
|
|
1509
|
+
const rows = await sql.unsafe(
|
|
1510
|
+
`SELECT id, email, name, role, tenant FROM ${USERS_TABLE} WHERE id = $1`,
|
|
1511
|
+
[id]
|
|
1512
|
+
);
|
|
1513
|
+
return rows.length ? rows[0] : null;
|
|
1514
|
+
}
|
|
1515
|
+
async function issueSession(user) {
|
|
1516
|
+
const token = signToken({ sub: user.id }, secret, accessTtlSeconds);
|
|
1517
|
+
const refreshToken = generateRefreshToken();
|
|
1518
|
+
const expiresAt = new Date(Date.now() + refreshTtlDays * 24 * 3600 * 1e3);
|
|
1519
|
+
await sql.unsafe(
|
|
1520
|
+
`INSERT INTO ${SESSIONS_TABLE} (token_hash, user_id, expires_at) VALUES ($1, $2, $3)`,
|
|
1521
|
+
[hashRefreshToken(refreshToken), user.id, expiresAt]
|
|
1522
|
+
);
|
|
1523
|
+
return { token, refreshToken };
|
|
1272
1524
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
}
|
|
1294
|
-
function stringify(v) {
|
|
1295
|
-
if (v == null || v === false || v === true) return "";
|
|
1296
|
-
if (Array.isArray(v)) return v.map(stringify).join("");
|
|
1297
|
-
if (v instanceof HtmlSafe) return v.value;
|
|
1298
|
-
return escape2(String(v));
|
|
1299
|
-
}
|
|
1300
|
-
function unsafe(s) {
|
|
1301
|
-
return new HtmlSafe(s);
|
|
1302
|
-
}
|
|
1303
|
-
var jsCache = /* @__PURE__ */ new Map();
|
|
1304
|
-
async function jsCacheFresh(inputs) {
|
|
1305
|
-
for (const [file, mtime] of Object.entries(inputs)) {
|
|
1306
|
-
try {
|
|
1307
|
-
const st = await stat(file);
|
|
1308
|
-
if (st.mtimeMs !== mtime) return false;
|
|
1309
|
-
} catch {
|
|
1310
|
-
return false;
|
|
1525
|
+
async function consumeRefreshToken(refreshToken) {
|
|
1526
|
+
const rows = await sql.unsafe(
|
|
1527
|
+
`SELECT s.user_id, s.expires_at FROM ${SESSIONS_TABLE} s
|
|
1528
|
+
WHERE s.token_hash = $1 AND s.revoked_at IS NULL`,
|
|
1529
|
+
[hashRefreshToken(refreshToken)]
|
|
1530
|
+
);
|
|
1531
|
+
if (!rows.length) throw new HttpError("Invalid refresh token", 401);
|
|
1532
|
+
const row = rows[0];
|
|
1533
|
+
if (new Date(row.expires_at) < /* @__PURE__ */ new Date()) {
|
|
1534
|
+
throw new HttpError("Refresh token expired", 401);
|
|
1311
1535
|
}
|
|
1536
|
+
const user = await findUserById(row.user_id);
|
|
1537
|
+
if (!user) throw new HttpError("User not found", 401);
|
|
1538
|
+
return user;
|
|
1312
1539
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
await import("postcss");
|
|
1321
|
-
await import("@tailwindcss/postcss");
|
|
1322
|
-
postcssAvailable = true;
|
|
1323
|
-
} catch {
|
|
1324
|
-
postcssAvailable = false;
|
|
1325
|
-
}
|
|
1326
|
-
return postcssAvailable;
|
|
1327
|
-
}
|
|
1328
|
-
function resolveEntry(entryPath) {
|
|
1329
|
-
if (entryPath.startsWith(".") || entryPath.startsWith("/")) {
|
|
1330
|
-
return resolve2(entryPath);
|
|
1331
|
-
}
|
|
1332
|
-
try {
|
|
1333
|
-
return fileURLToPath(import.meta.resolve(entryPath));
|
|
1334
|
-
} catch {
|
|
1335
|
-
return resolve2(entryPath);
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
function ui() {
|
|
1339
|
-
return async (_req, ctx, next) => {
|
|
1340
|
-
function htmlTag(strings, ...values) {
|
|
1341
|
-
let body = "";
|
|
1342
|
-
for (let i = 0; i < strings.length; i++) {
|
|
1343
|
-
body += strings[i];
|
|
1344
|
-
if (i < values.length) body += stringify(values[i]);
|
|
1540
|
+
const mw = (async (req, ctx, next) => {
|
|
1541
|
+
currentUser = null;
|
|
1542
|
+
const authHeader = req.headers.get("authorization");
|
|
1543
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
1544
|
+
const payload = verifyToken(authHeader.slice(7), secret);
|
|
1545
|
+
if (payload?.sub) {
|
|
1546
|
+
currentUser = await findUserById(String(payload.sub));
|
|
1345
1547
|
}
|
|
1346
|
-
return new Response(body, {
|
|
1347
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1348
|
-
});
|
|
1349
1548
|
}
|
|
1350
|
-
ctx.
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1549
|
+
ctx.user = currentUser;
|
|
1550
|
+
ctx.auth = {
|
|
1551
|
+
async register(input) {
|
|
1552
|
+
if (!input.email || !input.password) throw new HttpError("email and password are required", 400);
|
|
1553
|
+
if (input.password.length < 8) throw new HttpError("password must be at least 8 characters", 400);
|
|
1554
|
+
const email2 = normalizeEmail(input.email);
|
|
1555
|
+
const passwordHash = await hashPassword(input.password);
|
|
1556
|
+
const rows = await sql.unsafe(
|
|
1557
|
+
`INSERT INTO ${USERS_TABLE} (email, password_hash, name, role, tenant)
|
|
1558
|
+
VALUES ($1, $2, $3, $4, $5) RETURNING id, email, name, role, tenant`,
|
|
1559
|
+
[email2, passwordHash, input.name ?? null, input.role ?? null, input.tenant ?? null]
|
|
1560
|
+
);
|
|
1561
|
+
const user = rows[0];
|
|
1562
|
+
const session = await issueSession(user);
|
|
1563
|
+
return { ...session, user };
|
|
1356
1564
|
},
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1565
|
+
async login(email2, password) {
|
|
1566
|
+
const rows = await sql.unsafe(
|
|
1567
|
+
`SELECT id, email, password_hash, name, role, tenant FROM ${USERS_TABLE} WHERE email = $1`,
|
|
1568
|
+
[normalizeEmail(email2)]
|
|
1569
|
+
);
|
|
1570
|
+
const row = rows[0];
|
|
1571
|
+
if (!row) throw new HttpError("Invalid email or password", 401);
|
|
1572
|
+
const valid = await verifyPassword(password, String(row.password_hash));
|
|
1573
|
+
if (!valid) throw new HttpError("Invalid email or password", 401);
|
|
1574
|
+
const user = {
|
|
1575
|
+
id: String(row.id),
|
|
1576
|
+
email: String(row.email),
|
|
1577
|
+
name: row.name,
|
|
1578
|
+
role: row.role,
|
|
1579
|
+
tenant: row.tenant
|
|
1580
|
+
};
|
|
1581
|
+
const session = await issueSession(user);
|
|
1582
|
+
return { ...session, user };
|
|
1360
1583
|
},
|
|
1361
|
-
async
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
return new Response(cached.code, {
|
|
1366
|
-
headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
const result = await build({
|
|
1370
|
-
entryPoints: [absPath],
|
|
1371
|
-
bundle: true,
|
|
1372
|
-
format: "esm",
|
|
1373
|
-
platform: "browser",
|
|
1374
|
-
jsx: "automatic",
|
|
1375
|
-
jsxImportSource: "weifuwu/client",
|
|
1376
|
-
write: false,
|
|
1377
|
-
metafile: true
|
|
1378
|
-
});
|
|
1379
|
-
const code = result.outputFiles[0].text;
|
|
1380
|
-
const inputs = {};
|
|
1381
|
-
for (const file of Object.keys(result.metafile?.inputs ?? {})) {
|
|
1382
|
-
const abs = resolve2(file);
|
|
1383
|
-
try {
|
|
1384
|
-
inputs[abs] = (await stat(abs)).mtimeMs;
|
|
1385
|
-
} catch {
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
jsCache.set(absPath, { code, inputs });
|
|
1389
|
-
return new Response(code, {
|
|
1390
|
-
headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
|
|
1391
|
-
});
|
|
1584
|
+
async logout(refreshToken) {
|
|
1585
|
+
await sql.unsafe(`UPDATE ${SESSIONS_TABLE} SET revoked_at = now() WHERE token_hash = $1`, [
|
|
1586
|
+
hashRefreshToken(refreshToken)
|
|
1587
|
+
]);
|
|
1392
1588
|
},
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
const plugin = tw.default || tw;
|
|
1408
|
-
const instance = typeof plugin === "function" ? plugin() : plugin;
|
|
1409
|
-
const result = await postcss.default([instance]).process(code, { from: absPath });
|
|
1410
|
-
code = result.css;
|
|
1411
|
-
} catch (e) {
|
|
1412
|
-
throw new Error(`PostCSS \u7F16\u8BD1\u5931\u8D25 (${absPath}): ${e.message}`, { cause: e });
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
cssCache.set(absPath, { code, mtime: st.mtimeMs });
|
|
1416
|
-
return new Response(code, {
|
|
1417
|
-
headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
|
|
1418
|
-
});
|
|
1589
|
+
requireAuth() {
|
|
1590
|
+
if (!currentUser) throw new HttpError("Unauthorized", 401);
|
|
1591
|
+
return currentUser;
|
|
1592
|
+
},
|
|
1593
|
+
async setPassword(userId, newPassword) {
|
|
1594
|
+
if (newPassword.length < 8) throw new HttpError("password must be at least 8 characters", 400);
|
|
1595
|
+
const passwordHash = await hashPassword(newPassword);
|
|
1596
|
+
await sql.unsafe(`UPDATE ${USERS_TABLE} SET password_hash = $1 WHERE id = $2`, [
|
|
1597
|
+
passwordHash,
|
|
1598
|
+
userId
|
|
1599
|
+
]);
|
|
1600
|
+
},
|
|
1601
|
+
createToken(type, payload, opts) {
|
|
1602
|
+
return signToken({ type, ...payload }, secret, opts.ttlSeconds);
|
|
1419
1603
|
}
|
|
1420
1604
|
};
|
|
1421
|
-
return next(
|
|
1605
|
+
return next(req, ctx);
|
|
1606
|
+
});
|
|
1607
|
+
mw.__meta = { injects: ["user", "auth"], depends: ["sql"] };
|
|
1608
|
+
mw.migrate = async () => {
|
|
1609
|
+
await sql.unsafe(`
|
|
1610
|
+
CREATE TABLE IF NOT EXISTS ${USERS_TABLE} (
|
|
1611
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1612
|
+
email TEXT NOT NULL UNIQUE,
|
|
1613
|
+
password_hash TEXT NOT NULL,
|
|
1614
|
+
name TEXT,
|
|
1615
|
+
role TEXT,
|
|
1616
|
+
tenant TEXT,
|
|
1617
|
+
email_verified BOOLEAN NOT NULL DEFAULT false,
|
|
1618
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
1619
|
+
)
|
|
1620
|
+
`);
|
|
1621
|
+
await sql.unsafe(`
|
|
1622
|
+
CREATE TABLE IF NOT EXISTS ${SESSIONS_TABLE} (
|
|
1623
|
+
token_hash TEXT PRIMARY KEY,
|
|
1624
|
+
user_id UUID NOT NULL REFERENCES ${USERS_TABLE}(id) ON DELETE CASCADE,
|
|
1625
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
1626
|
+
revoked_at TIMESTAMPTZ,
|
|
1627
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
1628
|
+
)
|
|
1629
|
+
`);
|
|
1630
|
+
};
|
|
1631
|
+
mw.routes = (app, routeOpts) => {
|
|
1632
|
+
const p = routeOpts?.prefix ?? prefix;
|
|
1633
|
+
app.post(`${p}/register`, async (req, ctx) => {
|
|
1634
|
+
const body = await req.json().catch(() => ({}));
|
|
1635
|
+
const result = await ctx.auth.register(body);
|
|
1636
|
+
return created(result);
|
|
1637
|
+
});
|
|
1638
|
+
app.post(`${p}/login`, async (req, ctx) => {
|
|
1639
|
+
const body = await req.json().catch(() => ({}));
|
|
1640
|
+
if (!body.email || !body.password) return badRequest("email and password are required");
|
|
1641
|
+
const result = await ctx.auth.login(body.email, body.password);
|
|
1642
|
+
return ok(result);
|
|
1643
|
+
});
|
|
1644
|
+
app.post(`${p}/logout`, async (req, ctx) => {
|
|
1645
|
+
const body = await req.json().catch(() => ({}));
|
|
1646
|
+
if (!body.refreshToken) return badRequest("refreshToken is required");
|
|
1647
|
+
await ctx.auth.logout(body.refreshToken);
|
|
1648
|
+
return noContent();
|
|
1649
|
+
});
|
|
1650
|
+
app.post(`${p}/refresh`, async (req, ctx) => {
|
|
1651
|
+
const body = await req.json().catch(() => ({}));
|
|
1652
|
+
if (!body.refreshToken) throw new HttpError("refreshToken is required", 400);
|
|
1653
|
+
const user = await consumeRefreshToken(body.refreshToken);
|
|
1654
|
+
await ctx.auth.logout(body.refreshToken);
|
|
1655
|
+
const session = await issueSession(user);
|
|
1656
|
+
return ok({ ...session, user });
|
|
1657
|
+
});
|
|
1658
|
+
app.get(`${p}/me`, async (_req, ctx) => {
|
|
1659
|
+
if (!ctx.user) throw new HttpError("Unauthorized", 401);
|
|
1660
|
+
return ok(ctx.user);
|
|
1661
|
+
});
|
|
1422
1662
|
};
|
|
1663
|
+
return mw;
|
|
1423
1664
|
}
|
|
1424
1665
|
|
|
1425
|
-
// src/
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
-
}
|
|
1666
|
+
// src/queue/index.ts
|
|
1667
|
+
import { randomUUID } from "node:crypto";
|
|
1668
|
+
import { hostname } from "node:os";
|
|
1472
1669
|
|
|
1473
|
-
// src/
|
|
1474
|
-
|
|
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
|
-
}
|
|
1670
|
+
// src/db/redis/connection.ts
|
|
1671
|
+
import net2 from "node:net";
|
|
1516
1672
|
|
|
1517
|
-
// src/db/
|
|
1518
|
-
|
|
1519
|
-
|
|
1673
|
+
// src/db/errors.ts
|
|
1674
|
+
var DbError = class extends Error {
|
|
1675
|
+
kind;
|
|
1676
|
+
code;
|
|
1677
|
+
cause;
|
|
1678
|
+
constructor(kind, message, options) {
|
|
1679
|
+
super(message);
|
|
1680
|
+
this.name = "DbError";
|
|
1681
|
+
this.kind = kind;
|
|
1682
|
+
this.code = options?.code;
|
|
1683
|
+
this.cause = options?.cause;
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
var ConnectionError = class extends DbError {
|
|
1687
|
+
attempts;
|
|
1688
|
+
constructor(message, attempts = 1, cause) {
|
|
1689
|
+
super("connection", message, { cause });
|
|
1690
|
+
this.name = "ConnectionError";
|
|
1691
|
+
this.attempts = attempts;
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
var TimeoutError = class extends DbError {
|
|
1695
|
+
operation;
|
|
1696
|
+
ms;
|
|
1697
|
+
constructor(operation, ms) {
|
|
1698
|
+
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
1699
|
+
this.name = "TimeoutError";
|
|
1700
|
+
this.operation = operation;
|
|
1701
|
+
this.ms = ms;
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
var ValidationError = class extends DbError {
|
|
1705
|
+
constructor(message) {
|
|
1706
|
+
super("validation", message);
|
|
1707
|
+
this.name = "ValidationError";
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1520
1710
|
|
|
1521
|
-
// src/db/
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
out[1] = len >> 24 & 255;
|
|
1527
|
-
out[2] = len >> 16 & 255;
|
|
1528
|
-
out[3] = len >> 8 & 255;
|
|
1529
|
-
out[4] = len & 255;
|
|
1530
|
-
out.set(payload, 5);
|
|
1531
|
-
return out;
|
|
1532
|
-
}
|
|
1533
|
-
function startupMessage(params) {
|
|
1534
|
-
const body = [];
|
|
1535
|
-
body.push(0, 3, 0, 0);
|
|
1536
|
-
for (const [k, v] of Object.entries(params)) {
|
|
1537
|
-
for (const c of utf8(k)) body.push(c);
|
|
1538
|
-
body.push(0);
|
|
1539
|
-
for (const c of utf8(v)) body.push(c);
|
|
1540
|
-
body.push(0);
|
|
1711
|
+
// src/db/redis/resp.ts
|
|
1712
|
+
var RespError = class extends DbError {
|
|
1713
|
+
constructor(message) {
|
|
1714
|
+
super("protocol", message, { code: "RESP" });
|
|
1715
|
+
this.name = "RespError";
|
|
1541
1716
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
return
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
return encodeMessage("Q", concat(utf8(sql), new Uint8Array([0])));
|
|
1554
|
-
}
|
|
1555
|
-
function parseMessage(name, sql, paramTypes = []) {
|
|
1556
|
-
const body = concat(utf8(name), new Uint8Array([0]), utf8(sql), new Uint8Array([0]));
|
|
1557
|
-
const types = new Uint8Array(2 + paramTypes.length * 4);
|
|
1558
|
-
types[0] = paramTypes.length >> 8 & 255;
|
|
1559
|
-
types[1] = paramTypes.length & 255;
|
|
1560
|
-
paramTypes.forEach((oid, i) => {
|
|
1561
|
-
const off = 2 + i * 4;
|
|
1562
|
-
types[off] = oid >> 24 & 255;
|
|
1563
|
-
types[off + 1] = oid >> 16 & 255;
|
|
1564
|
-
types[off + 2] = oid >> 8 & 255;
|
|
1565
|
-
types[off + 3] = oid & 255;
|
|
1566
|
-
});
|
|
1567
|
-
return encodeMessage("P", concat(body, types));
|
|
1717
|
+
};
|
|
1718
|
+
var IncompleteError = class extends Error {
|
|
1719
|
+
constructor() {
|
|
1720
|
+
super("incomplete RESP message");
|
|
1721
|
+
this.name = "IncompleteError";
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
function decodeValue(v, decodeBytes) {
|
|
1725
|
+
if (v instanceof Uint8Array) return decodeBytes ? _decoder.decode(v) : v;
|
|
1726
|
+
if (Array.isArray(v)) return v.map((x) => decodeValue(x, decodeBytes));
|
|
1727
|
+
return v;
|
|
1568
1728
|
}
|
|
1569
|
-
function
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
for (
|
|
1573
|
-
|
|
1729
|
+
function encodeCommand(args) {
|
|
1730
|
+
const lens = new Array(args.length);
|
|
1731
|
+
let total = headerLen(args.length);
|
|
1732
|
+
for (let i = 0; i < args.length; i++) {
|
|
1733
|
+
const arg = args[i];
|
|
1734
|
+
const len = typeof arg === "string" ? Buffer.byteLength(arg) : arg instanceof Buffer ? arg.length : String(arg).length;
|
|
1735
|
+
lens[i] = len;
|
|
1736
|
+
total += headerLen(len) + len + 2;
|
|
1574
1737
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1738
|
+
const out = new Uint8Array(total);
|
|
1739
|
+
let off = 0;
|
|
1740
|
+
const head = `*${args.length}\r
|
|
1741
|
+
`;
|
|
1742
|
+
out.set(_encoder.encode(head), 0);
|
|
1743
|
+
off = head.length;
|
|
1744
|
+
for (let i = 0; i < args.length; i++) {
|
|
1745
|
+
const arg = args[i];
|
|
1746
|
+
const len = lens[i];
|
|
1747
|
+
const lh = `$${len}\r
|
|
1748
|
+
`;
|
|
1749
|
+
out.set(_encoder.encode(lh), off);
|
|
1750
|
+
off += lh.length;
|
|
1751
|
+
if (typeof arg === "string") {
|
|
1752
|
+
out.set(_encoder.encode(arg), off);
|
|
1753
|
+
} else if (arg instanceof Buffer) {
|
|
1754
|
+
out.set(arg, off);
|
|
1579
1755
|
} else {
|
|
1580
|
-
|
|
1581
|
-
body.push(bytes.length >> 24 & 255, bytes.length >> 16 & 255, bytes.length >> 8 & 255, bytes.length & 255);
|
|
1582
|
-
for (const b of bytes) body.push(b);
|
|
1756
|
+
out.set(_encoder.encode(String(arg)), off);
|
|
1583
1757
|
}
|
|
1758
|
+
off += len;
|
|
1759
|
+
out[off] = 13;
|
|
1760
|
+
out[off + 1] = 10;
|
|
1761
|
+
off += 2;
|
|
1584
1762
|
}
|
|
1585
|
-
|
|
1586
|
-
return encodeMessage("B", new Uint8Array(body));
|
|
1587
|
-
}
|
|
1588
|
-
function executeMessage(portal = "", maxRows = 0) {
|
|
1589
|
-
const body = new Uint8Array(portal.length + 1 + 4);
|
|
1590
|
-
body.set(utf8(portal), 0);
|
|
1591
|
-
body[portal.length] = 0;
|
|
1592
|
-
const off = portal.length + 1;
|
|
1593
|
-
body[off] = maxRows >> 24 & 255;
|
|
1594
|
-
body[off + 1] = maxRows >> 16 & 255;
|
|
1595
|
-
body[off + 2] = maxRows >> 8 & 255;
|
|
1596
|
-
body[off + 3] = maxRows & 255;
|
|
1597
|
-
return encodeMessage("E", body);
|
|
1598
|
-
}
|
|
1599
|
-
function syncMessage() {
|
|
1600
|
-
return encodeMessage("S", new Uint8Array(0));
|
|
1601
|
-
}
|
|
1602
|
-
function flushMessage() {
|
|
1603
|
-
return encodeMessage("H", new Uint8Array(0));
|
|
1604
|
-
}
|
|
1605
|
-
function describeMessage(kind, name = "") {
|
|
1606
|
-
return encodeMessage("D", concat(utf8(kind), utf8(name), new Uint8Array([0])));
|
|
1607
|
-
}
|
|
1608
|
-
function terminateMessage() {
|
|
1609
|
-
return encodeMessage("X", new Uint8Array(0));
|
|
1763
|
+
return out;
|
|
1610
1764
|
}
|
|
1611
|
-
function
|
|
1612
|
-
|
|
1765
|
+
function headerLen(n) {
|
|
1766
|
+
if (n < 10) return 4;
|
|
1767
|
+
if (n < 100) return 5;
|
|
1768
|
+
if (n < 1e3) return 6;
|
|
1769
|
+
if (n < 1e4) return 7;
|
|
1770
|
+
return 8;
|
|
1613
1771
|
}
|
|
1614
|
-
var
|
|
1772
|
+
var _decoder = new TextDecoder();
|
|
1773
|
+
var _encoder = new TextEncoder();
|
|
1774
|
+
var RespParser = class {
|
|
1615
1775
|
buf = new Uint8Array(0);
|
|
1616
1776
|
off = 0;
|
|
1777
|
+
/** 喂入数据分片。返回 { value, incomplete };incomplete=true 时需继续喂。 */
|
|
1617
1778
|
push(chunk) {
|
|
1779
|
+
this.append(chunk);
|
|
1780
|
+
const saved = this.off;
|
|
1781
|
+
try {
|
|
1782
|
+
const value = this.parseValue();
|
|
1783
|
+
this.compact();
|
|
1784
|
+
return { value, incomplete: false };
|
|
1785
|
+
} catch (e) {
|
|
1786
|
+
this.off = saved;
|
|
1787
|
+
if (e instanceof IncompleteError) return { value: null, incomplete: true };
|
|
1788
|
+
throw e;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
/** 喂入分片并解析 buffer 中所有完整响应(连接层:一次 data 事件可能含多个回复) */
|
|
1792
|
+
pushAll(chunk) {
|
|
1618
1793
|
this.append(chunk);
|
|
1619
1794
|
const out = [];
|
|
1620
1795
|
while (true) {
|
|
1621
1796
|
const saved = this.off;
|
|
1622
|
-
|
|
1623
|
-
|
|
1797
|
+
try {
|
|
1798
|
+
out.push(this.parseValue());
|
|
1799
|
+
} catch (e) {
|
|
1624
1800
|
this.off = saved;
|
|
1625
|
-
break;
|
|
1801
|
+
if (e instanceof IncompleteError) break;
|
|
1802
|
+
throw e;
|
|
1626
1803
|
}
|
|
1627
|
-
out.push(msg);
|
|
1628
1804
|
}
|
|
1629
1805
|
if (out.length > 0) this.compact();
|
|
1630
1806
|
return out;
|
|
@@ -1644,6 +1820,7 @@ var MessageStream = class {
|
|
|
1644
1820
|
this.buf = merged;
|
|
1645
1821
|
this.off = 0;
|
|
1646
1822
|
}
|
|
1823
|
+
/** 全部消费后压缩(释放底层) */
|
|
1647
1824
|
compact() {
|
|
1648
1825
|
if (this.off === this.buf.length) {
|
|
1649
1826
|
this.buf = new Uint8Array(0);
|
|
@@ -1653,1011 +1830,1339 @@ var MessageStream = class {
|
|
|
1653
1830
|
this.off = 0;
|
|
1654
1831
|
}
|
|
1655
1832
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
if (this.buf.length - this.off < 5) return null;
|
|
1659
|
-
const len = this.buf[this.off + 1] << 24 | this.buf[this.off + 2] << 16 | this.buf[this.off + 3] << 8 | this.buf[this.off + 4];
|
|
1660
|
-
if (this.buf.length - this.off < 1 + len) return null;
|
|
1833
|
+
parseValue() {
|
|
1834
|
+
if (this.buf.length - this.off < 3) throw new IncompleteError();
|
|
1661
1835
|
const type = String.fromCharCode(this.buf[this.off]);
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1836
|
+
this.off++;
|
|
1837
|
+
switch (type) {
|
|
1838
|
+
case "+": {
|
|
1839
|
+
const line = this.readLine();
|
|
1840
|
+
return line;
|
|
1841
|
+
}
|
|
1842
|
+
case "-": {
|
|
1843
|
+
const line = this.readLine();
|
|
1844
|
+
return new RespError(line);
|
|
1845
|
+
}
|
|
1846
|
+
case ":": {
|
|
1847
|
+
return this.readInt();
|
|
1848
|
+
}
|
|
1849
|
+
case "$": {
|
|
1850
|
+
const len = this.readInt();
|
|
1851
|
+
if (len === -1) return null;
|
|
1852
|
+
return this.readBulkBytes(len);
|
|
1853
|
+
}
|
|
1854
|
+
case "*": {
|
|
1855
|
+
const count = this.readInt();
|
|
1856
|
+
if (count === -1) return null;
|
|
1857
|
+
const items = [];
|
|
1858
|
+
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
1859
|
+
return items;
|
|
1860
|
+
}
|
|
1861
|
+
default:
|
|
1862
|
+
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* 读整数(: 或 $ 长度):扫描数字字符边算值,直到 \r\n(手动解析,免 parseInt + 字符串)。
|
|
1867
|
+
* 支持负号(-1)。未读到终止符抛 IncompleteError(push 回滚)。
|
|
1868
|
+
*/
|
|
1869
|
+
readInt() {
|
|
1870
|
+
const buf = this.buf;
|
|
1871
|
+
let i = this.off;
|
|
1872
|
+
let neg = false;
|
|
1873
|
+
if (i < buf.length && buf[i] === 45) {
|
|
1874
|
+
neg = true;
|
|
1875
|
+
i++;
|
|
1876
|
+
}
|
|
1877
|
+
let n = 0;
|
|
1878
|
+
while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) {
|
|
1879
|
+
n = n * 10 + (buf[i] - 48);
|
|
1880
|
+
i++;
|
|
1881
|
+
}
|
|
1882
|
+
if (i + 1 < buf.length && buf[i] === 13 && buf[i + 1] === 10) {
|
|
1883
|
+
this.off = i + 2;
|
|
1884
|
+
return neg ? -n : n;
|
|
1885
|
+
}
|
|
1886
|
+
throw new IncompleteError();
|
|
1887
|
+
}
|
|
1888
|
+
/** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
|
|
1889
|
+
readLine() {
|
|
1890
|
+
const idx = indexOfCRLF(this.buf, this.off);
|
|
1891
|
+
if (idx === -1) throw new IncompleteError();
|
|
1892
|
+
const line = _decoder.decode(this.buf.subarray(this.off, idx));
|
|
1893
|
+
this.off = idx + 2;
|
|
1894
|
+
return line;
|
|
1895
|
+
}
|
|
1896
|
+
/** 读 len 字节的 bulk 内容 + \r\n(字节中立——不 decode,由调用方决定 string/Buffer) */
|
|
1897
|
+
readBulkBytes(len) {
|
|
1898
|
+
if (this.buf.length - this.off < len + 2) throw new IncompleteError();
|
|
1899
|
+
const value = this.buf.subarray(this.off, this.off + len);
|
|
1900
|
+
this.off += len + 2;
|
|
1901
|
+
return value;
|
|
1665
1902
|
}
|
|
1666
1903
|
};
|
|
1667
|
-
function
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
const count = payload[0] << 8 | payload[1];
|
|
1673
|
-
const cols = [];
|
|
1674
|
-
let i = 2;
|
|
1675
|
-
for (let c = 0; c < count; c++) {
|
|
1676
|
-
let j = i;
|
|
1677
|
-
while (payload[j] !== 0) j++;
|
|
1678
|
-
const name = new TextDecoder().decode(payload.subarray(i, j));
|
|
1679
|
-
i = j + 1;
|
|
1680
|
-
i += 6;
|
|
1681
|
-
const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
1682
|
-
i += 4;
|
|
1683
|
-
const typeLen = payload[i] << 8 | payload[i + 1];
|
|
1684
|
-
i += 2;
|
|
1685
|
-
i += 6;
|
|
1686
|
-
cols.push({ name, typeOid, typeLen });
|
|
1687
|
-
}
|
|
1688
|
-
return cols;
|
|
1689
|
-
}
|
|
1690
|
-
function parseDataRow(payload) {
|
|
1691
|
-
const count = payload[0] << 8 | payload[1];
|
|
1692
|
-
const values = [];
|
|
1693
|
-
let i = 2;
|
|
1694
|
-
for (let c = 0; c < count; c++) {
|
|
1695
|
-
const len = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
1696
|
-
i += 4;
|
|
1697
|
-
if (len === -1) {
|
|
1698
|
-
values.push(null);
|
|
1699
|
-
} else {
|
|
1700
|
-
values.push(new TextDecoder().decode(payload.subarray(i, i + len)));
|
|
1701
|
-
i += len;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
return values;
|
|
1705
|
-
}
|
|
1706
|
-
function parseErrorFields(payload) {
|
|
1707
|
-
const out = {};
|
|
1708
|
-
let i = 0;
|
|
1709
|
-
while (i < payload.length && payload[i] !== 0) {
|
|
1710
|
-
const type = String.fromCharCode(payload[i]);
|
|
1711
|
-
let j = i + 1;
|
|
1712
|
-
while (j < payload.length && payload[j] !== 0) j++;
|
|
1713
|
-
const value = new TextDecoder().decode(payload.subarray(i + 1, j));
|
|
1714
|
-
switch (type) {
|
|
1715
|
-
case "S":
|
|
1716
|
-
out.severity = value;
|
|
1717
|
-
break;
|
|
1718
|
-
case "C":
|
|
1719
|
-
out.code = value;
|
|
1720
|
-
break;
|
|
1721
|
-
case "M":
|
|
1722
|
-
out.message = value;
|
|
1723
|
-
break;
|
|
1724
|
-
case "D":
|
|
1725
|
-
out.detail = value;
|
|
1726
|
-
break;
|
|
1727
|
-
case "H":
|
|
1728
|
-
out.hint = value;
|
|
1729
|
-
break;
|
|
1730
|
-
case "P":
|
|
1731
|
-
out.position = value;
|
|
1732
|
-
break;
|
|
1733
|
-
}
|
|
1734
|
-
i = j + 1;
|
|
1735
|
-
}
|
|
1736
|
-
return out;
|
|
1737
|
-
}
|
|
1738
|
-
function utf8(s) {
|
|
1739
|
-
return new TextEncoder().encode(s);
|
|
1740
|
-
}
|
|
1741
|
-
function concat(...parts) {
|
|
1742
|
-
let total = 0;
|
|
1743
|
-
for (const p of parts) total += p.length;
|
|
1744
|
-
const out = new Uint8Array(total);
|
|
1745
|
-
let off = 0;
|
|
1746
|
-
for (const p of parts) {
|
|
1747
|
-
out.set(p, off);
|
|
1748
|
-
off += p.length;
|
|
1904
|
+
function indexOfCRLF(buf, from = 0) {
|
|
1905
|
+
let i = buf.indexOf(13, from);
|
|
1906
|
+
while (i !== -1) {
|
|
1907
|
+
if (i + 1 < buf.length && buf[i + 1] === 10) return i;
|
|
1908
|
+
i = buf.indexOf(13, i + 1);
|
|
1749
1909
|
}
|
|
1750
|
-
return
|
|
1910
|
+
return -1;
|
|
1751
1911
|
}
|
|
1752
1912
|
|
|
1753
|
-
// src/db/
|
|
1754
|
-
var
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
cause;
|
|
1758
|
-
constructor(kind, message, options) {
|
|
1759
|
-
super(message);
|
|
1760
|
-
this.name = "DbError";
|
|
1761
|
-
this.kind = kind;
|
|
1762
|
-
this.code = options?.code;
|
|
1763
|
-
this.cause = options?.cause;
|
|
1764
|
-
}
|
|
1765
|
-
};
|
|
1766
|
-
var ConnectionError = class extends DbError {
|
|
1767
|
-
attempts;
|
|
1768
|
-
constructor(message, attempts = 1, cause) {
|
|
1769
|
-
super("connection", message, { cause });
|
|
1770
|
-
this.name = "ConnectionError";
|
|
1771
|
-
this.attempts = attempts;
|
|
1772
|
-
}
|
|
1773
|
-
};
|
|
1774
|
-
var TimeoutError = class extends DbError {
|
|
1775
|
-
operation;
|
|
1776
|
-
ms;
|
|
1777
|
-
constructor(operation, ms) {
|
|
1778
|
-
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
1779
|
-
this.name = "TimeoutError";
|
|
1780
|
-
this.operation = operation;
|
|
1781
|
-
this.ms = ms;
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
1784
|
-
var ValidationError = class extends DbError {
|
|
1785
|
-
constructor(message) {
|
|
1786
|
-
super("validation", message);
|
|
1787
|
-
this.name = "ValidationError";
|
|
1788
|
-
}
|
|
1789
|
-
};
|
|
1790
|
-
|
|
1791
|
-
// src/db/postgres/connection.ts
|
|
1792
|
-
var PgConnection = class {
|
|
1913
|
+
// src/db/redis/connection.ts
|
|
1914
|
+
var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
|
|
1915
|
+
var RedisConnection = class {
|
|
1916
|
+
ready = false;
|
|
1793
1917
|
opts;
|
|
1794
|
-
timeoutSet = false;
|
|
1795
|
-
awaitingReady = false;
|
|
1796
|
-
pendingErrorZ = false;
|
|
1797
1918
|
socket = null;
|
|
1798
|
-
|
|
1919
|
+
parser = new RespParser();
|
|
1920
|
+
pending = [];
|
|
1921
|
+
/** pending 头指针(避免 shift() O(n)——消费后定期 compact) */
|
|
1922
|
+
pendingHead = 0;
|
|
1923
|
+
offlineQueue = [];
|
|
1924
|
+
subs = /* @__PURE__ */ new Map();
|
|
1925
|
+
psubs = /* @__PURE__ */ new Map();
|
|
1799
1926
|
status = "idle";
|
|
1800
|
-
|
|
1927
|
+
retries = 0;
|
|
1928
|
+
reconnectTimer = null;
|
|
1929
|
+
connectPromise = null;
|
|
1930
|
+
closed = false;
|
|
1931
|
+
connectedOnce = false;
|
|
1801
1932
|
constructor(options = {}) {
|
|
1802
1933
|
this.opts = {
|
|
1803
1934
|
host: options.host ?? "127.0.0.1",
|
|
1804
|
-
port: options.port ??
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
password: options.password
|
|
1935
|
+
port: options.port ?? 6379,
|
|
1936
|
+
retryDelayMs: options.retryDelayMs ?? 100,
|
|
1937
|
+
maxRetries: options.maxRetries ?? 10,
|
|
1938
|
+
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
1939
|
+
maxOfflineQueue: options.maxOfflineQueue ?? 5e3
|
|
1810
1940
|
};
|
|
1811
1941
|
}
|
|
1812
|
-
|
|
1813
|
-
return this.status === "ready";
|
|
1814
|
-
}
|
|
1942
|
+
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
1815
1943
|
connect() {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
this.
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
sock.write(
|
|
1827
|
-
Buffer.from(
|
|
1828
|
-
startupMessage({ user: this.opts.user, database: this.opts.database })
|
|
1829
|
-
)
|
|
1830
|
-
);
|
|
1831
|
-
});
|
|
1832
|
-
sock.on("data", (chunk) => this.onData(chunk));
|
|
1833
|
-
sock.on("error", (err) => {
|
|
1834
|
-
clearTimeout(timeout);
|
|
1835
|
-
if (this.status === "connecting") {
|
|
1836
|
-
reject(new ConnectionError(`postgres: ${err.message}`));
|
|
1837
|
-
}
|
|
1838
|
-
});
|
|
1839
|
-
sock.on("close", () => {
|
|
1840
|
-
clearTimeout(timeout);
|
|
1841
|
-
this.socket = null;
|
|
1842
|
-
if (this.status !== "closed") {
|
|
1843
|
-
this.status = "closed";
|
|
1844
|
-
const q = this.currentQuery;
|
|
1845
|
-
this.currentQuery = null;
|
|
1846
|
-
if (q) q.reject(new ConnectionError("postgres: connection closed"));
|
|
1847
|
-
}
|
|
1848
|
-
});
|
|
1849
|
-
this.onReady = () => {
|
|
1850
|
-
clearTimeout(timeout);
|
|
1851
|
-
this.status = "ready";
|
|
1852
|
-
resolve3();
|
|
1853
|
-
};
|
|
1854
|
-
this.onAuthFail = (err) => {
|
|
1855
|
-
clearTimeout(timeout);
|
|
1856
|
-
reject(err);
|
|
1857
|
-
};
|
|
1944
|
+
if (this.connectPromise) return this.connectPromise;
|
|
1945
|
+
this.closed = false;
|
|
1946
|
+
this.connectPromise = new Promise((resolve3, reject) => {
|
|
1947
|
+
this.openSocket();
|
|
1948
|
+
this.onceReady = () => resolve3();
|
|
1949
|
+
this.onceFailed = (err) => reject(err);
|
|
1950
|
+
}).finally(() => {
|
|
1951
|
+
this.connectPromise = null;
|
|
1952
|
+
this.onceReady = void 0;
|
|
1953
|
+
this.onceFailed = void 0;
|
|
1858
1954
|
});
|
|
1955
|
+
return this.connectPromise;
|
|
1859
1956
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
this.
|
|
1957
|
+
onceReady;
|
|
1958
|
+
onceFailed;
|
|
1959
|
+
openSocket() {
|
|
1960
|
+
this.status = "connecting";
|
|
1961
|
+
const sock = net2.connect(this.opts.port, this.opts.host);
|
|
1962
|
+
this.socket = sock;
|
|
1963
|
+
sock.on("connect", () => {
|
|
1964
|
+
sock.setNoDelay(true);
|
|
1965
|
+
this.status = "ready";
|
|
1966
|
+
this.retries = 0;
|
|
1967
|
+
this.flushOffline();
|
|
1968
|
+
this.onceReady?.();
|
|
1969
|
+
if (this.connectedOnce) {
|
|
1970
|
+
for (const ch of this.subs.keys()) this.sendNow("SUBSCRIBE", [ch]);
|
|
1971
|
+
for (const pat of this.psubs.keys()) this.sendNow("PSUBSCRIBE", [pat]);
|
|
1972
|
+
}
|
|
1973
|
+
this.connectedOnce = true;
|
|
1974
|
+
});
|
|
1975
|
+
sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
|
|
1976
|
+
sock.on("error", (err) => {
|
|
1977
|
+
if (this.status === "connecting") {
|
|
1978
|
+
this.handleDisconnect(err);
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
sock.on("close", () => {
|
|
1982
|
+
this.socket = null;
|
|
1983
|
+
if (this.status !== "closed" && this.status !== "idle") {
|
|
1984
|
+
this.handleDisconnect(new ConnectionError("redis: socket closed"));
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
handleDisconnect(err) {
|
|
1989
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
1990
|
+
this.pending = [];
|
|
1991
|
+
this.pendingHead = 0;
|
|
1992
|
+
for (const p of queue2) p.reject(err);
|
|
1993
|
+
if (this.closed || this.status === "closed") return;
|
|
1994
|
+
this.retries++;
|
|
1995
|
+
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
1996
|
+
this.status = "closed";
|
|
1997
|
+
const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
|
|
1998
|
+
this.onceFailed?.(failErr);
|
|
1999
|
+
return;
|
|
1873
2000
|
}
|
|
2001
|
+
const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
|
|
2002
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2003
|
+
this.reconnectTimer = null;
|
|
2004
|
+
this.openSocket();
|
|
2005
|
+
}, delay);
|
|
1874
2006
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
this.
|
|
1882
|
-
|
|
1883
|
-
const salt = msg.payload.subarray(8, 12);
|
|
1884
|
-
const inner = crypto2.createHash("md5").update(this.opts.password + this.opts.user).digest("hex");
|
|
1885
|
-
const outer = crypto2.createHash("md5").update(inner + Buffer.from(salt).toString("latin1")).digest("hex");
|
|
1886
|
-
this.send(passwordMessage(`md5${outer}`));
|
|
1887
|
-
} else if (code === 10) {
|
|
1888
|
-
this.authStage = "sasl-initial";
|
|
1889
|
-
const nonce = crypto2.randomBytes(18).toString("base64url");
|
|
1890
|
-
const clientFirstBare = `n=,r=${nonce}`;
|
|
1891
|
-
this.authCtx = { clientNonce: nonce, clientFirstBare, serverFirst: "" };
|
|
1892
|
-
const mech = utf82("SCRAM-SHA-256\0");
|
|
1893
|
-
const resp = utf82(`n,,${clientFirstBare}`);
|
|
1894
|
-
const payload = new Uint8Array(mech.length + 4 + resp.length);
|
|
1895
|
-
payload.set(mech, 0);
|
|
1896
|
-
payload[mech.length] = resp.length >> 24 & 255;
|
|
1897
|
-
payload[mech.length + 1] = resp.length >> 16 & 255;
|
|
1898
|
-
payload[mech.length + 2] = resp.length >> 8 & 255;
|
|
1899
|
-
payload[mech.length + 3] = resp.length & 255;
|
|
1900
|
-
payload.set(resp, mech.length + 4);
|
|
1901
|
-
this.send(encodeP(payload));
|
|
1902
|
-
} else if (code === 11) {
|
|
1903
|
-
const serverFirst = new TextDecoder().decode(msg.payload.subarray(4));
|
|
1904
|
-
if (!this.authCtx) {
|
|
1905
|
-
this.onAuthFail?.(new ConnectionError("postgres: unexpected SASL continue"));
|
|
1906
|
-
return;
|
|
1907
|
-
}
|
|
1908
|
-
this.authCtx.serverFirst = serverFirst;
|
|
1909
|
-
const clientFinal = this.scramFinal(this.authCtx);
|
|
1910
|
-
this.authStage = "sasl-final";
|
|
1911
|
-
this.send(encodeP(utf82(clientFinal)));
|
|
1912
|
-
} else if (code === 12) {
|
|
1913
|
-
const serverFinal = new TextDecoder().decode(msg.payload.subarray(4));
|
|
1914
|
-
const vMatch = serverFinal.match(/v=([A-Za-z0-9+/=]+)/);
|
|
1915
|
-
if (vMatch && this.authCtx) {
|
|
1916
|
-
const expected = this.scramServerSignature(this.authCtx);
|
|
1917
|
-
if (vMatch[1] !== expected) {
|
|
1918
|
-
this.onAuthFail?.(new ConnectionError("postgres: SCRAM server signature mismatch"));
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
2007
|
+
onData(chunk) {
|
|
2008
|
+
try {
|
|
2009
|
+
const values = this.parser.pushAll(chunk);
|
|
2010
|
+
for (const raw of values) {
|
|
2011
|
+
const first = Array.isArray(raw) && raw.length > 0 ? decodeValue(raw[0], true) : void 0;
|
|
2012
|
+
if (typeof first === "string" && PUSH_TYPES.has(first)) {
|
|
2013
|
+
this.dispatchSubscribe(decodeValue(raw, true));
|
|
2014
|
+
continue;
|
|
1922
2015
|
}
|
|
1923
|
-
|
|
2016
|
+
const p = this.pending[this.pendingHead++];
|
|
2017
|
+
if (!p) break;
|
|
2018
|
+
if (raw instanceof RespError) p.reject(raw);
|
|
2019
|
+
else p.resolve(decodeValue(raw, !p.asBuffer));
|
|
1924
2020
|
}
|
|
1925
|
-
if (
|
|
1926
|
-
|
|
1927
|
-
this.
|
|
1928
|
-
this.onAuthFail?.(new ConnectionError(`postgres: auth failed: ${fields.message ?? "unknown"}`));
|
|
1929
|
-
this.socket?.destroy();
|
|
1930
|
-
return;
|
|
2021
|
+
if (this.pendingHead > 64 && this.pendingHead * 2 > this.pending.length) {
|
|
2022
|
+
this.pending = this.pending.slice(this.pendingHead);
|
|
2023
|
+
this.pendingHead = 0;
|
|
1931
2024
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
Buffer.from(queryMessage(`SET statement_timeout = ${this.opts.statementTimeoutMs}`))
|
|
1939
|
-
);
|
|
1940
|
-
return;
|
|
1941
|
-
}
|
|
1942
|
-
this.onReady?.();
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
return;
|
|
2025
|
+
} catch (e) {
|
|
2026
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
2027
|
+
this.pending = [];
|
|
2028
|
+
this.pendingHead = 0;
|
|
2029
|
+
for (const q of queue2) q.reject(e);
|
|
2030
|
+
this.socket?.destroy();
|
|
1946
2031
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。
|
|
2035
|
+
* opts.asBuffer=true:响应保留原始字节(Uint8Array)——getBuffer 等二进制场景。
|
|
2036
|
+
*/
|
|
2037
|
+
command(name, ...args) {
|
|
2038
|
+
const last = args[args.length - 1];
|
|
2039
|
+
const opts = typeof last === "object" && last !== null && !(last instanceof Uint8Array) ? args.pop() : void 0;
|
|
2040
|
+
if (this.status === "ready" && this.socket) {
|
|
2041
|
+
return this.sendNow(name, args, opts?.asBuffer);
|
|
2042
|
+
}
|
|
2043
|
+
if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
|
|
2044
|
+
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
2045
|
+
}
|
|
2046
|
+
if (this.offlineQueue.length >= this.opts.maxOfflineQueue) {
|
|
2047
|
+
return Promise.reject(new ConnectionError(`redis: offline queue full (${this.opts.maxOfflineQueue})`));
|
|
2048
|
+
}
|
|
2049
|
+
return new Promise((resolve3, reject) => {
|
|
2050
|
+
this.offlineQueue.push({ name, args, resolve: resolve3, reject, asBuffer: opts?.asBuffer });
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
sendNow(name, args, asBuffer) {
|
|
2054
|
+
return new Promise((resolve3, reject) => {
|
|
2055
|
+
this.pending.push({ resolve: resolve3, reject, asBuffer });
|
|
2056
|
+
this.socket.write(encodeCommand([name, ...args]));
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
flushOffline() {
|
|
2060
|
+
const queue2 = this.offlineQueue;
|
|
2061
|
+
this.offlineQueue = [];
|
|
2062
|
+
for (const q of queue2) {
|
|
2063
|
+
this.sendNow(q.name, q.args, q.asBuffer).then(q.resolve, q.reject);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
|
|
2067
|
+
batch(payload, count) {
|
|
2068
|
+
if (this.status !== "ready" || !this.socket) {
|
|
2069
|
+
return Promise.reject(new ConnectionError("redis: not connected"));
|
|
2070
|
+
}
|
|
2071
|
+
return new Promise((resolve3, reject) => {
|
|
2072
|
+
const results = [];
|
|
2073
|
+
const maybeResolve = () => {
|
|
2074
|
+
if (results.length === count) resolve3(results);
|
|
2075
|
+
};
|
|
2076
|
+
for (let i = 0; i < count; i++) {
|
|
2077
|
+
this.pending.push({
|
|
2078
|
+
// 正常响应与错误响应(RespError)都作为结果值收集——管道语义
|
|
2079
|
+
resolve: (v) => {
|
|
2080
|
+
results.push(v);
|
|
2081
|
+
maybeResolve();
|
|
2082
|
+
},
|
|
2083
|
+
reject: (e) => {
|
|
2084
|
+
results.push(e);
|
|
2085
|
+
maybeResolve();
|
|
1980
2086
|
}
|
|
1981
|
-
|
|
1982
|
-
this.send(executeMessage());
|
|
1983
|
-
this.send(syncMessage());
|
|
1984
|
-
}
|
|
1985
|
-
break;
|
|
1986
|
-
}
|
|
1987
|
-
case "Z": {
|
|
1988
|
-
if (this.pendingErrorZ) {
|
|
1989
|
-
this.pendingErrorZ = false;
|
|
1990
|
-
break;
|
|
1991
|
-
}
|
|
1992
|
-
const q = this.currentQuery;
|
|
1993
|
-
this.currentQuery = null;
|
|
1994
|
-
if (q) q.resolve(q.rows);
|
|
1995
|
-
if (this.awaitingReady) {
|
|
1996
|
-
this.awaitingReady = false;
|
|
1997
|
-
this.onReady?.();
|
|
1998
|
-
}
|
|
1999
|
-
this.notifyIdle();
|
|
2000
|
-
break;
|
|
2001
|
-
}
|
|
2002
|
-
case "E": {
|
|
2003
|
-
const fields = parseErrorFields(msg.payload);
|
|
2004
|
-
const q = this.currentQuery;
|
|
2005
|
-
this.currentQuery = null;
|
|
2006
|
-
if (q) {
|
|
2007
|
-
this.pendingErrorZ = true;
|
|
2008
|
-
if (q.awaitingDescribe) this.send(syncMessage());
|
|
2009
|
-
const err = new Error(fields.message ?? "postgres query error");
|
|
2010
|
-
err.code = fields.code;
|
|
2011
|
-
q.reject(err);
|
|
2012
|
-
}
|
|
2013
|
-
break;
|
|
2087
|
+
});
|
|
2014
2088
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2089
|
+
this.socket.write(payload);
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
/** 订阅频道:回调式(channel, message) */
|
|
2093
|
+
async subscribe(channel, fn) {
|
|
2094
|
+
this.subs.set(channel, fn);
|
|
2095
|
+
await this.command("SUBSCRIBE", channel);
|
|
2096
|
+
}
|
|
2097
|
+
/** 订阅模式:回调式(channel, message) */
|
|
2098
|
+
async psubscribe(pattern, fn) {
|
|
2099
|
+
this.psubs.set(pattern, fn);
|
|
2100
|
+
await this.command("PSUBSCRIBE", pattern);
|
|
2101
|
+
}
|
|
2102
|
+
/** 旁路分发订阅消息(RESP 数组路由到回调) */
|
|
2103
|
+
dispatchSubscribe(value) {
|
|
2104
|
+
const [type, a, b] = value;
|
|
2105
|
+
if (type === "message") {
|
|
2106
|
+
const fn = this.subs.get(a);
|
|
2107
|
+
fn?.(a, b);
|
|
2108
|
+
} else if (type === "pmessage") {
|
|
2109
|
+
const fn = this.psubs.get(a);
|
|
2110
|
+
fn?.(b, value[3] ?? "");
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
/** 主动关闭——不再重连 */
|
|
2114
|
+
async close() {
|
|
2115
|
+
this.closed = true;
|
|
2116
|
+
this.status = "closed";
|
|
2117
|
+
if (this.reconnectTimer) {
|
|
2118
|
+
clearTimeout(this.reconnectTimer);
|
|
2119
|
+
this.reconnectTimer = null;
|
|
2120
|
+
}
|
|
2121
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
2122
|
+
this.pending = [];
|
|
2123
|
+
this.pendingHead = 0;
|
|
2124
|
+
for (const p of queue2) p.reject(new ConnectionError("redis: connection closed"));
|
|
2125
|
+
const oq = this.offlineQueue;
|
|
2126
|
+
this.offlineQueue = [];
|
|
2127
|
+
for (const q of oq) q.reject(new ConnectionError("redis: connection closed"));
|
|
2128
|
+
const sock = this.socket;
|
|
2129
|
+
this.socket = null;
|
|
2130
|
+
sock?.destroy();
|
|
2131
|
+
}
|
|
2132
|
+
get connected() {
|
|
2133
|
+
return this.status === "ready";
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
// src/db/redis/client.ts
|
|
2138
|
+
var RedisClient = class _RedisClient {
|
|
2139
|
+
conn;
|
|
2140
|
+
constructor(conn) {
|
|
2141
|
+
this.conn = conn;
|
|
2142
|
+
}
|
|
2143
|
+
/** 建立连接并返回就绪的客户端 */
|
|
2144
|
+
static async connect(options = {}) {
|
|
2145
|
+
const conn = new RedisConnection(options);
|
|
2146
|
+
await conn.connect();
|
|
2147
|
+
return new _RedisClient(conn);
|
|
2148
|
+
}
|
|
2149
|
+
/** 底层命令透传(RESP 值);Buffer 参数字节原样发送 */
|
|
2150
|
+
command(name, ...args) {
|
|
2151
|
+
return this.conn.command(name, ...args);
|
|
2152
|
+
}
|
|
2153
|
+
// ── 基础命令 ──────────────────────────────
|
|
2154
|
+
async get(key) {
|
|
2155
|
+
const v = await this.conn.command("GET", key);
|
|
2156
|
+
return v === null ? null : String(v);
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* 二进制安全读取:返回原始字节(Uint8Array),不经过字符串解码。
|
|
2160
|
+
* 用于缓存二进制 payload(序列化字节、图片等)。key 不存在返回 null。
|
|
2161
|
+
*/
|
|
2162
|
+
async getBuffer(key) {
|
|
2163
|
+
const v = await this.conn.command("GET", key, { asBuffer: true });
|
|
2164
|
+
if (v === null) return null;
|
|
2165
|
+
return v instanceof Uint8Array ? v : new TextEncoder().encode(String(v));
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* SET。ttl 秒可省略;传入即安全生效(内部转 SET key val EX ttl)。
|
|
2169
|
+
*/
|
|
2170
|
+
async set(key, value, ttl) {
|
|
2171
|
+
const reply = ttl !== void 0 ? await this.conn.command("SET", key, value, "EX", ttl) : await this.conn.command("SET", key, value);
|
|
2172
|
+
return String(reply);
|
|
2173
|
+
}
|
|
2174
|
+
/** DEL 多 key,返回删除数量 */
|
|
2175
|
+
async del(...keys) {
|
|
2176
|
+
const v = await this.conn.command("DEL", ...keys);
|
|
2177
|
+
return Number(v);
|
|
2178
|
+
}
|
|
2179
|
+
/** INCR,返回自增后的值 */
|
|
2180
|
+
async incr(key) {
|
|
2181
|
+
return Number(await this.conn.command("INCR", key));
|
|
2182
|
+
}
|
|
2183
|
+
/** EXPIRE,返回 1=设置成功 0=key 不存在 */
|
|
2184
|
+
async expire(key, seconds) {
|
|
2185
|
+
return Number(await this.conn.command("EXPIRE", key, seconds));
|
|
2186
|
+
}
|
|
2187
|
+
/** TTL 剩余秒数;-1=无 TTL -2=key 不存在 */
|
|
2188
|
+
async ttl(key) {
|
|
2189
|
+
return Number(await this.conn.command("TTL", key));
|
|
2190
|
+
}
|
|
2191
|
+
// ── JSON 存取(自动序列化) ────────────────
|
|
2192
|
+
/** 读取并 JSON.parse;key 不存在返回 null */
|
|
2193
|
+
async jsonGet(key) {
|
|
2194
|
+
const raw = await this.conn.command("GET", key);
|
|
2195
|
+
if (raw === null) return null;
|
|
2196
|
+
try {
|
|
2197
|
+
return JSON.parse(String(raw));
|
|
2198
|
+
} catch {
|
|
2199
|
+
throw new ValidationError(`redis: value of key '${key}' is not valid JSON`);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
/** JSON.stringify 后写入,可选 TTL */
|
|
2203
|
+
async jsonSet(key, value, ttl) {
|
|
2204
|
+
return this.set(key, JSON.stringify(value), ttl);
|
|
2205
|
+
}
|
|
2206
|
+
// ── 缓存便捷 ──────────────────────────────
|
|
2207
|
+
/**
|
|
2208
|
+
* 缓存读-算-写。命中返回缓存值;miss 执行 fn 并回填。
|
|
2209
|
+
* fn 返回 null 时不缓存(防穿透)。
|
|
2210
|
+
*/
|
|
2211
|
+
async cache(key, fn, ttl) {
|
|
2212
|
+
const hit = await this.jsonGet(key);
|
|
2213
|
+
if (hit !== null) return hit;
|
|
2214
|
+
const value = await fn();
|
|
2215
|
+
if (value !== null) await this.jsonSet(key, value, ttl);
|
|
2216
|
+
return value;
|
|
2217
|
+
}
|
|
2218
|
+
/** 主动关闭连接 */
|
|
2219
|
+
async close() {
|
|
2220
|
+
await this.conn.close();
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
|
|
2224
|
+
// src/db/redis/subscriber.ts
|
|
2225
|
+
var RedisSubscriber = class {
|
|
2226
|
+
conn;
|
|
2227
|
+
constructor(options = {}) {
|
|
2228
|
+
this.conn = new RedisConnection(options);
|
|
2229
|
+
}
|
|
2230
|
+
async connect() {
|
|
2231
|
+
await this.conn.connect();
|
|
2232
|
+
}
|
|
2233
|
+
/** 订阅频道:回调式 */
|
|
2234
|
+
subscribe(channel, fn) {
|
|
2235
|
+
return this.conn.subscribe(channel, fn);
|
|
2236
|
+
}
|
|
2237
|
+
/** 订阅模式(通配符):回调式 */
|
|
2238
|
+
psubscribe(pattern, fn) {
|
|
2239
|
+
return this.conn.psubscribe(pattern, fn);
|
|
2240
|
+
}
|
|
2241
|
+
async close() {
|
|
2242
|
+
await this.conn.close();
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
// src/db/redis/pool.ts
|
|
2247
|
+
var RedisPool = class _RedisPool {
|
|
2248
|
+
clients = [];
|
|
2249
|
+
rr = 0;
|
|
2250
|
+
closed = false;
|
|
2251
|
+
keyPrefix;
|
|
2252
|
+
opts;
|
|
2253
|
+
initPromise = null;
|
|
2254
|
+
/** 懒连接模式:构造不连接,首命令时初始化(中间件注入场景) */
|
|
2255
|
+
constructor(opts = {}) {
|
|
2256
|
+
this.keyPrefix = opts.keyPrefix ?? "";
|
|
2257
|
+
this.opts = opts;
|
|
2258
|
+
}
|
|
2259
|
+
/** 建立池:创建 poolSize 个连接(eager) */
|
|
2260
|
+
static async create(options = {}) {
|
|
2261
|
+
const pool = new _RedisPool(options);
|
|
2262
|
+
await pool.ensure();
|
|
2263
|
+
return pool;
|
|
2264
|
+
}
|
|
2265
|
+
/** 懒连接:首次使用时初始化连接(中间件场景:构造即注入,首命令才连) */
|
|
2266
|
+
readyPromise = null;
|
|
2267
|
+
ensure() {
|
|
2268
|
+
if (this.readyPromise) return this.readyPromise;
|
|
2269
|
+
if (!this.initPromise) {
|
|
2270
|
+
this.initPromise = this.init();
|
|
2020
2271
|
}
|
|
2272
|
+
return this.initPromise;
|
|
2273
|
+
}
|
|
2274
|
+
async init() {
|
|
2275
|
+
const poolSize = this.opts.poolSize ?? 5;
|
|
2276
|
+
this.clients = await Promise.all(
|
|
2277
|
+
Array.from({ length: poolSize }, () => RedisClient.connect(this.opts))
|
|
2278
|
+
);
|
|
2279
|
+
this.readyPromise = Promise.resolve();
|
|
2280
|
+
}
|
|
2281
|
+
/** 应用 key 前缀(command 透传不加) */
|
|
2282
|
+
k(key) {
|
|
2283
|
+
return this.keyPrefix + key;
|
|
2284
|
+
}
|
|
2285
|
+
/** round-robin 选择连接 */
|
|
2286
|
+
next() {
|
|
2287
|
+
if (this.closed || this.clients.length === 0) {
|
|
2288
|
+
throw new ConnectionError("redis: pool is closed");
|
|
2289
|
+
}
|
|
2290
|
+
const client = this.clients[this.rr % this.clients.length];
|
|
2291
|
+
this.rr++;
|
|
2292
|
+
return client;
|
|
2293
|
+
}
|
|
2294
|
+
// ── 与 RedisClient 相同的方法面(代理到轮询连接) ──
|
|
2295
|
+
async command(name, ...args) {
|
|
2296
|
+
await this.ensure();
|
|
2297
|
+
return this.next().command(name, ...args);
|
|
2298
|
+
}
|
|
2299
|
+
async get(key) {
|
|
2300
|
+
await this.ensure();
|
|
2301
|
+
return this.next().get(this.k(key));
|
|
2302
|
+
}
|
|
2303
|
+
/** 二进制安全读取(原始字节,不解码) */
|
|
2304
|
+
async getBuffer(key) {
|
|
2305
|
+
await this.ensure();
|
|
2306
|
+
return this.next().getBuffer(this.k(key));
|
|
2307
|
+
}
|
|
2308
|
+
async set(key, value, ttl) {
|
|
2309
|
+
await this.ensure();
|
|
2310
|
+
return this.next().set(this.k(key), value, ttl);
|
|
2311
|
+
}
|
|
2312
|
+
async del(...keys) {
|
|
2313
|
+
await this.ensure();
|
|
2314
|
+
return this.next().del(...keys.map((k) => this.k(k)));
|
|
2315
|
+
}
|
|
2316
|
+
async incr(key) {
|
|
2317
|
+
await this.ensure();
|
|
2318
|
+
return this.next().incr(this.k(key));
|
|
2319
|
+
}
|
|
2320
|
+
async expire(key, seconds) {
|
|
2321
|
+
await this.ensure();
|
|
2322
|
+
return this.next().expire(this.k(key), seconds);
|
|
2021
2323
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
ctx.serverFirst.split(",").map((kv) => {
|
|
2026
|
-
const i = kv.indexOf("=");
|
|
2027
|
-
return [kv.slice(0, i), kv.slice(i + 1)];
|
|
2028
|
-
})
|
|
2029
|
-
);
|
|
2030
|
-
const salt = Buffer.from(parts.get("s") ?? "", "base64");
|
|
2031
|
-
const iterations = parseInt(parts.get("i") ?? "4096", 10);
|
|
2032
|
-
const nonce = parts.get("r") ?? "";
|
|
2033
|
-
const password = this.opts.password ?? "";
|
|
2034
|
-
const saltedPassword = crypto2.pbkdf2Sync(password, salt, iterations, 32, "sha256");
|
|
2035
|
-
const clientKey = hmac(saltedPassword, "Client Key");
|
|
2036
|
-
const storedKey = crypto2.createHash("sha256").update(clientKey).digest();
|
|
2037
|
-
const channelBinding = "c=biws";
|
|
2038
|
-
const clientFinalWithoutProof = `${channelBinding},r=${nonce}`;
|
|
2039
|
-
const authMessage = `${ctx.clientFirstBare},${ctx.serverFirst},${clientFinalWithoutProof}`;
|
|
2040
|
-
const clientSignature = hmac(storedKey, authMessage);
|
|
2041
|
-
const clientProof = xor(clientKey, clientSignature);
|
|
2042
|
-
return `${clientFinalWithoutProof},p=${Buffer.from(clientProof).toString("base64")}`;
|
|
2324
|
+
async ttl(key) {
|
|
2325
|
+
await this.ensure();
|
|
2326
|
+
return this.next().ttl(this.k(key));
|
|
2043
2327
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
const i = kv.indexOf("=");
|
|
2048
|
-
return [kv.slice(0, i), kv.slice(i + 1)];
|
|
2049
|
-
})
|
|
2050
|
-
);
|
|
2051
|
-
const salt = Buffer.from(parts.get("s") ?? "", "base64");
|
|
2052
|
-
const iterations = parseInt(parts.get("i") ?? "4096", 10);
|
|
2053
|
-
const nonce = parts.get("r") ?? "";
|
|
2054
|
-
const saltedPassword = crypto2.pbkdf2Sync(this.opts.password ?? "", salt, iterations, 32, "sha256");
|
|
2055
|
-
const serverKey = hmac(saltedPassword, "Server Key");
|
|
2056
|
-
const clientFinalWithoutProof = `c=biws,r=${nonce}`;
|
|
2057
|
-
const authMessage = `${ctx.clientFirstBare},${ctx.serverFirst},${clientFinalWithoutProof}`;
|
|
2058
|
-
return Buffer.from(hmac(serverKey, authMessage)).toString("base64");
|
|
2328
|
+
async jsonGet(key) {
|
|
2329
|
+
await this.ensure();
|
|
2330
|
+
return this.next().jsonGet(this.k(key));
|
|
2059
2331
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
try {
|
|
2064
|
-
const result = await fn({ query: (sql, params) => this.query(sql, params) });
|
|
2065
|
-
await this.query("COMMIT");
|
|
2066
|
-
return result;
|
|
2067
|
-
} catch (e) {
|
|
2068
|
-
await this.query("ROLLBACK").catch(() => {
|
|
2069
|
-
});
|
|
2070
|
-
throw e;
|
|
2071
|
-
}
|
|
2332
|
+
async jsonSet(key, value, ttl) {
|
|
2333
|
+
await this.ensure();
|
|
2334
|
+
return this.next().jsonSet(this.k(key), value, ttl);
|
|
2072
2335
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
return
|
|
2076
|
-
if (this.status !== "ready" || !this.socket) {
|
|
2077
|
-
reject(new ConnectionError("postgres: not connected"));
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
if (this.currentQuery) {
|
|
2081
|
-
reject(new ConnectionError("postgres: query already in progress"));
|
|
2082
|
-
return;
|
|
2083
|
-
}
|
|
2084
|
-
if (!params || params.length === 0) {
|
|
2085
|
-
this.currentQuery = {
|
|
2086
|
-
columns: [],
|
|
2087
|
-
rows: [],
|
|
2088
|
-
resolve: resolve3,
|
|
2089
|
-
reject
|
|
2090
|
-
};
|
|
2091
|
-
this.socket.write(Buffer.from(queryMessage(sql)));
|
|
2092
|
-
} else {
|
|
2093
|
-
const sig = `${sql}|${params.length}`;
|
|
2094
|
-
let stmtEntry = this.prepared.get(sig);
|
|
2095
|
-
const encoded = encodeParams(params);
|
|
2096
|
-
if (!stmtEntry) {
|
|
2097
|
-
const name = `wf_s${++this.stmtSeq}`;
|
|
2098
|
-
this.currentQuery = {
|
|
2099
|
-
columns: [],
|
|
2100
|
-
rows: [],
|
|
2101
|
-
resolve: resolve3,
|
|
2102
|
-
reject,
|
|
2103
|
-
sql,
|
|
2104
|
-
params: encoded,
|
|
2105
|
-
awaitingDescribe: true,
|
|
2106
|
-
prepKey: sig,
|
|
2107
|
-
prepName: name
|
|
2108
|
-
};
|
|
2109
|
-
this.socket.write(Buffer.from(parseMessage(name, sql, params.map(() => 0))));
|
|
2110
|
-
this.socket.write(Buffer.from(describeMessage("S", name)));
|
|
2111
|
-
this.socket.write(Buffer.from(flushMessage()));
|
|
2112
|
-
} else {
|
|
2113
|
-
this.currentQuery = {
|
|
2114
|
-
columns: [...stmtEntry.columns],
|
|
2115
|
-
rows: [],
|
|
2116
|
-
resolve: resolve3,
|
|
2117
|
-
reject,
|
|
2118
|
-
sql,
|
|
2119
|
-
params: encoded,
|
|
2120
|
-
awaitingDescribe: false
|
|
2121
|
-
};
|
|
2122
|
-
this.socket.write(Buffer.from(bindMessage(stmtEntry.name, encoded)));
|
|
2123
|
-
this.socket.write(Buffer.from(executeMessage()));
|
|
2124
|
-
this.socket.write(Buffer.from(syncMessage()));
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
});
|
|
2336
|
+
async cache(key, fn, ttl) {
|
|
2337
|
+
await this.ensure();
|
|
2338
|
+
return this.next().cache(this.k(key), fn, ttl);
|
|
2128
2339
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2340
|
+
/** PUBLISH 消息到频道,返回收到消息的订阅者数 */
|
|
2341
|
+
async publish(channel, message) {
|
|
2342
|
+
await this.ensure();
|
|
2343
|
+
return Number(await this.next().command("PUBLISH", channel, message));
|
|
2131
2344
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
this.
|
|
2135
|
-
for (const w of ws) w();
|
|
2345
|
+
/** 创建独立订阅者连接(Pub/Sub 场景) */
|
|
2346
|
+
createSubscriber() {
|
|
2347
|
+
return new RedisSubscriber({ ...this.opts });
|
|
2136
2348
|
}
|
|
2137
|
-
/**
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2349
|
+
/** 清空当前库(测试/重置场景) */
|
|
2350
|
+
async flushdb() {
|
|
2351
|
+
await this.ensure();
|
|
2352
|
+
const v = await this.next().command("FLUSHDB");
|
|
2353
|
+
return String(v);
|
|
2141
2354
|
}
|
|
2142
|
-
/**
|
|
2355
|
+
/** 关闭所有池内连接 */
|
|
2143
2356
|
async close() {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2357
|
+
this.closed = true;
|
|
2358
|
+
this.readyPromise = null;
|
|
2359
|
+
await Promise.all(this.clients.map((c) => c.close()));
|
|
2360
|
+
this.clients = [];
|
|
2361
|
+
}
|
|
2362
|
+
get size() {
|
|
2363
|
+
return this.clients.length;
|
|
2150
2364
|
}
|
|
2151
2365
|
};
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
function
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
out[4] = len & 255;
|
|
2163
|
-
out.set(payload, 5);
|
|
2164
|
-
return out;
|
|
2165
|
-
}
|
|
2166
|
-
function hmac(key, data) {
|
|
2167
|
-
return crypto2.createHmac("sha256", key).update(data).digest();
|
|
2168
|
-
}
|
|
2169
|
-
function xor(a, b) {
|
|
2170
|
-
const out = new Uint8Array(a.length);
|
|
2171
|
-
for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i];
|
|
2172
|
-
return out;
|
|
2173
|
-
}
|
|
2174
|
-
function encodeParams(params) {
|
|
2175
|
-
return params.map((p) => {
|
|
2176
|
-
if (p === null || p === void 0) return null;
|
|
2177
|
-
if (typeof p === "object") return JSON.stringify(p);
|
|
2178
|
-
return String(p);
|
|
2366
|
+
|
|
2367
|
+
// src/queue/index.ts
|
|
2368
|
+
var GROUP = "workers";
|
|
2369
|
+
function makePool(options) {
|
|
2370
|
+
const url = options?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
|
|
2371
|
+
const u = new URL(url);
|
|
2372
|
+
return new RedisPool({
|
|
2373
|
+
host: u.hostname,
|
|
2374
|
+
port: Number(u.port || 6379),
|
|
2375
|
+
poolSize: options?.poolSize
|
|
2179
2376
|
});
|
|
2180
2377
|
}
|
|
2181
|
-
function
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
case "text":
|
|
2219
|
-
if (typeof value !== "string") {
|
|
2220
|
-
throw new ValidationError(`schema: '${column}' must be a string, got ${typeof value}`);
|
|
2378
|
+
function queue(options) {
|
|
2379
|
+
const prefix = options?.prefix ?? "q:";
|
|
2380
|
+
const pool = makePool(options);
|
|
2381
|
+
const stream = (name) => `${prefix}${name}`;
|
|
2382
|
+
const deadStream = (name) => `${prefix}${name}:dead`;
|
|
2383
|
+
async function pushJob(name, payload) {
|
|
2384
|
+
const id = await pool.command("XADD", stream(name), "*", "payload", JSON.stringify(payload));
|
|
2385
|
+
return String(id);
|
|
2386
|
+
}
|
|
2387
|
+
const queueClient = {
|
|
2388
|
+
async add(name, data, opts) {
|
|
2389
|
+
const id = await pushJob(name, {
|
|
2390
|
+
id: randomUUID(),
|
|
2391
|
+
name,
|
|
2392
|
+
data,
|
|
2393
|
+
attempts: 0,
|
|
2394
|
+
maxAttempts: opts?.attempts ?? 3,
|
|
2395
|
+
addedAt: Date.now()
|
|
2396
|
+
});
|
|
2397
|
+
return { id };
|
|
2398
|
+
},
|
|
2399
|
+
length: async (name) => Number(await pool.command("XLEN", stream(name))),
|
|
2400
|
+
worker(name, handler, opts) {
|
|
2401
|
+
const consumer = opts?.consumer ?? `${hostname()}-${process.pid}-${Math.random().toString(36).slice(2, 6)}`;
|
|
2402
|
+
const concurrency = opts?.concurrency ?? 1;
|
|
2403
|
+
const visibilityTimeout = opts?.visibilityTimeout ?? 3e4;
|
|
2404
|
+
const s = stream(name);
|
|
2405
|
+
const dead = deadStream(name);
|
|
2406
|
+
const delayed = `${prefix}${name}:delayed`;
|
|
2407
|
+
let running = false;
|
|
2408
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
2409
|
+
async function ensureGroup() {
|
|
2410
|
+
try {
|
|
2411
|
+
await pool.command("XGROUP", "CREATE", s, GROUP, "0", "MKSTREAM");
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
if (!String(e.message).includes("BUSYGROUP")) throw e;
|
|
2414
|
+
}
|
|
2221
2415
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2416
|
+
async function processEntry(entryId, fields) {
|
|
2417
|
+
let payload;
|
|
2418
|
+
try {
|
|
2419
|
+
payload = JSON.parse(fields.payload ?? "{}");
|
|
2420
|
+
} catch {
|
|
2421
|
+
await pool.command("XACK", s, GROUP, entryId);
|
|
2422
|
+
await pool.command("XADD", dead, "*", "payload", JSON.stringify({
|
|
2423
|
+
originalId: entryId,
|
|
2424
|
+
name,
|
|
2425
|
+
error: "unparseable payload",
|
|
2426
|
+
attempts: -1
|
|
2427
|
+
}));
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
const attempts = Number(payload.attempts ?? 0);
|
|
2431
|
+
const maxAttempts = Number(payload.maxAttempts ?? 3);
|
|
2432
|
+
try {
|
|
2433
|
+
await handler({
|
|
2434
|
+
id: entryId,
|
|
2435
|
+
name,
|
|
2436
|
+
data: payload.data,
|
|
2437
|
+
attempts
|
|
2438
|
+
});
|
|
2439
|
+
await pool.command("XACK", s, GROUP, entryId);
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
const nextAttempt = attempts + 1;
|
|
2442
|
+
await pool.command("XACK", s, GROUP, entryId);
|
|
2443
|
+
if (nextAttempt >= maxAttempts) {
|
|
2444
|
+
await pool.command("XADD", dead, "*", "payload", JSON.stringify({
|
|
2445
|
+
originalId: entryId,
|
|
2446
|
+
name,
|
|
2447
|
+
data: payload.data,
|
|
2448
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2449
|
+
attempts: nextAttempt
|
|
2450
|
+
}));
|
|
2451
|
+
console.error(`[queue] ${name} job failed permanently (attempt ${nextAttempt}/${maxAttempts}):`, err);
|
|
2452
|
+
} else {
|
|
2453
|
+
await pool.command("ZADD", delayed, String(Date.now() + visibilityTimeout), JSON.stringify({
|
|
2454
|
+
...payload,
|
|
2455
|
+
attempts: nextAttempt
|
|
2456
|
+
}));
|
|
2457
|
+
console.error(
|
|
2458
|
+
`[queue] ${name} job failed (attempt ${nextAttempt}/${maxAttempts}), retry in ${visibilityTimeout}ms:`,
|
|
2459
|
+
err
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2226
2463
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2464
|
+
async function requeueDelayed() {
|
|
2465
|
+
const now = Date.now();
|
|
2466
|
+
let due;
|
|
2467
|
+
try {
|
|
2468
|
+
due = await pool.command("ZRANGEBYSCORE", delayed, 0, now);
|
|
2469
|
+
} catch (e) {
|
|
2470
|
+
if (isPoolClosed(e)) {
|
|
2471
|
+
running = false;
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
if (running) console.error("[queue] ZRANGEBYSCORE:", e instanceof Error ? e.message : e);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
for (const member of due) {
|
|
2478
|
+
const removed = await pool.command("ZREM", delayed, member);
|
|
2479
|
+
if (removed) {
|
|
2480
|
+
await pool.command("XADD", s, "*", "payload", member);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2231
2483
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2484
|
+
async function claimStale() {
|
|
2485
|
+
try {
|
|
2486
|
+
const result = await pool.command(
|
|
2487
|
+
"XAUTOCLAIM",
|
|
2488
|
+
s,
|
|
2489
|
+
GROUP,
|
|
2490
|
+
consumer,
|
|
2491
|
+
visibilityTimeout,
|
|
2492
|
+
"0",
|
|
2493
|
+
"COUNT",
|
|
2494
|
+
String(concurrency)
|
|
2495
|
+
);
|
|
2496
|
+
const entries = result[1];
|
|
2497
|
+
for (const [entryId, fields] of entries) {
|
|
2498
|
+
await processEntry(entryId, flatFieldsToRecord(fields));
|
|
2499
|
+
}
|
|
2500
|
+
} catch (e) {
|
|
2501
|
+
if (isPoolClosed(e)) {
|
|
2502
|
+
running = false;
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
if (running) console.error("[queue] XAUTOCLAIM:", e instanceof Error ? e.message : e);
|
|
2506
|
+
}
|
|
2238
2507
|
}
|
|
2239
|
-
|
|
2508
|
+
async function loop() {
|
|
2509
|
+
await ensureGroup();
|
|
2510
|
+
while (running) {
|
|
2511
|
+
await claimStale();
|
|
2512
|
+
await requeueDelayed();
|
|
2513
|
+
let result;
|
|
2514
|
+
try {
|
|
2515
|
+
result = await pool.command(
|
|
2516
|
+
"XREADGROUP",
|
|
2517
|
+
"GROUP",
|
|
2518
|
+
GROUP,
|
|
2519
|
+
consumer,
|
|
2520
|
+
"COUNT",
|
|
2521
|
+
String(concurrency),
|
|
2522
|
+
"BLOCK",
|
|
2523
|
+
"1000",
|
|
2524
|
+
"STREAMS",
|
|
2525
|
+
s,
|
|
2526
|
+
">"
|
|
2527
|
+
);
|
|
2528
|
+
} catch (e) {
|
|
2529
|
+
if (isPoolClosed(e)) {
|
|
2530
|
+
running = false;
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
if (running) console.error("[queue] XREADGROUP:", e instanceof Error ? e.message : e);
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
if (!result) continue;
|
|
2537
|
+
const streamEntries = result[0];
|
|
2538
|
+
const entries = streamEntries[1];
|
|
2539
|
+
for (const [entryId, fields] of entries) {
|
|
2540
|
+
const p = processEntry(entryId, flatFieldsToRecord(fields));
|
|
2541
|
+
inflight.add(p);
|
|
2542
|
+
p.finally(() => inflight.delete(p)).catch(() => {
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return {
|
|
2548
|
+
start: async () => {
|
|
2549
|
+
if (running) return;
|
|
2550
|
+
running = true;
|
|
2551
|
+
loop().catch((e) => console.error("[queue] worker loop crashed:", e));
|
|
2552
|
+
},
|
|
2553
|
+
stop: async () => {
|
|
2554
|
+
running = false;
|
|
2555
|
+
await Promise.allSettled([...inflight]);
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
const mw = (async (req, ctx, next) => {
|
|
2561
|
+
ctx.queue = queueClient;
|
|
2562
|
+
return next(req, ctx);
|
|
2563
|
+
});
|
|
2564
|
+
mw.__meta = { injects: ["queue"], depends: [] };
|
|
2565
|
+
mw.queue = queueClient;
|
|
2566
|
+
mw.close = () => pool.close();
|
|
2567
|
+
return mw;
|
|
2568
|
+
}
|
|
2569
|
+
function flatFieldsToRecord(fields) {
|
|
2570
|
+
const rec = {};
|
|
2571
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
2572
|
+
rec[String(fields[i])] = String(fields[i + 1]);
|
|
2240
2573
|
}
|
|
2574
|
+
return rec;
|
|
2241
2575
|
}
|
|
2242
|
-
function
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2576
|
+
function isPoolClosed(e) {
|
|
2577
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2578
|
+
return /pool is closed|connection closed/.test(msg);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// src/ui/index.ts
|
|
2582
|
+
import { build } from "esbuild";
|
|
2583
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2584
|
+
import { resolve as resolve2 } from "node:path";
|
|
2585
|
+
import { fileURLToPath } from "node:url";
|
|
2586
|
+
|
|
2587
|
+
// src/ui/html-safe.ts
|
|
2588
|
+
var HtmlSafe = class {
|
|
2589
|
+
value;
|
|
2590
|
+
constructor(value) {
|
|
2591
|
+
this.value = value;
|
|
2247
2592
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
if (!def) continue;
|
|
2251
|
-
validateValue(def, value, col);
|
|
2593
|
+
toString() {
|
|
2594
|
+
return this.value;
|
|
2252
2595
|
}
|
|
2596
|
+
};
|
|
2597
|
+
|
|
2598
|
+
// src/client/vnode.ts
|
|
2599
|
+
var ASYNC_MARK = "__wfAsyncComponent";
|
|
2600
|
+
function isAsyncComponent(type) {
|
|
2601
|
+
return typeof type === "function" && type?.[ASYNC_MARK] === true;
|
|
2253
2602
|
}
|
|
2603
|
+
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
2604
|
+
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
2254
2605
|
|
|
2255
|
-
// src/
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2606
|
+
// src/client/reactive.ts
|
|
2607
|
+
function createReactiveState(dirty) {
|
|
2608
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
2609
|
+
const reactive = (target) => {
|
|
2610
|
+
if (target === null || typeof target !== "object") return target;
|
|
2611
|
+
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
2612
|
+
const proxy = new Proxy(target, {
|
|
2613
|
+
set(target2, key, value) {
|
|
2614
|
+
const old = Reflect.get(target2, key);
|
|
2615
|
+
if (old === value) return true;
|
|
2616
|
+
Reflect.set(target2, key, value);
|
|
2617
|
+
dirty();
|
|
2618
|
+
return true;
|
|
2619
|
+
},
|
|
2620
|
+
get(target2, key) {
|
|
2621
|
+
const value = Reflect.get(target2, key);
|
|
2622
|
+
if (typeof value === "object" && value !== null) return reactive(value);
|
|
2623
|
+
return value;
|
|
2624
|
+
},
|
|
2625
|
+
deleteProperty(target2, key) {
|
|
2626
|
+
if (Reflect.has(target2, key)) {
|
|
2627
|
+
Reflect.deleteProperty(target2, key);
|
|
2628
|
+
dirty();
|
|
2629
|
+
}
|
|
2630
|
+
return true;
|
|
2631
|
+
}
|
|
2632
|
+
});
|
|
2633
|
+
proxyCache.set(target, proxy);
|
|
2634
|
+
return proxy;
|
|
2635
|
+
};
|
|
2636
|
+
return reactive({});
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// src/ui/ssr.ts
|
|
2640
|
+
function escape(s) {
|
|
2641
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2642
|
+
}
|
|
2643
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
2644
|
+
"area",
|
|
2645
|
+
"base",
|
|
2646
|
+
"br",
|
|
2647
|
+
"col",
|
|
2648
|
+
"embed",
|
|
2649
|
+
"hr",
|
|
2650
|
+
"img",
|
|
2651
|
+
"input",
|
|
2652
|
+
"link",
|
|
2653
|
+
"meta",
|
|
2654
|
+
"param",
|
|
2655
|
+
"source",
|
|
2656
|
+
"track",
|
|
2657
|
+
"wbr"
|
|
2658
|
+
]);
|
|
2659
|
+
function classToString(v) {
|
|
2660
|
+
if (typeof v === "string") return v;
|
|
2661
|
+
if (Array.isArray(v)) return v.filter(Boolean).join(" ");
|
|
2662
|
+
if (v && typeof v === "object") {
|
|
2663
|
+
return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
|
|
2267
2664
|
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2665
|
+
return "";
|
|
2666
|
+
}
|
|
2667
|
+
function styleToString(v) {
|
|
2668
|
+
if (typeof v === "string") return v;
|
|
2669
|
+
if (v && typeof v === "object") {
|
|
2670
|
+
return Object.entries(v).filter(([, x]) => x != null).map(([k, x]) => `${k}:${typeof x === "number" ? x + "px" : String(x)}`).join(";");
|
|
2272
2671
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2672
|
+
return "";
|
|
2673
|
+
}
|
|
2674
|
+
function createSsrContext(serverCtx, dataStore) {
|
|
2675
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
2676
|
+
const ui2 = {
|
|
2677
|
+
$: () => createReactiveState(() => {
|
|
2678
|
+
}),
|
|
2679
|
+
dirty: () => {
|
|
2680
|
+
},
|
|
2681
|
+
render: () => {
|
|
2682
|
+
},
|
|
2683
|
+
selfId: (name) => {
|
|
2684
|
+
if (usedIds.has(name)) throw new Error(`[weifuwu] Duplicate component ID: "${name}"`);
|
|
2685
|
+
usedIds.add(name);
|
|
2686
|
+
},
|
|
2687
|
+
useMedia: () => {
|
|
2688
|
+
},
|
|
2689
|
+
useBreakpoint: () => {
|
|
2690
|
+
},
|
|
2691
|
+
usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
|
|
2692
|
+
} })
|
|
2693
|
+
};
|
|
2694
|
+
const ctx = Object.create(serverCtx ?? {});
|
|
2695
|
+
ctx.ui = ui2;
|
|
2696
|
+
ctx.data = {
|
|
2697
|
+
async get(key, fetcher) {
|
|
2698
|
+
if (dataStore.has(key)) return dataStore.get(key);
|
|
2699
|
+
if (!fetcher) return void 0;
|
|
2700
|
+
const val = await fetcher();
|
|
2701
|
+
dataStore.set(key, val);
|
|
2702
|
+
return val;
|
|
2703
|
+
},
|
|
2704
|
+
set(key, val) {
|
|
2705
|
+
dataStore.set(key, val);
|
|
2706
|
+
},
|
|
2707
|
+
has(key) {
|
|
2708
|
+
return dataStore.has(key);
|
|
2278
2709
|
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
);
|
|
2290
|
-
this.all = conns;
|
|
2291
|
-
this.available = [...conns];
|
|
2292
|
-
this.readyPromise = Promise.resolve();
|
|
2710
|
+
};
|
|
2711
|
+
return ctx;
|
|
2712
|
+
}
|
|
2713
|
+
async function renderSsr(input, ctx) {
|
|
2714
|
+
if (input == null || typeof input === "boolean") return "";
|
|
2715
|
+
if (typeof input === "string" || typeof input === "number") return escape(String(input));
|
|
2716
|
+
if (Array.isArray(input)) {
|
|
2717
|
+
let s = "";
|
|
2718
|
+
for (const c of input) s += await renderSsr(c, ctx);
|
|
2719
|
+
return s;
|
|
2293
2720
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
};
|
|
2306
|
-
if (timeoutMs > 0) {
|
|
2307
|
-
waiter.timer = setTimeout(() => {
|
|
2308
|
-
const idx = this.waiters.indexOf(waiter);
|
|
2309
|
-
if (idx >= 0) this.waiters.splice(idx, 1);
|
|
2310
|
-
reject(new TimeoutError("postgres: pool acquire", timeoutMs));
|
|
2311
|
-
}, timeoutMs);
|
|
2721
|
+
const vnode = input;
|
|
2722
|
+
if (vnode.type === Portal) return renderSsr(vnode.props?.children, ctx);
|
|
2723
|
+
if (vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
|
|
2724
|
+
if (typeof vnode.type === "function") {
|
|
2725
|
+
let def;
|
|
2726
|
+
if (isAsyncComponent(vnode.type)) {
|
|
2727
|
+
def = await vnode.type(ctx);
|
|
2728
|
+
if (typeof def !== "function") {
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
`asyncComponent factory <${vnode.type.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
|
|
2731
|
+
);
|
|
2312
2732
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2733
|
+
} else {
|
|
2734
|
+
def = vnode.type;
|
|
2735
|
+
}
|
|
2736
|
+
const childCtx = Object.create(ctx);
|
|
2737
|
+
const renderFn = def(vnode.props ?? {}, childCtx);
|
|
2738
|
+
if (typeof renderFn !== "function") {
|
|
2739
|
+
throw new Error(
|
|
2740
|
+
`Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
return renderSsr(renderFn(vnode.props ?? {}), childCtx);
|
|
2315
2744
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2745
|
+
const tag = vnode.type;
|
|
2746
|
+
const props = vnode.props ?? {};
|
|
2747
|
+
const attrs = [];
|
|
2748
|
+
let innerHTML;
|
|
2749
|
+
for (const [key, value] of Object.entries(props)) {
|
|
2750
|
+
if (key === "children" || key === "key") continue;
|
|
2751
|
+
if (key === "ref") continue;
|
|
2752
|
+
if (key.startsWith("on") && typeof value === "function") continue;
|
|
2753
|
+
if (key === "innerHTML") {
|
|
2754
|
+
innerHTML = String(value ?? "");
|
|
2755
|
+
continue;
|
|
2320
2756
|
}
|
|
2321
|
-
if (
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2757
|
+
if (key === "class" || key === "className") {
|
|
2758
|
+
const cls = classToString(value);
|
|
2759
|
+
if (cls) attrs.push(` class="${escape(cls)}"`);
|
|
2760
|
+
continue;
|
|
2325
2761
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
dispatchAvailable(conn) {
|
|
2330
|
-
const waiter = this.waiters.shift();
|
|
2331
|
-
if (waiter) {
|
|
2332
|
-
if (waiter.timer) clearTimeout(waiter.timer);
|
|
2333
|
-
waiter.resolve(conn);
|
|
2334
|
-
} else {
|
|
2335
|
-
this.available.push(conn);
|
|
2762
|
+
if (key === "style" && value && typeof value === "object") {
|
|
2763
|
+
attrs.push(` style="${escape(styleToString(value))}"`);
|
|
2764
|
+
continue;
|
|
2336
2765
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
if (this.closed) return;
|
|
2341
|
-
const c = new PgConnection(this.opts);
|
|
2342
|
-
c.connect().then(() => {
|
|
2343
|
-
if (this.closed) {
|
|
2344
|
-
c.close();
|
|
2345
|
-
return;
|
|
2346
|
-
}
|
|
2347
|
-
this.all.push(c);
|
|
2348
|
-
this.dispatchAvailable(c);
|
|
2349
|
-
}).catch(() => {
|
|
2350
|
-
setTimeout(() => this.replenish(), 500);
|
|
2351
|
-
});
|
|
2352
|
-
}
|
|
2353
|
-
async query(sql, params) {
|
|
2354
|
-
await this.ensure();
|
|
2355
|
-
const conn = await this.acquire();
|
|
2356
|
-
const start = performance.now();
|
|
2357
|
-
try {
|
|
2358
|
-
const rows = await conn.query(sql, params);
|
|
2359
|
-
this.opts.onQuery?.(sql, performance.now() - start, rows.length);
|
|
2360
|
-
return rows;
|
|
2361
|
-
} finally {
|
|
2362
|
-
this.release(conn);
|
|
2766
|
+
if (value === true) {
|
|
2767
|
+
attrs.push(` ${key}`);
|
|
2768
|
+
continue;
|
|
2363
2769
|
}
|
|
2770
|
+
if (value === false || value == null) continue;
|
|
2771
|
+
attrs.push(` ${key}="${escape(String(value))}"`);
|
|
2364
2772
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2773
|
+
const attrStr = attrs.join("");
|
|
2774
|
+
if (VOID_TAGS.has(tag)) return `<${tag}${attrStr}>`;
|
|
2775
|
+
if (innerHTML !== void 0) return `<${tag}${attrStr}>${innerHTML}</${tag}>`;
|
|
2776
|
+
const children = await renderSsr(props.children, ctx);
|
|
2777
|
+
return `<${tag}${attrStr}>${children}</${tag}>`;
|
|
2778
|
+
}
|
|
2779
|
+
async function ssrToString(Comp, props, serverCtx, opts = {}) {
|
|
2780
|
+
const dataStore = opts.data ?? /* @__PURE__ */ new Map();
|
|
2781
|
+
const ctx = createSsrContext(serverCtx, dataStore);
|
|
2782
|
+
const html = await renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
|
|
2783
|
+
return new HtmlSafe(html);
|
|
2784
|
+
}
|
|
2785
|
+
function serializeData(data) {
|
|
2786
|
+
const json = JSON.stringify(Object.fromEntries(data)).replace(/</g, "\\u003c");
|
|
2787
|
+
return `<script>window.__DATA__=${json};</script>`;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/ui/index.ts
|
|
2791
|
+
function escape2(s) {
|
|
2792
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2793
|
+
}
|
|
2794
|
+
function stringify(v) {
|
|
2795
|
+
if (v == null || v === false || v === true) return "";
|
|
2796
|
+
if (Array.isArray(v)) return v.map(stringify).join("");
|
|
2797
|
+
if (v instanceof HtmlSafe) return v.value;
|
|
2798
|
+
return escape2(String(v));
|
|
2799
|
+
}
|
|
2800
|
+
function unsafe(s) {
|
|
2801
|
+
return new HtmlSafe(s);
|
|
2802
|
+
}
|
|
2803
|
+
var jsCache = /* @__PURE__ */ new Map();
|
|
2804
|
+
async function jsCacheFresh(inputs) {
|
|
2805
|
+
for (const [file, mtime] of Object.entries(inputs)) {
|
|
2806
|
+
try {
|
|
2807
|
+
const st = await stat(file);
|
|
2808
|
+
if (st.mtimeMs !== mtime) return false;
|
|
2809
|
+
} catch {
|
|
2810
|
+
return false;
|
|
2374
2811
|
}
|
|
2375
|
-
validateRow(schema, row);
|
|
2376
|
-
const cols = Object.keys(row);
|
|
2377
|
-
if (cols.length === 0) throw new ValidationError("schema: insert requires at least one column");
|
|
2378
|
-
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
2379
|
-
const sql = `INSERT INTO ${table} (${cols.join(", ")}) VALUES (${placeholders})`;
|
|
2380
|
-
return this.query(sql, cols.map((c) => row[c]));
|
|
2381
2812
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2813
|
+
return true;
|
|
2814
|
+
}
|
|
2815
|
+
var cssCache = /* @__PURE__ */ new Map();
|
|
2816
|
+
var postcssAvailable;
|
|
2817
|
+
async function checkPostcss() {
|
|
2818
|
+
if (postcssAvailable !== void 0) return postcssAvailable;
|
|
2819
|
+
try {
|
|
2820
|
+
await import("postcss");
|
|
2821
|
+
await import("@tailwindcss/postcss");
|
|
2822
|
+
postcssAvailable = true;
|
|
2823
|
+
} catch {
|
|
2824
|
+
postcssAvailable = false;
|
|
2390
2825
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
const { sql, params } = parseTagged(strings, values);
|
|
2397
|
-
return tx.query(sql, params);
|
|
2398
|
-
};
|
|
2399
|
-
return fn(txSql);
|
|
2400
|
-
});
|
|
2826
|
+
return postcssAvailable;
|
|
2827
|
+
}
|
|
2828
|
+
function resolveEntry(entryPath) {
|
|
2829
|
+
if (entryPath.startsWith(".") || entryPath.startsWith("/")) {
|
|
2830
|
+
return resolve2(entryPath);
|
|
2401
2831
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
return
|
|
2832
|
+
try {
|
|
2833
|
+
return fileURLToPath(import.meta.resolve(entryPath));
|
|
2834
|
+
} catch {
|
|
2835
|
+
return resolve2(entryPath);
|
|
2406
2836
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2837
|
+
}
|
|
2838
|
+
function ui() {
|
|
2839
|
+
return async (_req, ctx, next) => {
|
|
2840
|
+
function htmlTag(strings, ...values) {
|
|
2841
|
+
let body = "";
|
|
2842
|
+
for (let i = 0; i < strings.length; i++) {
|
|
2843
|
+
body += strings[i];
|
|
2844
|
+
if (i < values.length) body += stringify(values[i]);
|
|
2845
|
+
}
|
|
2846
|
+
return new Response(body, {
|
|
2847
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2848
|
+
});
|
|
2415
2849
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2850
|
+
ctx.ui = {
|
|
2851
|
+
html: Object.assign(htmlTag, { unsafe }),
|
|
2852
|
+
/** SSR 渲染组件 → HTML 片段(HtmlSafe:内联进 ctx.ui.html 不二次转义) */
|
|
2853
|
+
async ssr(Comp, props, opts) {
|
|
2854
|
+
const safe = await ssrToString(Comp, props ?? {}, ctx, opts);
|
|
2855
|
+
return safe;
|
|
2856
|
+
},
|
|
2857
|
+
/** 序列化 SSR 数据存储 → window.__DATA__ 脚本(HtmlSafe) */
|
|
2858
|
+
ssrData(data) {
|
|
2859
|
+
return new HtmlSafe(serializeData(data));
|
|
2860
|
+
},
|
|
2861
|
+
async js(entryPath) {
|
|
2862
|
+
const absPath = resolveEntry(entryPath);
|
|
2863
|
+
const cached = jsCache.get(absPath);
|
|
2864
|
+
if (cached && await jsCacheFresh(cached.inputs)) {
|
|
2865
|
+
return new Response(cached.code, {
|
|
2866
|
+
headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
const result = await build({
|
|
2870
|
+
entryPoints: [absPath],
|
|
2871
|
+
bundle: true,
|
|
2872
|
+
format: "esm",
|
|
2873
|
+
platform: "browser",
|
|
2874
|
+
jsx: "automatic",
|
|
2875
|
+
jsxImportSource: "weifuwu/client",
|
|
2876
|
+
write: false,
|
|
2877
|
+
metafile: true
|
|
2429
2878
|
});
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2879
|
+
const code = result.outputFiles[0].text;
|
|
2880
|
+
const inputs = {};
|
|
2881
|
+
for (const file of Object.keys(result.metafile?.inputs ?? {})) {
|
|
2882
|
+
const abs = resolve2(file);
|
|
2883
|
+
try {
|
|
2884
|
+
inputs[abs] = (await stat(abs)).mtimeMs;
|
|
2885
|
+
} catch {
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
jsCache.set(absPath, { code, inputs });
|
|
2889
|
+
return new Response(code, {
|
|
2890
|
+
headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
|
|
2891
|
+
});
|
|
2892
|
+
},
|
|
2893
|
+
async css(entryPath) {
|
|
2894
|
+
const absPath = resolveEntry(entryPath);
|
|
2895
|
+
const st = await import("node:fs").then((fs) => fs.promises.stat(absPath));
|
|
2896
|
+
const cached = cssCache.get(absPath);
|
|
2897
|
+
if (cached && cached.mtime === st.mtimeMs) {
|
|
2898
|
+
return new Response(cached.code, {
|
|
2899
|
+
headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
let code = await readFile(absPath, "utf-8");
|
|
2903
|
+
if (await checkPostcss()) {
|
|
2904
|
+
try {
|
|
2905
|
+
const postcss = await import("postcss");
|
|
2906
|
+
const tw = await import("@tailwindcss/postcss");
|
|
2907
|
+
const plugin = tw.default || tw;
|
|
2908
|
+
const instance = typeof plugin === "function" ? plugin() : plugin;
|
|
2909
|
+
const result = await postcss.default([instance]).process(code, { from: absPath });
|
|
2910
|
+
code = result.css;
|
|
2911
|
+
} catch (e) {
|
|
2912
|
+
throw new Error(`PostCSS \u7F16\u8BD1\u5931\u8D25 (${absPath}): ${e.message}`, { cause: e });
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
cssCache.set(absPath, { code, mtime: st.mtimeMs });
|
|
2916
|
+
return new Response(code, {
|
|
2917
|
+
headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
return next(_req, ctx);
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
// src/client/route-match.ts
|
|
2926
|
+
function joinPaths(a, b) {
|
|
2927
|
+
if (!b || b === "/") return a || "/";
|
|
2928
|
+
const left = a.endsWith("/") ? a.slice(0, -1) : a;
|
|
2929
|
+
const right = b.startsWith("/") ? b : "/" + b;
|
|
2930
|
+
return left + right;
|
|
2931
|
+
}
|
|
2932
|
+
function compilePath(path) {
|
|
2933
|
+
const keys = [];
|
|
2934
|
+
const reStr = path.replace(/:(\w+)/g, (_, key) => {
|
|
2935
|
+
keys.push(key);
|
|
2936
|
+
return "([^/]+)";
|
|
2937
|
+
}).replace(/\*/g, ".*");
|
|
2938
|
+
return { re: new RegExp(`^${reStr}$`), keys };
|
|
2939
|
+
}
|
|
2940
|
+
function flattenRoutes(routes, basePath = "", chain = []) {
|
|
2941
|
+
const result = [];
|
|
2942
|
+
for (const route of routes) {
|
|
2943
|
+
const fullPath = joinPaths(basePath, route.path);
|
|
2944
|
+
const { re, keys } = compilePath(fullPath);
|
|
2945
|
+
const fullChain = [...chain, route];
|
|
2946
|
+
result.push({ re, keys, def: route, chain: fullChain });
|
|
2947
|
+
if (route.children) {
|
|
2948
|
+
result.push(...flattenRoutes(route.children, fullPath, fullChain));
|
|
2434
2949
|
}
|
|
2435
2950
|
}
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
this.waiters = [];
|
|
2444
|
-
for (const w of ws) {
|
|
2445
|
-
if (w.timer) clearTimeout(w.timer);
|
|
2446
|
-
w.reject(new ConnectionError("postgres: pool is closed"));
|
|
2951
|
+
return result;
|
|
2952
|
+
}
|
|
2953
|
+
function matchRoute(path, routes) {
|
|
2954
|
+
let best = null;
|
|
2955
|
+
for (const fr of routes) {
|
|
2956
|
+
if (path.match(fr.re)) {
|
|
2957
|
+
if (!best || fr.chain.length > best.chain.length) best = fr;
|
|
2447
2958
|
}
|
|
2448
2959
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
};
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
const frag = values[i]?.__fragment;
|
|
2458
|
-
if (frag) {
|
|
2459
|
-
const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
|
|
2460
|
-
params.push(frag.params[parseInt(idx, 10) - 1]);
|
|
2461
|
-
return `$${params.length}`;
|
|
2462
|
-
});
|
|
2463
|
-
sql += renumbered + strings[i + 1];
|
|
2464
|
-
} else {
|
|
2465
|
-
params.push(values[i]);
|
|
2466
|
-
sql += `$${params.length}` + strings[i + 1];
|
|
2960
|
+
return best;
|
|
2961
|
+
}
|
|
2962
|
+
function extractParams(path, match) {
|
|
2963
|
+
const params = {};
|
|
2964
|
+
const m = path.match(match.re);
|
|
2965
|
+
if (m) {
|
|
2966
|
+
for (let i = 0; i < match.keys.length; i++) {
|
|
2967
|
+
params[match.keys[i]] = decodeURIComponent(m[i + 1]);
|
|
2467
2968
|
}
|
|
2468
2969
|
}
|
|
2469
|
-
return
|
|
2970
|
+
return params;
|
|
2470
2971
|
}
|
|
2471
2972
|
|
|
2472
|
-
// src/
|
|
2473
|
-
var
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
return next(req, ctx);
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
};
|
|
2514
|
-
mw.isMigrated = async (moduleName) => {
|
|
2515
|
-
const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
|
|
2516
|
-
return rows.length > 0;
|
|
2973
|
+
// src/ui/ssr-page.ts
|
|
2974
|
+
var DEFAULT_TEMPLATE = (p) => `<!DOCTYPE html>
|
|
2975
|
+
<html lang="zh-CN">
|
|
2976
|
+
<head>
|
|
2977
|
+
<meta charset="UTF-8">
|
|
2978
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2979
|
+
<title>${escapeHtml(p.title)}</title>
|
|
2980
|
+
${(p.styles ?? []).map((s) => `<link rel="stylesheet" href="${escapeHtml(s)}">`).join("\n ")}
|
|
2981
|
+
</head>
|
|
2982
|
+
<body>
|
|
2983
|
+
<div id="root">${p.html}</div>
|
|
2984
|
+
${p.dataScript}
|
|
2985
|
+
${p.bundle ? `<script src="${escapeHtml(p.bundle)}"></script>` : ""}
|
|
2986
|
+
</body>
|
|
2987
|
+
</html>`;
|
|
2988
|
+
function escapeHtml(s) {
|
|
2989
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2990
|
+
}
|
|
2991
|
+
function uiSsr(opts) {
|
|
2992
|
+
const flatRoutes = flattenRoutes(opts.routes);
|
|
2993
|
+
const template = opts.template ?? DEFAULT_TEMPLATE;
|
|
2994
|
+
return async (req, ctx, next) => {
|
|
2995
|
+
if (req.method !== "GET") return next(req, ctx);
|
|
2996
|
+
const url = new URL(req.url);
|
|
2997
|
+
const match = matchRoute(url.pathname, flatRoutes);
|
|
2998
|
+
if (!match) return next(req, ctx);
|
|
2999
|
+
const params = extractParams(url.pathname, match);
|
|
3000
|
+
const def = match.chain[match.chain.length - 1] ?? match.def;
|
|
3001
|
+
const pageCtx = Object.assign(Object.create(ctx), {
|
|
3002
|
+
params,
|
|
3003
|
+
route: { path: url.pathname, params, query: Object.fromEntries(url.searchParams) }
|
|
3004
|
+
});
|
|
3005
|
+
if (!def.component) return next(req, ctx);
|
|
3006
|
+
const data = /* @__PURE__ */ new Map();
|
|
3007
|
+
const html = await ssrToString(def.component, {}, pageCtx, { data });
|
|
3008
|
+
const title = opts.title ? opts.title({ params, def }) : def.title || "weifuwu";
|
|
3009
|
+
const dataScript = new HtmlSafe(serializeData(data)).toString();
|
|
3010
|
+
const body = template({ html: html.toString(), dataScript, title, bundle: opts.bundle, styles: opts.styles });
|
|
3011
|
+
return new Response(body, {
|
|
3012
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
3013
|
+
});
|
|
2517
3014
|
};
|
|
2518
|
-
mw.transaction = (async (fn) => {
|
|
2519
|
-
return sql.transaction(fn);
|
|
2520
|
-
});
|
|
2521
|
-
mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
|
|
2522
|
-
mw.close = () => pool.close();
|
|
2523
|
-
return mw;
|
|
2524
3015
|
}
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
3016
|
+
|
|
3017
|
+
// src/db/postgres/connection.ts
|
|
3018
|
+
import net3 from "node:net";
|
|
3019
|
+
import crypto2 from "node:crypto";
|
|
3020
|
+
|
|
3021
|
+
// src/db/postgres/protocol.ts
|
|
3022
|
+
function encodeMessage(type, payload) {
|
|
3023
|
+
const out = new Uint8Array(1 + 4 + payload.length);
|
|
3024
|
+
out[0] = type.charCodeAt(0);
|
|
3025
|
+
const len = 4 + payload.length;
|
|
3026
|
+
out[1] = len >> 24 & 255;
|
|
3027
|
+
out[2] = len >> 16 & 255;
|
|
3028
|
+
out[3] = len >> 8 & 255;
|
|
3029
|
+
out[4] = len & 255;
|
|
3030
|
+
out.set(payload, 5);
|
|
3031
|
+
return out;
|
|
3032
|
+
}
|
|
3033
|
+
function startupMessage(params) {
|
|
3034
|
+
const body = [];
|
|
3035
|
+
body.push(0, 3, 0, 0);
|
|
3036
|
+
for (const [k, v] of Object.entries(params)) {
|
|
3037
|
+
for (const c of utf8(k)) body.push(c);
|
|
3038
|
+
body.push(0);
|
|
3039
|
+
for (const c of utf8(v)) body.push(c);
|
|
3040
|
+
body.push(0);
|
|
2546
3041
|
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
sql.transaction = (fn) => pool.transaction(fn);
|
|
2557
|
-
sql.close = () => pool.close();
|
|
2558
|
-
return sql;
|
|
3042
|
+
body.push(0);
|
|
3043
|
+
const out = new Uint8Array(4 + body.length);
|
|
3044
|
+
const len = 4 + body.length;
|
|
3045
|
+
out[0] = len >> 24 & 255;
|
|
3046
|
+
out[1] = len >> 16 & 255;
|
|
3047
|
+
out[2] = len >> 8 & 255;
|
|
3048
|
+
out[3] = len & 255;
|
|
3049
|
+
out.set(body, 4);
|
|
3050
|
+
return out;
|
|
2559
3051
|
}
|
|
2560
|
-
function
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
3052
|
+
function queryMessage(sql) {
|
|
3053
|
+
return encodeMessage("Q", concat(utf8(sql), new Uint8Array([0])));
|
|
3054
|
+
}
|
|
3055
|
+
function parseMessage(name, sql, paramTypes = []) {
|
|
3056
|
+
const body = concat(utf8(name), new Uint8Array([0]), utf8(sql), new Uint8Array([0]));
|
|
3057
|
+
const types = new Uint8Array(2 + paramTypes.length * 4);
|
|
3058
|
+
types[0] = paramTypes.length >> 8 & 255;
|
|
3059
|
+
types[1] = paramTypes.length & 255;
|
|
3060
|
+
paramTypes.forEach((oid, i) => {
|
|
3061
|
+
const off = 2 + i * 4;
|
|
3062
|
+
types[off] = oid >> 24 & 255;
|
|
3063
|
+
types[off + 1] = oid >> 16 & 255;
|
|
3064
|
+
types[off + 2] = oid >> 8 & 255;
|
|
3065
|
+
types[off + 3] = oid & 255;
|
|
3066
|
+
});
|
|
3067
|
+
return encodeMessage("P", concat(body, types));
|
|
3068
|
+
}
|
|
3069
|
+
function bindMessage(statement, params, paramFormats = []) {
|
|
3070
|
+
const stmtBytes = utf8(statement);
|
|
3071
|
+
const lens = new Array(params.length);
|
|
3072
|
+
let total = 1 + stmtBytes.length + 1;
|
|
3073
|
+
total += 2 + paramFormats.length * 2;
|
|
3074
|
+
total += 2;
|
|
3075
|
+
for (let i = 0; i < params.length; i++) {
|
|
3076
|
+
const p = params[i];
|
|
3077
|
+
total += 4;
|
|
3078
|
+
if (p !== null) {
|
|
3079
|
+
const len = typeof p === "string" ? utf8Len(p) : p.length;
|
|
3080
|
+
lens[i] = len;
|
|
3081
|
+
total += len;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
total += 2;
|
|
3085
|
+
const body = new Uint8Array(total);
|
|
3086
|
+
let off = 0;
|
|
3087
|
+
body[off++] = 0;
|
|
3088
|
+
body.set(stmtBytes, off);
|
|
3089
|
+
off += stmtBytes.length;
|
|
3090
|
+
body[off++] = 0;
|
|
3091
|
+
body[off] = paramFormats.length >> 8 & 255;
|
|
3092
|
+
body[off + 1] = paramFormats.length & 255;
|
|
3093
|
+
off += 2;
|
|
3094
|
+
for (const f of paramFormats) {
|
|
3095
|
+
body[off] = f >> 8 & 255;
|
|
3096
|
+
body[off + 1] = f & 255;
|
|
3097
|
+
off += 2;
|
|
3098
|
+
}
|
|
3099
|
+
body[off] = params.length >> 8 & 255;
|
|
3100
|
+
body[off + 1] = params.length & 255;
|
|
3101
|
+
off += 2;
|
|
3102
|
+
for (let i = 0; i < params.length; i++) {
|
|
3103
|
+
const p = params[i];
|
|
3104
|
+
if (p === null) {
|
|
3105
|
+
body[off] = body[off + 1] = body[off + 2] = body[off + 3] = 255;
|
|
3106
|
+
off += 4;
|
|
2571
3107
|
} else {
|
|
2572
|
-
|
|
2573
|
-
|
|
3108
|
+
const len = lens[i];
|
|
3109
|
+
body[off] = len >> 24 & 255;
|
|
3110
|
+
body[off + 1] = len >> 16 & 255;
|
|
3111
|
+
body[off + 2] = len >> 8 & 255;
|
|
3112
|
+
body[off + 3] = len & 255;
|
|
3113
|
+
off += 4;
|
|
3114
|
+
if (typeof p === "string") body.set(utf8(p), off);
|
|
3115
|
+
else body.set(p, off);
|
|
3116
|
+
off += len;
|
|
2574
3117
|
}
|
|
2575
3118
|
}
|
|
2576
|
-
|
|
3119
|
+
body[off] = 0;
|
|
3120
|
+
body[off + 1] = 0;
|
|
3121
|
+
return encodeMessage("B", body);
|
|
2577
3122
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
// unique_violation
|
|
2581
|
-
"23503": 400,
|
|
2582
|
-
// foreign_key_violation
|
|
2583
|
-
"23502": 400,
|
|
2584
|
-
// not_null_violation
|
|
2585
|
-
"23514": 400,
|
|
2586
|
-
// check_violation
|
|
2587
|
-
"22P02": 400,
|
|
2588
|
-
// invalid_text_representation
|
|
2589
|
-
"22003": 400
|
|
2590
|
-
// numeric_value_out_of_range
|
|
2591
|
-
};
|
|
2592
|
-
function wrapError(promise) {
|
|
2593
|
-
return promise.catch((err) => {
|
|
2594
|
-
const code = err?.code;
|
|
2595
|
-
if (code && PG_ERROR_MAP[code]) {
|
|
2596
|
-
throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
|
|
2597
|
-
}
|
|
2598
|
-
throw err;
|
|
2599
|
-
});
|
|
3123
|
+
function utf8Len(s) {
|
|
3124
|
+
return Buffer.byteLength(s);
|
|
2600
3125
|
}
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
};
|
|
2612
|
-
var IncompleteError = class extends Error {
|
|
2613
|
-
constructor() {
|
|
2614
|
-
super("incomplete RESP message");
|
|
2615
|
-
this.name = "IncompleteError";
|
|
2616
|
-
}
|
|
2617
|
-
};
|
|
2618
|
-
function encodeCommand(args) {
|
|
2619
|
-
const parts = [`*${args.length}\r
|
|
2620
|
-
`];
|
|
2621
|
-
for (const arg of args) {
|
|
2622
|
-
const s = typeof arg === "string" ? arg : arg instanceof Buffer ? arg.toString() : String(arg);
|
|
2623
|
-
parts.push(`$${Buffer.byteLength(s)}\r
|
|
2624
|
-
${s}\r
|
|
2625
|
-
`);
|
|
2626
|
-
}
|
|
2627
|
-
return _encoder.encode(parts.join(""));
|
|
3126
|
+
function executeMessage(portal = "", maxRows = 0) {
|
|
3127
|
+
const body = new Uint8Array(portal.length + 1 + 4);
|
|
3128
|
+
body.set(utf8(portal), 0);
|
|
3129
|
+
body[portal.length] = 0;
|
|
3130
|
+
const off = portal.length + 1;
|
|
3131
|
+
body[off] = maxRows >> 24 & 255;
|
|
3132
|
+
body[off + 1] = maxRows >> 16 & 255;
|
|
3133
|
+
body[off + 2] = maxRows >> 8 & 255;
|
|
3134
|
+
body[off + 3] = maxRows & 255;
|
|
3135
|
+
return encodeMessage("E", body);
|
|
2628
3136
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
3137
|
+
function syncMessage() {
|
|
3138
|
+
return encodeMessage("S", new Uint8Array(0));
|
|
3139
|
+
}
|
|
3140
|
+
function flushMessage() {
|
|
3141
|
+
return encodeMessage("H", new Uint8Array(0));
|
|
3142
|
+
}
|
|
3143
|
+
function describeMessage(kind, name = "") {
|
|
3144
|
+
return encodeMessage("D", concat(utf8(kind), utf8(name), new Uint8Array([0])));
|
|
3145
|
+
}
|
|
3146
|
+
function terminateMessage() {
|
|
3147
|
+
return encodeMessage("X", new Uint8Array(0));
|
|
3148
|
+
}
|
|
3149
|
+
function passwordMessage(password) {
|
|
3150
|
+
return encodeMessage("p", utf8(password));
|
|
3151
|
+
}
|
|
3152
|
+
var MessageStream = class {
|
|
2632
3153
|
buf = new Uint8Array(0);
|
|
2633
3154
|
off = 0;
|
|
2634
|
-
/** 喂入数据分片。返回 { value, incomplete };incomplete=true 时需继续喂。 */
|
|
2635
3155
|
push(chunk) {
|
|
2636
|
-
this.append(chunk);
|
|
2637
|
-
const saved = this.off;
|
|
2638
|
-
try {
|
|
2639
|
-
const value = this.parseValue();
|
|
2640
|
-
this.compact();
|
|
2641
|
-
return { value, incomplete: false };
|
|
2642
|
-
} catch (e) {
|
|
2643
|
-
this.off = saved;
|
|
2644
|
-
if (e instanceof IncompleteError) return { value: null, incomplete: true };
|
|
2645
|
-
throw e;
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
/** 喂入分片并解析 buffer 中所有完整响应(连接层:一次 data 事件可能含多个回复) */
|
|
2649
|
-
pushAll(chunk) {
|
|
2650
3156
|
this.append(chunk);
|
|
2651
3157
|
const out = [];
|
|
2652
3158
|
while (true) {
|
|
2653
3159
|
const saved = this.off;
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
} catch (e) {
|
|
3160
|
+
const msg = this.tryRead();
|
|
3161
|
+
if (!msg) {
|
|
2657
3162
|
this.off = saved;
|
|
2658
|
-
|
|
2659
|
-
throw e;
|
|
3163
|
+
break;
|
|
2660
3164
|
}
|
|
3165
|
+
out.push(msg);
|
|
2661
3166
|
}
|
|
2662
3167
|
if (out.length > 0) this.compact();
|
|
2663
3168
|
return out;
|
|
@@ -2677,7 +3182,6 @@ var RespParser = class {
|
|
|
2677
3182
|
this.buf = merged;
|
|
2678
3183
|
this.off = 0;
|
|
2679
3184
|
}
|
|
2680
|
-
/** 全部消费后压缩(释放底层) */
|
|
2681
3185
|
compact() {
|
|
2682
3186
|
if (this.off === this.buf.length) {
|
|
2683
3187
|
this.buf = new Uint8Array(0);
|
|
@@ -2687,388 +3191,596 @@ var RespParser = class {
|
|
|
2687
3191
|
this.off = 0;
|
|
2688
3192
|
}
|
|
2689
3193
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
3194
|
+
/** 尝试读一条完整消息;不完整返回 null(不消费) */
|
|
3195
|
+
tryRead() {
|
|
3196
|
+
if (this.buf.length - this.off < 5) return null;
|
|
3197
|
+
const len = this.buf[this.off + 1] << 24 | this.buf[this.off + 2] << 16 | this.buf[this.off + 3] << 8 | this.buf[this.off + 4];
|
|
3198
|
+
if (this.buf.length - this.off < 1 + len) return null;
|
|
2692
3199
|
const type = String.fromCharCode(this.buf[this.off]);
|
|
2693
|
-
this.off
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
const line = this.readLine();
|
|
2697
|
-
return line;
|
|
2698
|
-
}
|
|
2699
|
-
case "-": {
|
|
2700
|
-
const line = this.readLine();
|
|
2701
|
-
return new RespError(line);
|
|
2702
|
-
}
|
|
2703
|
-
case ":": {
|
|
2704
|
-
const line = this.readLine();
|
|
2705
|
-
return parseInt(line, 10);
|
|
2706
|
-
}
|
|
2707
|
-
case "$": {
|
|
2708
|
-
const len = parseInt(this.readLine(), 10);
|
|
2709
|
-
if (len === -1) return null;
|
|
2710
|
-
return this.readBulk(len);
|
|
2711
|
-
}
|
|
2712
|
-
case "*": {
|
|
2713
|
-
const count = parseInt(this.readLine(), 10);
|
|
2714
|
-
if (count === -1) return null;
|
|
2715
|
-
const items = [];
|
|
2716
|
-
for (let i = 0; i < count; i++) items.push(this.parseValue());
|
|
2717
|
-
return items;
|
|
2718
|
-
}
|
|
2719
|
-
default:
|
|
2720
|
-
throw new DbError("protocol", `unknown RESP type byte: ${type}`, { code: "RESP" });
|
|
2721
|
-
}
|
|
3200
|
+
const payload = this.buf.subarray(this.off + 5, this.off + 1 + len);
|
|
3201
|
+
this.off += 1 + len;
|
|
3202
|
+
return { type, payload };
|
|
2722
3203
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3204
|
+
};
|
|
3205
|
+
function authCode(msg) {
|
|
3206
|
+
if (msg.type !== "R" || msg.payload.length < 4) return -1;
|
|
3207
|
+
return msg.payload[0] << 24 | msg.payload[1] << 16 | msg.payload[2] << 8 | msg.payload[3];
|
|
3208
|
+
}
|
|
3209
|
+
function parseRowDescription(payload) {
|
|
3210
|
+
const count = payload[0] << 8 | payload[1];
|
|
3211
|
+
const cols = [];
|
|
3212
|
+
let i = 2;
|
|
3213
|
+
for (let c = 0; c < count; c++) {
|
|
3214
|
+
let j = i;
|
|
3215
|
+
while (payload[j] !== 0) j++;
|
|
3216
|
+
const name = _decoder2.decode(payload.subarray(i, j));
|
|
3217
|
+
i = j + 1;
|
|
3218
|
+
i += 6;
|
|
3219
|
+
const typeOid = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
3220
|
+
i += 4;
|
|
3221
|
+
const typeLen = payload[i] << 8 | payload[i + 1];
|
|
3222
|
+
i += 2;
|
|
3223
|
+
i += 6;
|
|
3224
|
+
cols.push({ name, typeOid, typeLen });
|
|
2730
3225
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3226
|
+
return cols;
|
|
3227
|
+
}
|
|
3228
|
+
function parseDataRow(payload) {
|
|
3229
|
+
const count = payload[0] << 8 | payload[1];
|
|
3230
|
+
const values = [];
|
|
3231
|
+
let i = 2;
|
|
3232
|
+
for (let c = 0; c < count; c++) {
|
|
3233
|
+
const len = payload[i] << 24 | payload[i + 1] << 16 | payload[i + 2] << 8 | payload[i + 3];
|
|
3234
|
+
i += 4;
|
|
3235
|
+
if (len === -1) {
|
|
3236
|
+
values.push(null);
|
|
3237
|
+
} else {
|
|
3238
|
+
values.push(_decoder2.decode(payload.subarray(i, i + len)));
|
|
3239
|
+
i += len;
|
|
3240
|
+
}
|
|
2737
3241
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
3242
|
+
return values;
|
|
3243
|
+
}
|
|
3244
|
+
function parseErrorFields(payload) {
|
|
3245
|
+
const out = {};
|
|
3246
|
+
let i = 0;
|
|
3247
|
+
while (i < payload.length && payload[i] !== 0) {
|
|
3248
|
+
const type = String.fromCharCode(payload[i]);
|
|
3249
|
+
let j = i + 1;
|
|
3250
|
+
while (j < payload.length && payload[j] !== 0) j++;
|
|
3251
|
+
const value = _decoder2.decode(payload.subarray(i + 1, j));
|
|
3252
|
+
switch (type) {
|
|
3253
|
+
case "S":
|
|
3254
|
+
out.severity = value;
|
|
3255
|
+
break;
|
|
3256
|
+
case "C":
|
|
3257
|
+
out.code = value;
|
|
3258
|
+
break;
|
|
3259
|
+
case "M":
|
|
3260
|
+
out.message = value;
|
|
3261
|
+
break;
|
|
3262
|
+
case "D":
|
|
3263
|
+
out.detail = value;
|
|
3264
|
+
break;
|
|
3265
|
+
case "H":
|
|
3266
|
+
out.hint = value;
|
|
3267
|
+
break;
|
|
3268
|
+
case "P":
|
|
3269
|
+
out.position = value;
|
|
3270
|
+
break;
|
|
3271
|
+
}
|
|
3272
|
+
i = j + 1;
|
|
2742
3273
|
}
|
|
2743
|
-
return
|
|
3274
|
+
return out;
|
|
3275
|
+
}
|
|
3276
|
+
var _decoder2 = new TextDecoder();
|
|
3277
|
+
var _encoder2 = new TextEncoder();
|
|
3278
|
+
function utf8(s) {
|
|
3279
|
+
return _encoder2.encode(s);
|
|
3280
|
+
}
|
|
3281
|
+
function concat(...parts) {
|
|
3282
|
+
let total = 0;
|
|
3283
|
+
for (const p of parts) total += p.length;
|
|
3284
|
+
const out = new Uint8Array(total);
|
|
3285
|
+
let off = 0;
|
|
3286
|
+
for (const p of parts) {
|
|
3287
|
+
out.set(p, off);
|
|
3288
|
+
off += p.length;
|
|
3289
|
+
}
|
|
3290
|
+
return out;
|
|
2744
3291
|
}
|
|
2745
3292
|
|
|
2746
|
-
// src/db/
|
|
2747
|
-
var
|
|
2748
|
-
var RedisConnection = class {
|
|
2749
|
-
ready = false;
|
|
3293
|
+
// src/db/postgres/connection.ts
|
|
3294
|
+
var PgConnection = class _PgConnection {
|
|
2750
3295
|
opts;
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
retries = 0;
|
|
2759
|
-
reconnectTimer = null;
|
|
2760
|
-
connectPromise = null;
|
|
2761
|
-
closed = false;
|
|
2762
|
-
connectedOnce = false;
|
|
3296
|
+
timeoutSet = false;
|
|
3297
|
+
awaitingReady = false;
|
|
3298
|
+
pendingErrorZ = false;
|
|
3299
|
+
socket = null;
|
|
3300
|
+
stream = new MessageStream();
|
|
3301
|
+
status = "idle";
|
|
3302
|
+
waiters = [];
|
|
2763
3303
|
constructor(options = {}) {
|
|
2764
3304
|
this.opts = {
|
|
2765
3305
|
host: options.host ?? "127.0.0.1",
|
|
2766
|
-
port: options.port ??
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3306
|
+
port: options.port ?? 5432,
|
|
3307
|
+
user: options.user ?? "postgres",
|
|
3308
|
+
database: options.database ?? "postgres",
|
|
3309
|
+
connectTimeoutMs: options.connectTimeoutMs ?? 1e4,
|
|
3310
|
+
statementTimeoutMs: options.statementTimeoutMs ?? 0,
|
|
3311
|
+
password: options.password
|
|
2770
3312
|
};
|
|
2771
3313
|
}
|
|
2772
|
-
|
|
3314
|
+
get connected() {
|
|
3315
|
+
return this.status === "ready";
|
|
3316
|
+
}
|
|
2773
3317
|
connect() {
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
this.
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
this.
|
|
2782
|
-
|
|
2783
|
-
|
|
3318
|
+
return new Promise((resolve3, reject) => {
|
|
3319
|
+
this.status = "connecting";
|
|
3320
|
+
const sock = net3.connect(this.opts.port, this.opts.host);
|
|
3321
|
+
this.socket = sock;
|
|
3322
|
+
const timeout = setTimeout(() => {
|
|
3323
|
+
sock.destroy();
|
|
3324
|
+
reject(new ConnectionError(`postgres: connect timeout after ${this.opts.connectTimeoutMs}ms`));
|
|
3325
|
+
}, this.opts.connectTimeoutMs);
|
|
3326
|
+
sock.once("connect", () => {
|
|
3327
|
+
sock.setNoDelay(true);
|
|
3328
|
+
sock.write(startupMessage({ user: this.opts.user, database: this.opts.database }));
|
|
3329
|
+
});
|
|
3330
|
+
sock.on("data", (chunk) => this.onData(chunk));
|
|
3331
|
+
sock.on("error", (err) => {
|
|
3332
|
+
clearTimeout(timeout);
|
|
3333
|
+
if (this.status === "connecting") {
|
|
3334
|
+
reject(new ConnectionError(`postgres: ${err.message}`));
|
|
3335
|
+
}
|
|
3336
|
+
});
|
|
3337
|
+
sock.on("close", () => {
|
|
3338
|
+
clearTimeout(timeout);
|
|
3339
|
+
this.socket = null;
|
|
3340
|
+
if (this.status !== "closed") {
|
|
3341
|
+
this.status = "closed";
|
|
3342
|
+
const q = this.currentQuery;
|
|
3343
|
+
this.currentQuery = null;
|
|
3344
|
+
if (q) q.reject(new ConnectionError("postgres: connection closed"));
|
|
3345
|
+
}
|
|
3346
|
+
});
|
|
3347
|
+
this.onReady = () => {
|
|
3348
|
+
clearTimeout(timeout);
|
|
3349
|
+
this.status = "ready";
|
|
3350
|
+
resolve3();
|
|
3351
|
+
};
|
|
3352
|
+
this.onAuthFail = (err) => {
|
|
3353
|
+
clearTimeout(timeout);
|
|
3354
|
+
reject(err);
|
|
3355
|
+
};
|
|
2784
3356
|
});
|
|
2785
|
-
return this.connectPromise;
|
|
2786
3357
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3358
|
+
onReady = null;
|
|
3359
|
+
onAuthFail = null;
|
|
3360
|
+
expectingAuth = true;
|
|
3361
|
+
authCtx = null;
|
|
3362
|
+
prepared = /* @__PURE__ */ new Map();
|
|
3363
|
+
// sig → stmt + 列缓存(LRU)
|
|
3364
|
+
static PREPARED_MAX = 128;
|
|
3365
|
+
stmtSeq = 0;
|
|
3366
|
+
currentQuery = null;
|
|
3367
|
+
onData(chunk) {
|
|
3368
|
+
const msgs = this.stream.push(new Uint8Array(chunk));
|
|
3369
|
+
for (const msg of msgs) {
|
|
3370
|
+
this.handle(msg);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
handle(msg) {
|
|
3374
|
+
if (this.expectingAuth) {
|
|
3375
|
+
if (msg.type === "R") {
|
|
3376
|
+
const code = authCode(msg);
|
|
3377
|
+
if (code === 0) {
|
|
3378
|
+
} else if (code === 3) {
|
|
3379
|
+
this.send(passwordMessage(this.opts.password ?? ""));
|
|
3380
|
+
} else if (code === 5) {
|
|
3381
|
+
const salt = msg.payload.subarray(8, 12);
|
|
3382
|
+
const inner = crypto2.createHash("md5").update(this.opts.password + this.opts.user).digest("hex");
|
|
3383
|
+
const outer = crypto2.createHash("md5").update(inner + Buffer.from(salt).toString("latin1")).digest("hex");
|
|
3384
|
+
this.send(passwordMessage(`md5${outer}`));
|
|
3385
|
+
} else if (code === 10) {
|
|
3386
|
+
const nonce = crypto2.randomBytes(18).toString("base64url");
|
|
3387
|
+
const clientFirstBare = `n=,r=${nonce}`;
|
|
3388
|
+
this.authCtx = { clientNonce: nonce, clientFirstBare, serverFirst: "" };
|
|
3389
|
+
const mech = utf82("SCRAM-SHA-256\0");
|
|
3390
|
+
const resp = utf82(`n,,${clientFirstBare}`);
|
|
3391
|
+
const payload = new Uint8Array(mech.length + 4 + resp.length);
|
|
3392
|
+
payload.set(mech, 0);
|
|
3393
|
+
payload[mech.length] = resp.length >> 24 & 255;
|
|
3394
|
+
payload[mech.length + 1] = resp.length >> 16 & 255;
|
|
3395
|
+
payload[mech.length + 2] = resp.length >> 8 & 255;
|
|
3396
|
+
payload[mech.length + 3] = resp.length & 255;
|
|
3397
|
+
payload.set(resp, mech.length + 4);
|
|
3398
|
+
this.send(encodeP(payload));
|
|
3399
|
+
} else if (code === 11) {
|
|
3400
|
+
const serverFirst = new TextDecoder().decode(msg.payload.subarray(4));
|
|
3401
|
+
if (!this.authCtx) {
|
|
3402
|
+
this.onAuthFail?.(new ConnectionError("postgres: unexpected SASL continue"));
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
this.authCtx.serverFirst = serverFirst;
|
|
3406
|
+
const clientFinal = this.scramFinal(this.authCtx);
|
|
3407
|
+
this.send(encodeP(utf82(clientFinal)));
|
|
3408
|
+
} else if (code === 12) {
|
|
3409
|
+
const serverFinal = new TextDecoder().decode(msg.payload.subarray(4));
|
|
3410
|
+
const vMatch = serverFinal.match(/v=([A-Za-z0-9+/=]+)/);
|
|
3411
|
+
if (vMatch && this.authCtx) {
|
|
3412
|
+
const expected = this.scramServerSignature(this.authCtx);
|
|
3413
|
+
if (vMatch[1] !== expected) {
|
|
3414
|
+
this.onAuthFail?.(new ConnectionError("postgres: SCRAM server signature mismatch"));
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
return;
|
|
2802
3420
|
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
3421
|
+
if (msg.type === "E") {
|
|
3422
|
+
const fields = parseErrorFields(msg.payload);
|
|
3423
|
+
this.expectingAuth = false;
|
|
3424
|
+
this.onAuthFail?.(new ConnectionError(`postgres: auth failed: ${fields.message ?? "unknown"}`));
|
|
3425
|
+
this.socket?.destroy();
|
|
3426
|
+
return;
|
|
2809
3427
|
}
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3428
|
+
if (msg.type === "Z") {
|
|
3429
|
+
this.expectingAuth = false;
|
|
3430
|
+
if (this.opts.statementTimeoutMs > 0 && !this.timeoutSet) {
|
|
3431
|
+
this.timeoutSet = true;
|
|
3432
|
+
this.awaitingReady = true;
|
|
3433
|
+
this.socket?.write(queryMessage(`SET statement_timeout = ${this.opts.statementTimeoutMs}`));
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
this.onReady?.();
|
|
3437
|
+
return;
|
|
2815
3438
|
}
|
|
2816
|
-
});
|
|
2817
|
-
}
|
|
2818
|
-
handleDisconnect(err) {
|
|
2819
|
-
const queue = this.pending;
|
|
2820
|
-
this.pending = [];
|
|
2821
|
-
const retryErr = err instanceof ConnectionError ? err : err;
|
|
2822
|
-
for (const p of queue) p.reject(retryErr);
|
|
2823
|
-
if (this.closed || this.status === "closed") return;
|
|
2824
|
-
this.retries++;
|
|
2825
|
-
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
2826
|
-
this.status = "closed";
|
|
2827
|
-
const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
|
|
2828
|
-
this.onceFailed?.(failErr);
|
|
2829
3439
|
return;
|
|
2830
3440
|
}
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
try {
|
|
2839
|
-
const values = this.parser.pushAll(chunk);
|
|
2840
|
-
for (const value of values) {
|
|
2841
|
-
if (Array.isArray(value) && typeof value[0] === "string" && PUSH_TYPES.has(value[0])) {
|
|
2842
|
-
this.dispatchSubscribe(value);
|
|
2843
|
-
continue;
|
|
3441
|
+
switch (msg.type) {
|
|
3442
|
+
case "T": {
|
|
3443
|
+
if (this.currentQuery) {
|
|
3444
|
+
this.currentQuery.columns = parseRowDescription(msg.payload);
|
|
3445
|
+
const sig = `${this.currentQuery.sql}|${this.currentQuery.params?.length ?? 0}`;
|
|
3446
|
+
const entry = this.getPrepared(sig);
|
|
3447
|
+
if (entry) entry.columns = this.currentQuery.columns;
|
|
2844
3448
|
}
|
|
2845
|
-
|
|
2846
|
-
if (!p) break;
|
|
2847
|
-
if (value instanceof RespError) p.reject(value);
|
|
2848
|
-
else p.resolve(value);
|
|
3449
|
+
break;
|
|
2849
3450
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
3451
|
+
case "D": {
|
|
3452
|
+
const values = parseDataRow(msg.payload);
|
|
3453
|
+
if (this.currentQuery && this.currentQuery.columns) {
|
|
3454
|
+
const row = {};
|
|
3455
|
+
this.currentQuery.columns.forEach((col, i) => {
|
|
3456
|
+
row[col.name] = convertValue(col.typeOid, values[i] ?? null);
|
|
3457
|
+
});
|
|
3458
|
+
this.currentQuery.rows.push(row);
|
|
3459
|
+
}
|
|
3460
|
+
break;
|
|
3461
|
+
}
|
|
3462
|
+
case "C":
|
|
3463
|
+
break;
|
|
3464
|
+
// CommandComplete——忽略 tag
|
|
3465
|
+
case "t": {
|
|
3466
|
+
if (this.currentQuery?.awaitingDescribe && this.currentQuery.sql !== void 0) {
|
|
3467
|
+
this.currentQuery.awaitingDescribe = false;
|
|
3468
|
+
if (this.currentQuery.prepKey && this.currentQuery.prepName) {
|
|
3469
|
+
this.setPrepared(this.currentQuery.prepKey, {
|
|
3470
|
+
name: this.currentQuery.prepName,
|
|
3471
|
+
columns: this.currentQuery.columns
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
this.send(bindMessage(this.currentQuery.prepName ?? "", this.currentQuery.params ?? []));
|
|
3475
|
+
this.send(executeMessage());
|
|
3476
|
+
this.send(syncMessage());
|
|
3477
|
+
}
|
|
3478
|
+
break;
|
|
3479
|
+
}
|
|
3480
|
+
case "Z": {
|
|
3481
|
+
if (this.pendingErrorZ) {
|
|
3482
|
+
this.pendingErrorZ = false;
|
|
3483
|
+
break;
|
|
3484
|
+
}
|
|
3485
|
+
const q = this.currentQuery;
|
|
3486
|
+
this.currentQuery = null;
|
|
3487
|
+
if (q) q.resolve(q.rows);
|
|
3488
|
+
if (this.awaitingReady) {
|
|
3489
|
+
this.awaitingReady = false;
|
|
3490
|
+
this.onReady?.();
|
|
3491
|
+
}
|
|
3492
|
+
this.notifyIdle();
|
|
3493
|
+
break;
|
|
3494
|
+
}
|
|
3495
|
+
case "E": {
|
|
3496
|
+
const fields = parseErrorFields(msg.payload);
|
|
3497
|
+
const q = this.currentQuery;
|
|
3498
|
+
this.currentQuery = null;
|
|
3499
|
+
if (q) {
|
|
3500
|
+
this.pendingErrorZ = true;
|
|
3501
|
+
if (q.awaitingDescribe) this.send(syncMessage());
|
|
3502
|
+
const err = new Error(fields.message ?? "postgres query error");
|
|
3503
|
+
err.code = fields.code;
|
|
3504
|
+
q.reject(err);
|
|
3505
|
+
}
|
|
3506
|
+
break;
|
|
3507
|
+
}
|
|
3508
|
+
case "N":
|
|
3509
|
+
break;
|
|
3510
|
+
// NoticeResponse 忽略
|
|
3511
|
+
default:
|
|
3512
|
+
break;
|
|
2855
3513
|
}
|
|
2856
3514
|
}
|
|
2857
|
-
/**
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3515
|
+
/** SCRAM client-final 消息 */
|
|
3516
|
+
scramFinal(ctx) {
|
|
3517
|
+
const parts = new Map(
|
|
3518
|
+
ctx.serverFirst.split(",").map((kv) => {
|
|
3519
|
+
const i = kv.indexOf("=");
|
|
3520
|
+
return [kv.slice(0, i), kv.slice(i + 1)];
|
|
3521
|
+
})
|
|
3522
|
+
);
|
|
3523
|
+
const salt = Buffer.from(parts.get("s") ?? "", "base64");
|
|
3524
|
+
const iterations = parseInt(parts.get("i") ?? "4096", 10);
|
|
3525
|
+
const nonce = parts.get("r") ?? "";
|
|
3526
|
+
const password = this.opts.password ?? "";
|
|
3527
|
+
const saltedPassword = crypto2.pbkdf2Sync(password, salt, iterations, 32, "sha256");
|
|
3528
|
+
const clientKey = hmac(saltedPassword, "Client Key");
|
|
3529
|
+
const storedKey = crypto2.createHash("sha256").update(clientKey).digest();
|
|
3530
|
+
const channelBinding = "c=biws";
|
|
3531
|
+
const clientFinalWithoutProof = `${channelBinding},r=${nonce}`;
|
|
3532
|
+
const authMessage = `${ctx.clientFirstBare},${ctx.serverFirst},${clientFinalWithoutProof}`;
|
|
3533
|
+
const clientSignature = hmac(storedKey, authMessage);
|
|
3534
|
+
const clientProof = xor(clientKey, clientSignature);
|
|
3535
|
+
return `${clientFinalWithoutProof},p=${Buffer.from(clientProof).toString("base64")}`;
|
|
2868
3536
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3537
|
+
scramServerSignature(ctx) {
|
|
3538
|
+
const parts = new Map(
|
|
3539
|
+
ctx.serverFirst.split(",").map((kv) => {
|
|
3540
|
+
const i = kv.indexOf("=");
|
|
3541
|
+
return [kv.slice(0, i), kv.slice(i + 1)];
|
|
3542
|
+
})
|
|
3543
|
+
);
|
|
3544
|
+
const salt = Buffer.from(parts.get("s") ?? "", "base64");
|
|
3545
|
+
const iterations = parseInt(parts.get("i") ?? "4096", 10);
|
|
3546
|
+
const nonce = parts.get("r") ?? "";
|
|
3547
|
+
const saltedPassword = crypto2.pbkdf2Sync(this.opts.password ?? "", salt, iterations, 32, "sha256");
|
|
3548
|
+
const serverKey = hmac(saltedPassword, "Server Key");
|
|
3549
|
+
const clientFinalWithoutProof = `c=biws,r=${nonce}`;
|
|
3550
|
+
const authMessage = `${ctx.clientFirstBare},${ctx.serverFirst},${clientFinalWithoutProof}`;
|
|
3551
|
+
return Buffer.from(hmac(serverKey, authMessage)).toString("base64");
|
|
2874
3552
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
this.
|
|
3553
|
+
/** LRU 读取:命中移到尾部(最近使用),超限删最旧 */
|
|
3554
|
+
getPrepared(sig) {
|
|
3555
|
+
const entry = this.prepared.get(sig);
|
|
3556
|
+
if (entry) {
|
|
3557
|
+
this.prepared.delete(sig);
|
|
3558
|
+
this.prepared.set(sig, entry);
|
|
3559
|
+
}
|
|
3560
|
+
return entry;
|
|
3561
|
+
}
|
|
3562
|
+
/** LRU 写入:刷新位置;超上限淘汰最旧(长运行服务防无限累积) */
|
|
3563
|
+
setPrepared(sig, entry) {
|
|
3564
|
+
this.prepared.delete(sig);
|
|
3565
|
+
this.prepared.set(sig, entry);
|
|
3566
|
+
if (this.prepared.size > _PgConnection.PREPARED_MAX) {
|
|
3567
|
+
const oldest = this.prepared.keys().next().value;
|
|
3568
|
+
if (oldest !== void 0) this.prepared.delete(oldest);
|
|
2880
3569
|
}
|
|
2881
3570
|
}
|
|
2882
|
-
/**
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3571
|
+
/** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
|
|
3572
|
+
async transaction(fn) {
|
|
3573
|
+
await this.query("BEGIN");
|
|
3574
|
+
try {
|
|
3575
|
+
const result = await fn({ query: (sql, params) => this.query(sql, params) });
|
|
3576
|
+
await this.query("COMMIT");
|
|
3577
|
+
return result;
|
|
3578
|
+
} catch (e) {
|
|
3579
|
+
await this.query("ROLLBACK").catch(() => {
|
|
3580
|
+
});
|
|
3581
|
+
throw e;
|
|
2886
3582
|
}
|
|
3583
|
+
}
|
|
3584
|
+
/** 查询:无参数走简单协议(Q),有参数走扩展查询(Parse/Bind/Execute/Sync) */
|
|
3585
|
+
query(sql, params) {
|
|
2887
3586
|
return new Promise((resolve3, reject) => {
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3587
|
+
if (this.status !== "ready" || !this.socket) {
|
|
3588
|
+
reject(new ConnectionError("postgres: not connected"));
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
if (this.currentQuery) {
|
|
3592
|
+
reject(new ConnectionError("postgres: query already in progress"));
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
if (!params || params.length === 0) {
|
|
3596
|
+
this.currentQuery = {
|
|
3597
|
+
columns: [],
|
|
3598
|
+
rows: [],
|
|
3599
|
+
resolve: resolve3,
|
|
3600
|
+
reject
|
|
3601
|
+
};
|
|
3602
|
+
this.socket.write(queryMessage(sql));
|
|
3603
|
+
} else {
|
|
3604
|
+
const sig = `${sql}|${params.length}`;
|
|
3605
|
+
let stmtEntry = this.getPrepared(sig);
|
|
3606
|
+
const encoded = encodeParams(params);
|
|
3607
|
+
if (!stmtEntry) {
|
|
3608
|
+
const name = `wf_s${++this.stmtSeq}`;
|
|
3609
|
+
this.currentQuery = {
|
|
3610
|
+
columns: [],
|
|
3611
|
+
rows: [],
|
|
3612
|
+
resolve: resolve3,
|
|
3613
|
+
reject,
|
|
3614
|
+
sql,
|
|
3615
|
+
params: encoded,
|
|
3616
|
+
awaitingDescribe: true,
|
|
3617
|
+
prepKey: sig,
|
|
3618
|
+
prepName: name
|
|
3619
|
+
};
|
|
3620
|
+
this.socket.write(parseMessage(name, sql, params.map(() => 0)));
|
|
3621
|
+
this.socket.write(describeMessage("S", name));
|
|
3622
|
+
this.socket.write(flushMessage());
|
|
3623
|
+
} else {
|
|
3624
|
+
this.currentQuery = {
|
|
3625
|
+
columns: [...stmtEntry.columns],
|
|
3626
|
+
rows: [],
|
|
3627
|
+
resolve: resolve3,
|
|
3628
|
+
reject,
|
|
3629
|
+
sql,
|
|
3630
|
+
params: encoded,
|
|
3631
|
+
awaitingDescribe: false
|
|
3632
|
+
};
|
|
3633
|
+
this.socket.write(bindMessage(stmtEntry.name, encoded));
|
|
3634
|
+
this.socket.write(executeMessage());
|
|
3635
|
+
this.socket.write(syncMessage());
|
|
3636
|
+
}
|
|
2904
3637
|
}
|
|
2905
|
-
this.socket.write(payload);
|
|
2906
3638
|
});
|
|
2907
3639
|
}
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
this.subs.set(channel, fn);
|
|
2911
|
-
await this.command("SUBSCRIBE", channel);
|
|
3640
|
+
send(data) {
|
|
3641
|
+
this.socket?.write(data);
|
|
2912
3642
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
this.
|
|
2916
|
-
|
|
3643
|
+
notifyIdle() {
|
|
3644
|
+
const ws = this.waiters;
|
|
3645
|
+
this.waiters = [];
|
|
3646
|
+
for (const w of ws) w();
|
|
2917
3647
|
}
|
|
2918
|
-
/**
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
const fn = this.subs.get(a);
|
|
2923
|
-
fn?.(a, b);
|
|
2924
|
-
} else if (type === "pmessage") {
|
|
2925
|
-
const fn = this.psubs.get(a);
|
|
2926
|
-
fn?.(b, value[3] ?? "");
|
|
2927
|
-
}
|
|
3648
|
+
/** 等待连接空闲(事务/多语句流程用) */
|
|
3649
|
+
waitIdle() {
|
|
3650
|
+
if (!this.currentQuery) return Promise.resolve();
|
|
3651
|
+
return new Promise((resolve3) => this.waiters.push(resolve3));
|
|
2928
3652
|
}
|
|
2929
|
-
/**
|
|
3653
|
+
/** 终止连接 */
|
|
2930
3654
|
async close() {
|
|
2931
|
-
this.closed = true;
|
|
2932
|
-
this.status = "closed";
|
|
2933
|
-
if (this.reconnectTimer) {
|
|
2934
|
-
clearTimeout(this.reconnectTimer);
|
|
2935
|
-
this.reconnectTimer = null;
|
|
2936
|
-
}
|
|
2937
|
-
const queue = this.pending;
|
|
2938
|
-
this.pending = [];
|
|
2939
|
-
for (const p of queue) p.reject(new ConnectionError("redis: connection closed"));
|
|
2940
3655
|
const sock = this.socket;
|
|
2941
|
-
this.
|
|
2942
|
-
|
|
2943
|
-
sock.destroy();
|
|
2944
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
3656
|
+
if (this.status === "ready" && sock) {
|
|
3657
|
+
sock.write(terminateMessage());
|
|
2945
3658
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
return this.status === "ready";
|
|
3659
|
+
this.status = "closed";
|
|
3660
|
+
if (sock) sock.destroy();
|
|
2949
3661
|
}
|
|
2950
3662
|
};
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
}
|
|
2993
|
-
/** TTL 剩余秒数;-1=无 TTL -2=key 不存在 */
|
|
2994
|
-
async ttl(key) {
|
|
2995
|
-
return Number(await this.conn.command("TTL", key));
|
|
2996
|
-
}
|
|
2997
|
-
// ── JSON 存取(自动序列化) ────────────────
|
|
2998
|
-
/** 读取并 JSON.parse;key 不存在返回 null */
|
|
2999
|
-
async jsonGet(key) {
|
|
3000
|
-
const raw = await this.conn.command("GET", key);
|
|
3001
|
-
if (raw === null) return null;
|
|
3002
|
-
try {
|
|
3003
|
-
return JSON.parse(String(raw));
|
|
3004
|
-
} catch {
|
|
3005
|
-
throw new ValidationError(`redis: value of key '${key}' is not valid JSON`);
|
|
3663
|
+
function utf82(s) {
|
|
3664
|
+
return new TextEncoder().encode(s);
|
|
3665
|
+
}
|
|
3666
|
+
function encodeP(payload) {
|
|
3667
|
+
const out = new Uint8Array(1 + 4 + payload.length);
|
|
3668
|
+
out[0] = 112;
|
|
3669
|
+
const len = 4 + payload.length;
|
|
3670
|
+
out[1] = len >> 24 & 255;
|
|
3671
|
+
out[2] = len >> 16 & 255;
|
|
3672
|
+
out[3] = len >> 8 & 255;
|
|
3673
|
+
out[4] = len & 255;
|
|
3674
|
+
out.set(payload, 5);
|
|
3675
|
+
return out;
|
|
3676
|
+
}
|
|
3677
|
+
function hmac(key, data) {
|
|
3678
|
+
return crypto2.createHmac("sha256", key).update(data).digest();
|
|
3679
|
+
}
|
|
3680
|
+
function xor(a, b) {
|
|
3681
|
+
const out = new Uint8Array(a.length);
|
|
3682
|
+
for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i];
|
|
3683
|
+
return out;
|
|
3684
|
+
}
|
|
3685
|
+
function encodeParams(params) {
|
|
3686
|
+
return params.map((p) => {
|
|
3687
|
+
if (p === null || p === void 0) return null;
|
|
3688
|
+
if (typeof p === "object") return JSON.stringify(p);
|
|
3689
|
+
return String(p);
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
function convertValue(oid, value) {
|
|
3693
|
+
if (value === null) return null;
|
|
3694
|
+
switch (oid) {
|
|
3695
|
+
case 114:
|
|
3696
|
+
case 3802:
|
|
3697
|
+
return JSON.parse(value);
|
|
3698
|
+
case 20: {
|
|
3699
|
+
const n = BigInt(value);
|
|
3700
|
+
if (n > BigInt(Number.MAX_SAFE_INTEGER) || n < BigInt(-Number.MAX_SAFE_INTEGER)) {
|
|
3701
|
+
return value;
|
|
3702
|
+
}
|
|
3703
|
+
return Number(n);
|
|
3006
3704
|
}
|
|
3705
|
+
case 21:
|
|
3706
|
+
case 23:
|
|
3707
|
+
case 26:
|
|
3708
|
+
return Number(value);
|
|
3709
|
+
case 700:
|
|
3710
|
+
case 701:
|
|
3711
|
+
case 1700:
|
|
3712
|
+
return parseFloat(value);
|
|
3713
|
+
case 16:
|
|
3714
|
+
return value === "t";
|
|
3715
|
+
default:
|
|
3716
|
+
return value;
|
|
3007
3717
|
}
|
|
3008
|
-
|
|
3009
|
-
async jsonSet(key, value, ttl) {
|
|
3010
|
-
return this.set(key, JSON.stringify(value), ttl);
|
|
3011
|
-
}
|
|
3012
|
-
// ── 缓存便捷 ──────────────────────────────
|
|
3013
|
-
/**
|
|
3014
|
-
* 缓存读-算-写。命中返回缓存值;miss 执行 fn 并回填。
|
|
3015
|
-
* fn 返回 null 时不缓存(防穿透)。
|
|
3016
|
-
*/
|
|
3017
|
-
async cache(key, fn, ttl) {
|
|
3018
|
-
const hit = await this.jsonGet(key);
|
|
3019
|
-
if (hit !== null) return hit;
|
|
3020
|
-
const value = await fn();
|
|
3021
|
-
if (value !== null) await this.jsonSet(key, value, ttl);
|
|
3022
|
-
return value;
|
|
3023
|
-
}
|
|
3024
|
-
/** 主动关闭连接 */
|
|
3025
|
-
async close() {
|
|
3026
|
-
await this.conn.close();
|
|
3027
|
-
}
|
|
3028
|
-
};
|
|
3718
|
+
}
|
|
3029
3719
|
|
|
3030
|
-
// src/db/
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
await this.conn.connect();
|
|
3720
|
+
// src/db/postgres/schema.ts
|
|
3721
|
+
function validateValue(def, value, column) {
|
|
3722
|
+
if (value === void 0 || value === null) {
|
|
3723
|
+
if (def.required) {
|
|
3724
|
+
throw new ValidationError(`schema: '${column}' is required`);
|
|
3725
|
+
}
|
|
3726
|
+
return;
|
|
3038
3727
|
}
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3728
|
+
switch (def.type) {
|
|
3729
|
+
case "text":
|
|
3730
|
+
if (typeof value !== "string") {
|
|
3731
|
+
throw new ValidationError(`schema: '${column}' must be a string, got ${typeof value}`);
|
|
3732
|
+
}
|
|
3733
|
+
break;
|
|
3734
|
+
case "int":
|
|
3735
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
3736
|
+
throw new ValidationError(`schema: '${column}' must be an integer, got ${typeof value}`);
|
|
3737
|
+
}
|
|
3738
|
+
break;
|
|
3739
|
+
case "jsonb":
|
|
3740
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3741
|
+
throw new ValidationError(`schema: '${column}' must be a JSON object`);
|
|
3742
|
+
}
|
|
3743
|
+
break;
|
|
3744
|
+
case "enum":
|
|
3745
|
+
if (!def.values?.includes(value)) {
|
|
3746
|
+
throw new ValidationError(
|
|
3747
|
+
`schema: '${column}' must be one of [${def.values?.join(", ")}], got ${String(value)}`
|
|
3748
|
+
);
|
|
3749
|
+
}
|
|
3750
|
+
break;
|
|
3042
3751
|
}
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3752
|
+
}
|
|
3753
|
+
function validateRow(schema, row) {
|
|
3754
|
+
for (const [col, def] of Object.entries(schema)) {
|
|
3755
|
+
if (def.required && (row[col] === void 0 || row[col] === null)) {
|
|
3756
|
+
throw new ValidationError(`schema: '${col}' is required`);
|
|
3757
|
+
}
|
|
3046
3758
|
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3759
|
+
for (const [col, value] of Object.entries(row)) {
|
|
3760
|
+
const def = schema[col];
|
|
3761
|
+
if (!def) continue;
|
|
3762
|
+
validateValue(def, value, col);
|
|
3049
3763
|
}
|
|
3050
|
-
}
|
|
3764
|
+
}
|
|
3051
3765
|
|
|
3052
|
-
// src/db/
|
|
3053
|
-
var
|
|
3054
|
-
|
|
3055
|
-
|
|
3766
|
+
// src/db/postgres/pool.ts
|
|
3767
|
+
var PgPool = class _PgPool {
|
|
3768
|
+
all = [];
|
|
3769
|
+
available = [];
|
|
3770
|
+
waiters = [];
|
|
3056
3771
|
closed = false;
|
|
3057
|
-
keyPrefix;
|
|
3058
3772
|
opts;
|
|
3059
3773
|
initPromise = null;
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
this.opts =
|
|
3774
|
+
schemas = /* @__PURE__ */ new Map();
|
|
3775
|
+
/** 懒连接:构造不连接,ensure() 首次初始化(中间件注入场景) */
|
|
3776
|
+
constructor(options = {}) {
|
|
3777
|
+
this.opts = options;
|
|
3064
3778
|
}
|
|
3065
|
-
/** 建立池:创建 poolSize 个连接(eager) */
|
|
3066
3779
|
static async create(options = {}) {
|
|
3067
|
-
const pool = new
|
|
3780
|
+
const pool = new _PgPool(options);
|
|
3068
3781
|
await pool.ensure();
|
|
3069
3782
|
return pool;
|
|
3070
3783
|
}
|
|
3071
|
-
/** 懒连接:首次使用时初始化连接(中间件场景:构造即注入,首命令才连) */
|
|
3072
3784
|
readyPromise = null;
|
|
3073
3785
|
ensure() {
|
|
3074
3786
|
if (this.readyPromise) return this.readyPromise;
|
|
@@ -3079,91 +3791,324 @@ var RedisPool = class _RedisPool {
|
|
|
3079
3791
|
}
|
|
3080
3792
|
async init() {
|
|
3081
3793
|
const poolSize = this.opts.poolSize ?? 5;
|
|
3082
|
-
|
|
3083
|
-
Array.from({ length: poolSize }, () =>
|
|
3794
|
+
const conns = await Promise.all(
|
|
3795
|
+
Array.from({ length: poolSize }, async () => {
|
|
3796
|
+
const c = new PgConnection(this.opts);
|
|
3797
|
+
await c.connect();
|
|
3798
|
+
return c;
|
|
3799
|
+
})
|
|
3084
3800
|
);
|
|
3801
|
+
this.all = conns;
|
|
3802
|
+
this.available = [...conns];
|
|
3085
3803
|
this.readyPromise = Promise.resolve();
|
|
3086
3804
|
}
|
|
3087
|
-
/**
|
|
3088
|
-
|
|
3089
|
-
|
|
3805
|
+
/** 获取一个空闲连接(全忙则排队等待) */
|
|
3806
|
+
acquire() {
|
|
3807
|
+
if (this.closed) return Promise.reject(new ConnectionError("postgres: pool is closed"));
|
|
3808
|
+
if (this.available.length > 0) {
|
|
3809
|
+
return Promise.resolve(this.available.pop());
|
|
3810
|
+
}
|
|
3811
|
+
const timeoutMs = this.opts.acquireTimeoutMs ?? 3e4;
|
|
3812
|
+
return new Promise((resolve3, reject) => {
|
|
3813
|
+
const waiter = {
|
|
3814
|
+
resolve: resolve3,
|
|
3815
|
+
reject
|
|
3816
|
+
};
|
|
3817
|
+
if (timeoutMs > 0) {
|
|
3818
|
+
waiter.timer = setTimeout(() => {
|
|
3819
|
+
const idx = this.waiters.indexOf(waiter);
|
|
3820
|
+
if (idx >= 0) this.waiters.splice(idx, 1);
|
|
3821
|
+
reject(new TimeoutError("postgres: pool acquire", timeoutMs));
|
|
3822
|
+
}, timeoutMs);
|
|
3823
|
+
}
|
|
3824
|
+
this.waiters.push(waiter);
|
|
3825
|
+
});
|
|
3090
3826
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3827
|
+
release(conn) {
|
|
3828
|
+
if (this.closed) {
|
|
3829
|
+
conn.close();
|
|
3830
|
+
return;
|
|
3095
3831
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3832
|
+
if (!conn.connected) {
|
|
3833
|
+
conn.close();
|
|
3834
|
+
this.replenish();
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
this.dispatchAvailable(conn);
|
|
3099
3838
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3839
|
+
/** 连接可用事件统一入口:优先唤醒等待者,否则回空闲池(release 与 replenish 共用) */
|
|
3840
|
+
dispatchAvailable(conn) {
|
|
3841
|
+
const waiter = this.waiters.shift();
|
|
3842
|
+
if (waiter) {
|
|
3843
|
+
if (waiter.timer) clearTimeout(waiter.timer);
|
|
3844
|
+
waiter.resolve(conn);
|
|
3845
|
+
} else {
|
|
3846
|
+
this.available.push(conn);
|
|
3847
|
+
}
|
|
3104
3848
|
}
|
|
3105
|
-
|
|
3849
|
+
/** 坏连接剔除后异步重建(池容量保持)——就绪后走统一分发(唤醒 waiter) */
|
|
3850
|
+
replenish() {
|
|
3851
|
+
if (this.closed) return;
|
|
3852
|
+
const c = new PgConnection(this.opts);
|
|
3853
|
+
c.connect().then(() => {
|
|
3854
|
+
if (this.closed) {
|
|
3855
|
+
c.close();
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
this.all.push(c);
|
|
3859
|
+
this.dispatchAvailable(c);
|
|
3860
|
+
}).catch(() => {
|
|
3861
|
+
setTimeout(() => this.replenish(), 500);
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
async query(sql, params) {
|
|
3106
3865
|
await this.ensure();
|
|
3107
|
-
|
|
3866
|
+
const conn = await this.acquire();
|
|
3867
|
+
const start = performance.now();
|
|
3868
|
+
try {
|
|
3869
|
+
const rows = await conn.query(sql, params);
|
|
3870
|
+
this.opts.onQuery?.(sql, performance.now() - start, rows.length);
|
|
3871
|
+
return rows;
|
|
3872
|
+
} finally {
|
|
3873
|
+
this.release(conn);
|
|
3874
|
+
}
|
|
3108
3875
|
}
|
|
3109
|
-
|
|
3876
|
+
/** 注册表结构(元数据闭环:校验/类型推断的起点) */
|
|
3877
|
+
register(table, schema) {
|
|
3878
|
+
this.schemas.set(table, schema);
|
|
3879
|
+
}
|
|
3880
|
+
/** 写前校验 + 参数化插入(schema 驱动,脏数据源头拦截) */
|
|
3881
|
+
async insert(table, row) {
|
|
3882
|
+
const schema = this.schemas.get(table);
|
|
3883
|
+
if (!schema) {
|
|
3884
|
+
throw new ValidationError(`schema: table '${table}' not registered\u2014call register() first`);
|
|
3885
|
+
}
|
|
3886
|
+
validateRow(schema, row);
|
|
3887
|
+
const cols = Object.keys(row);
|
|
3888
|
+
if (cols.length === 0) throw new ValidationError("schema: insert requires at least one column");
|
|
3889
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
3890
|
+
const sql = `INSERT INTO ${table} (${cols.join(", ")}) VALUES (${placeholders})`;
|
|
3891
|
+
return this.query(sql, cols.map((c) => row[c]));
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* tagged template: sql\`SELECT * FROM t WHERE id = \${id}\`
|
|
3895
|
+
* 插值 = 参数(postgres.js 语义,防注入);表名必须硬编码(插值会被当参数)。
|
|
3896
|
+
* 对象插值自动 JSON.stringify → jsonb。
|
|
3897
|
+
*/
|
|
3898
|
+
tag(strings, ...values) {
|
|
3899
|
+
const { sql, params } = parseTagged(strings, values);
|
|
3900
|
+
return this.query(sql, params);
|
|
3901
|
+
}
|
|
3902
|
+
/** postgres.js 兼容事务 API: begin(fn)——fn 收到 tagged template 事务 sql */
|
|
3903
|
+
async begin(fn) {
|
|
3110
3904
|
await this.ensure();
|
|
3111
|
-
return this.
|
|
3905
|
+
return this.transaction(async (tx) => {
|
|
3906
|
+
const txSql = (strings, ...values) => {
|
|
3907
|
+
const { sql, params } = parseTagged(strings, values);
|
|
3908
|
+
return tx.query(sql, params);
|
|
3909
|
+
};
|
|
3910
|
+
return fn(txSql);
|
|
3911
|
+
});
|
|
3912
|
+
}
|
|
3913
|
+
/** 片段:可嵌套的 SQL 片段(postgres.js fragment 语义,条件过滤模式) */
|
|
3914
|
+
frag(strings, ...values) {
|
|
3915
|
+
const { sql, params } = parseTagged(strings, values);
|
|
3916
|
+
return { __fragment: { sql, params } };
|
|
3112
3917
|
}
|
|
3113
|
-
|
|
3918
|
+
/** 原生 SQL(DDL / 动态表名场景);$1 占位符 + 参数数组 */
|
|
3919
|
+
async unsafe(sql, params) {
|
|
3114
3920
|
await this.ensure();
|
|
3115
|
-
|
|
3921
|
+
const conn = await this.acquire();
|
|
3922
|
+
try {
|
|
3923
|
+
return await conn.query(sql, params);
|
|
3924
|
+
} finally {
|
|
3925
|
+
this.release(conn);
|
|
3926
|
+
}
|
|
3116
3927
|
}
|
|
3117
|
-
|
|
3928
|
+
/** 事务:固定在单个连接上执行整个 BEGIN→fn→COMMIT/ROLLBACK */
|
|
3929
|
+
async transaction(fn) {
|
|
3118
3930
|
await this.ensure();
|
|
3119
|
-
|
|
3931
|
+
const conn = await this.acquire();
|
|
3932
|
+
try {
|
|
3933
|
+
await conn.query("BEGIN");
|
|
3934
|
+
try {
|
|
3935
|
+
const result = await fn({ query: (sql, params) => conn.query(sql, params) });
|
|
3936
|
+
await conn.query("COMMIT");
|
|
3937
|
+
return result;
|
|
3938
|
+
} catch (e) {
|
|
3939
|
+
await conn.query("ROLLBACK").catch(() => {
|
|
3940
|
+
});
|
|
3941
|
+
throw e;
|
|
3942
|
+
}
|
|
3943
|
+
} finally {
|
|
3944
|
+
this.release(conn);
|
|
3945
|
+
}
|
|
3120
3946
|
}
|
|
3121
|
-
async
|
|
3122
|
-
|
|
3123
|
-
|
|
3947
|
+
async close() {
|
|
3948
|
+
this.closed = true;
|
|
3949
|
+
this.readyPromise = null;
|
|
3950
|
+
this.available = [];
|
|
3951
|
+
await Promise.all(this.all.map((c) => c.close()));
|
|
3952
|
+
this.all = [];
|
|
3953
|
+
const ws = this.waiters;
|
|
3954
|
+
this.waiters = [];
|
|
3955
|
+
for (const w of ws) {
|
|
3956
|
+
if (w.timer) clearTimeout(w.timer);
|
|
3957
|
+
w.reject(new ConnectionError("postgres: pool is closed"));
|
|
3958
|
+
}
|
|
3124
3959
|
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
return this.next().ttl(this.k(key));
|
|
3960
|
+
get size() {
|
|
3961
|
+
return this.available.length + this.waiters.length;
|
|
3128
3962
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3963
|
+
};
|
|
3964
|
+
function parseTagged(strings, values) {
|
|
3965
|
+
let sql = strings[0];
|
|
3966
|
+
const params = [];
|
|
3967
|
+
for (let i = 0; i < values.length; i++) {
|
|
3968
|
+
const frag = values[i]?.__fragment;
|
|
3969
|
+
if (frag) {
|
|
3970
|
+
const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
|
|
3971
|
+
params.push(frag.params[parseInt(idx, 10) - 1]);
|
|
3972
|
+
return `$${params.length}`;
|
|
3973
|
+
});
|
|
3974
|
+
sql += renumbered + strings[i + 1];
|
|
3975
|
+
} else {
|
|
3976
|
+
params.push(values[i]);
|
|
3977
|
+
sql += `$${params.length}` + strings[i + 1];
|
|
3978
|
+
}
|
|
3132
3979
|
}
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3980
|
+
return { sql, params };
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
// src/postgres/client.ts
|
|
3984
|
+
var MIGRATIONS_TABLE = "_weifuwu_migrations";
|
|
3985
|
+
function postgres(options) {
|
|
3986
|
+
const opts = typeof options === "string" ? { connection: options } : options ?? {};
|
|
3987
|
+
const connection = opts.connection ?? process.env.DATABASE_URL;
|
|
3988
|
+
if (!connection) {
|
|
3989
|
+
throw new Error(
|
|
3990
|
+
"postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
|
|
3991
|
+
);
|
|
3136
3992
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3993
|
+
const u = new URL(connection);
|
|
3994
|
+
const pool = new PgPool({
|
|
3995
|
+
host: u.hostname,
|
|
3996
|
+
port: Number(u.port || 5432),
|
|
3997
|
+
user: decodeURIComponent(u.username),
|
|
3998
|
+
password: decodeURIComponent(u.password),
|
|
3999
|
+
database: u.pathname.replace(/^\//, ""),
|
|
4000
|
+
poolSize: opts.max ?? opts.poolSize ?? 10,
|
|
4001
|
+
acquireTimeoutMs: opts.acquireTimeoutMs,
|
|
4002
|
+
statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
|
|
4003
|
+
onQuery: opts.onQuery
|
|
4004
|
+
});
|
|
4005
|
+
const sql = makeSql(pool);
|
|
4006
|
+
const mw = ((req, ctx, next) => {
|
|
4007
|
+
ctx.sql = sql;
|
|
4008
|
+
return next(req, ctx);
|
|
4009
|
+
});
|
|
4010
|
+
mw.__meta = { injects: ["sql"], depends: [] };
|
|
4011
|
+
mw.sql = sql;
|
|
4012
|
+
mw.migrate = async () => {
|
|
4013
|
+
await sql.unsafe(`
|
|
4014
|
+
CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
|
|
4015
|
+
name TEXT PRIMARY KEY,
|
|
4016
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
4017
|
+
)
|
|
4018
|
+
`);
|
|
4019
|
+
};
|
|
4020
|
+
mw.markMigrated = async (moduleName) => {
|
|
4021
|
+
await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
|
|
4022
|
+
moduleName
|
|
4023
|
+
]);
|
|
4024
|
+
};
|
|
4025
|
+
mw.isMigrated = async (moduleName) => {
|
|
4026
|
+
const rows = await sql.unsafe(`SELECT 1 FROM "_weifuwu_migrations" WHERE name = $1`, [moduleName]);
|
|
4027
|
+
return rows.length > 0;
|
|
4028
|
+
};
|
|
4029
|
+
mw.transaction = (async (fn) => {
|
|
4030
|
+
return sql.transaction(fn);
|
|
4031
|
+
});
|
|
4032
|
+
mw.poolStats = () => ({ active: 0, idle: pool.size, waiting: 0, max: pool.size });
|
|
4033
|
+
mw.close = () => pool.close();
|
|
4034
|
+
return mw;
|
|
4035
|
+
}
|
|
4036
|
+
var TaggedQuery = class {
|
|
4037
|
+
text;
|
|
4038
|
+
params;
|
|
4039
|
+
executor;
|
|
4040
|
+
constructor(text, params, executor) {
|
|
4041
|
+
this.text = text;
|
|
4042
|
+
this.params = params;
|
|
4043
|
+
this.executor = executor;
|
|
3140
4044
|
}
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
await this.ensure();
|
|
3144
|
-
return Number(await this.next().command("PUBLISH", channel, message));
|
|
4045
|
+
then(resolve3, reject) {
|
|
4046
|
+
return this.executor(this.text, this.params).then(resolve3, reject);
|
|
3145
4047
|
}
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
return new RedisSubscriber({ ...this.opts });
|
|
4048
|
+
catch(reject) {
|
|
4049
|
+
return this.executor(this.text, this.params).catch(reject);
|
|
3149
4050
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
await this.ensure();
|
|
3153
|
-
const v = await this.next().command("FLUSHDB");
|
|
3154
|
-
return String(v);
|
|
4051
|
+
finally(fn) {
|
|
4052
|
+
return this.executor(this.text, this.params).finally(fn);
|
|
3155
4053
|
}
|
|
3156
|
-
/**
|
|
3157
|
-
|
|
3158
|
-
this.
|
|
3159
|
-
this.readyPromise = null;
|
|
3160
|
-
await Promise.all(this.clients.map((c) => c.close()));
|
|
3161
|
-
this.clients = [];
|
|
4054
|
+
/** 嵌套片段(agent-platform 条件过滤模式) */
|
|
4055
|
+
get __fragment() {
|
|
4056
|
+
return { sql: this.text, params: this.params };
|
|
3162
4057
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
4058
|
+
};
|
|
4059
|
+
function makeSql(pool) {
|
|
4060
|
+
const sql = ((strings, ...values) => {
|
|
4061
|
+
const { sql: text, params } = parseTaggedFromPool(strings, values);
|
|
4062
|
+
return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
|
|
4063
|
+
});
|
|
4064
|
+
sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
|
|
4065
|
+
sql.query = (query, params) => wrapError(pool.query(query, params));
|
|
4066
|
+
sql.begin = (fn) => pool.begin(fn);
|
|
4067
|
+
sql.transaction = (fn) => pool.transaction(fn);
|
|
4068
|
+
sql.close = () => pool.close();
|
|
4069
|
+
return sql;
|
|
4070
|
+
}
|
|
4071
|
+
function parseTaggedFromPool(strings, values) {
|
|
4072
|
+
let sql = strings[0];
|
|
4073
|
+
const params = [];
|
|
4074
|
+
for (let i = 0; i < values.length; i++) {
|
|
4075
|
+
const frag = values[i]?.__fragment;
|
|
4076
|
+
if (frag) {
|
|
4077
|
+
const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
|
|
4078
|
+
params.push(frag.params[parseInt(idx, 10) - 1]);
|
|
4079
|
+
return `$${params.length}`;
|
|
4080
|
+
});
|
|
4081
|
+
sql += renumbered + strings[i + 1];
|
|
4082
|
+
} else {
|
|
4083
|
+
params.push(values[i]);
|
|
4084
|
+
sql += `$${params.length}` + strings[i + 1];
|
|
4085
|
+
}
|
|
3165
4086
|
}
|
|
4087
|
+
return { sql, params };
|
|
4088
|
+
}
|
|
4089
|
+
var PG_ERROR_MAP = {
|
|
4090
|
+
"23505": 409,
|
|
4091
|
+
// unique_violation
|
|
4092
|
+
"23503": 400,
|
|
4093
|
+
// foreign_key_violation
|
|
4094
|
+
"23502": 400,
|
|
4095
|
+
// not_null_violation
|
|
4096
|
+
"23514": 400,
|
|
4097
|
+
// check_violation
|
|
4098
|
+
"22P02": 400,
|
|
4099
|
+
// invalid_text_representation
|
|
4100
|
+
"22003": 400
|
|
4101
|
+
// numeric_value_out_of_range
|
|
3166
4102
|
};
|
|
4103
|
+
function wrapError(promise) {
|
|
4104
|
+
return promise.catch((err) => {
|
|
4105
|
+
const code = err?.code;
|
|
4106
|
+
if (code && PG_ERROR_MAP[code]) {
|
|
4107
|
+
throw new HttpError(`\u6570\u636E\u5E93\u9519\u8BEF: ${err.message}`, PG_ERROR_MAP[code]);
|
|
4108
|
+
}
|
|
4109
|
+
throw err;
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
3167
4112
|
|
|
3168
4113
|
// src/redis/client.ts
|
|
3169
4114
|
function redis(options) {
|
|
@@ -3187,44 +4132,6 @@ function redis(options) {
|
|
|
3187
4132
|
return mw;
|
|
3188
4133
|
}
|
|
3189
4134
|
|
|
3190
|
-
// src/response.ts
|
|
3191
|
-
function ok(data, init) {
|
|
3192
|
-
return Response.json(data, { status: 200, ...init });
|
|
3193
|
-
}
|
|
3194
|
-
function created(data, init) {
|
|
3195
|
-
return Response.json(data, { status: 201, ...init });
|
|
3196
|
-
}
|
|
3197
|
-
function noContent(init) {
|
|
3198
|
-
return new Response(null, { status: 204, ...init });
|
|
3199
|
-
}
|
|
3200
|
-
function badRequest(message) {
|
|
3201
|
-
return Response.json({ error: message ?? "Bad Request" }, { status: 400 });
|
|
3202
|
-
}
|
|
3203
|
-
function unauthorized(message) {
|
|
3204
|
-
return Response.json({ error: message ?? "Unauthorized" }, { status: 401 });
|
|
3205
|
-
}
|
|
3206
|
-
function forbidden(message) {
|
|
3207
|
-
return Response.json({ error: message ?? "Forbidden" }, { status: 403 });
|
|
3208
|
-
}
|
|
3209
|
-
function notFound(message) {
|
|
3210
|
-
return Response.json({ error: message ?? "Not Found" }, { status: 404 });
|
|
3211
|
-
}
|
|
3212
|
-
function conflict(message) {
|
|
3213
|
-
return Response.json({ error: message ?? "Conflict" }, { status: 409 });
|
|
3214
|
-
}
|
|
3215
|
-
function unprocessable(message) {
|
|
3216
|
-
return Response.json({ error: message ?? "Unprocessable Entity" }, { status: 422 });
|
|
3217
|
-
}
|
|
3218
|
-
function tooManyRequests(message) {
|
|
3219
|
-
return Response.json({ error: message ?? "Too Many Requests" }, { status: 429 });
|
|
3220
|
-
}
|
|
3221
|
-
function serverError(message) {
|
|
3222
|
-
return Response.json({ error: message ?? "Internal Server Error" }, { status: 500 });
|
|
3223
|
-
}
|
|
3224
|
-
function redirect(url, status = 302) {
|
|
3225
|
-
return new Response(null, { status, headers: { Location: url } });
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
4135
|
// src/request.ts
|
|
3229
4136
|
async function parseBody(req) {
|
|
3230
4137
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
@@ -3246,14 +4153,18 @@ export {
|
|
|
3246
4153
|
cors,
|
|
3247
4154
|
createMiddleware,
|
|
3248
4155
|
created,
|
|
4156
|
+
email,
|
|
3249
4157
|
forbidden,
|
|
3250
4158
|
noContent,
|
|
3251
4159
|
notFound,
|
|
3252
4160
|
ok,
|
|
3253
4161
|
parseBody,
|
|
3254
4162
|
postgres,
|
|
4163
|
+
queue,
|
|
4164
|
+
rateLimit,
|
|
3255
4165
|
redirect,
|
|
3256
4166
|
redis,
|
|
4167
|
+
sendSmtp,
|
|
3257
4168
|
serve,
|
|
3258
4169
|
serveStatic,
|
|
3259
4170
|
serverError,
|
|
@@ -3261,5 +4172,6 @@ export {
|
|
|
3261
4172
|
ui,
|
|
3262
4173
|
uiSsr,
|
|
3263
4174
|
unauthorized,
|
|
3264
|
-
unprocessable
|
|
4175
|
+
unprocessable,
|
|
4176
|
+
userSystem
|
|
3265
4177
|
};
|