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/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 hostname = options?.hostname ?? "0.0.0.0";
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
- if (err instanceof HttpError && err.status === 413) {
106
- res.writeHead(413, { "Content-Type": "text/plain" });
107
- res.end("Request Body Too Large");
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
- if (!(err instanceof HttpError) || err.status >= 500) {
111
- const url = req.url ?? "/";
112
- const method = req.method ?? "GET";
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 hostname;
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, hostname, () => {
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 hostname;
208
- return _cachedHostname || hostname;
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 this.errorHandler ? this.errorHandler(err, req, ctx) : new Response("Internal Server Error", { status: 500 });
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/ui/index.ts
1082
- import { build } from "esbuild";
1083
- import { readFile, stat } from "node:fs/promises";
1084
- import { resolve as resolve2 } from "node:path";
1085
- import { fileURLToPath } from "node:url";
1086
-
1087
- // src/ui/html-safe.ts
1088
- var HtmlSafe = class {
1089
- value;
1090
- constructor(value) {
1091
- this.value = value;
1092
- }
1093
- toString() {
1094
- return this.value;
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
- // src/client/vnode.ts
1099
- var ASYNC_MARK = "__wfAsyncComponent";
1100
- function isAsyncComponent(type) {
1101
- return typeof type === "function" && type?.[ASYNC_MARK] === true;
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/client/reactive.ts
1107
- function createReactiveState(dirty) {
1108
- const proxyCache = /* @__PURE__ */ new WeakMap();
1109
- const reactive = (target) => {
1110
- if (target === null || typeof target !== "object") return target;
1111
- if (proxyCache.has(target)) return proxyCache.get(target);
1112
- const proxy = new Proxy(target, {
1113
- set(target2, key, value) {
1114
- const old = Reflect.get(target2, key);
1115
- if (old === value) return true;
1116
- Reflect.set(target2, key, value);
1117
- dirty();
1118
- return true;
1119
- },
1120
- get(target2, key) {
1121
- const value = Reflect.get(target2, key);
1122
- if (typeof value === "object" && value !== null) return reactive(value);
1123
- return value;
1124
- },
1125
- deleteProperty(target2, key) {
1126
- if (Reflect.has(target2, key)) {
1127
- Reflect.deleteProperty(target2, key);
1128
- dirty();
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
- return true;
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
- proxyCache.set(target, proxy);
1134
- return proxy;
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
- return reactive({});
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/ui/ssr.ts
1140
- function escape(s) {
1141
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- var VOID_TAGS = /* @__PURE__ */ new Set([
1144
- "area",
1145
- "base",
1146
- "br",
1147
- "col",
1148
- "embed",
1149
- "hr",
1150
- "img",
1151
- "input",
1152
- "link",
1153
- "meta",
1154
- "param",
1155
- "source",
1156
- "track",
1157
- "wbr"
1158
- ]);
1159
- function classToString(v) {
1160
- if (typeof v === "string") return v;
1161
- if (Array.isArray(v)) return v.filter(Boolean).join(" ");
1162
- if (v && typeof v === "object") {
1163
- return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
1164
- }
1165
- return "";
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
- function styleToString(v) {
1168
- if (typeof v === "string") return v;
1169
- if (v && typeof v === "object") {
1170
- return Object.entries(v).filter(([, x]) => x != null).map(([k, x]) => `${k}:${typeof x === "number" ? x + "px" : String(x)}`).join(";");
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 createSsrContext(serverCtx, dataStore) {
1175
- const usedIds = /* @__PURE__ */ new Set();
1176
- const ui2 = {
1177
- $: () => createReactiveState(() => {
1178
- }),
1179
- dirty: () => {
1180
- },
1181
- render: () => {
1182
- },
1183
- selfId: (name) => {
1184
- if (usedIds.has(name)) throw new Error(`[weifuwu] Duplicate component ID: "${name}"`);
1185
- usedIds.add(name);
1186
- },
1187
- useMedia: () => {
1188
- },
1189
- useBreakpoint: () => {
1190
- },
1191
- usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
1192
- } })
1193
- };
1194
- const ctx = Object.create(serverCtx ?? {});
1195
- ctx.ui = ui2;
1196
- ctx.data = {
1197
- async get(key, fetcher) {
1198
- if (dataStore.has(key)) return dataStore.get(key);
1199
- if (!fetcher) return void 0;
1200
- const val = await fetcher();
1201
- dataStore.set(key, val);
1202
- return val;
1203
- },
1204
- set(key, val) {
1205
- dataStore.set(key, val);
1206
- },
1207
- has(key) {
1208
- return dataStore.has(key);
1209
- }
1210
- };
1211
- return ctx;
1441
+ function generateRefreshToken() {
1442
+ return randomBytes2(32).toString("hex");
1212
1443
  }
1213
- async function renderSsr(input, ctx) {
1214
- if (input == null || typeof input === "boolean") return "";
1215
- if (typeof input === "string" || typeof input === "number") return escape(String(input));
1216
- if (Array.isArray(input)) {
1217
- let s = "";
1218
- for (const c of input) s += await renderSsr(c, ctx);
1219
- return s;
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
- const vnode = input;
1222
- if (vnode.type === Portal) return renderSsr(vnode.props?.children, ctx);
1223
- if (vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
1224
- if (typeof vnode.type === "function") {
1225
- let def;
1226
- if (isAsyncComponent(vnode.type)) {
1227
- def = await vnode.type(ctx);
1228
- if (typeof def !== "function") {
1229
- throw new Error(
1230
- `asyncComponent factory <${vnode.type.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
1231
- );
1232
- }
1233
- } else {
1234
- def = vnode.type;
1235
- }
1236
- const childCtx = Object.create(ctx);
1237
- const renderFn = def(vnode.props ?? {}, childCtx);
1238
- if (typeof renderFn !== "function") {
1239
- throw new Error(
1240
- `Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
1241
- );
1242
- }
1243
- return renderSsr(renderFn(vnode.props ?? {}), childCtx);
1244
- }
1245
- const tag = vnode.type;
1246
- const props = vnode.props ?? {};
1247
- const attrs = [];
1248
- let innerHTML;
1249
- for (const [key, value] of Object.entries(props)) {
1250
- if (key === "children" || key === "key") continue;
1251
- if (key === "ref") continue;
1252
- if (key.startsWith("on") && typeof value === "function") continue;
1253
- if (key === "innerHTML") {
1254
- innerHTML = String(value ?? "");
1255
- continue;
1256
- }
1257
- if (key === "class" || key === "className") {
1258
- const cls = classToString(value);
1259
- if (cls) attrs.push(` class="${escape(cls)}"`);
1260
- continue;
1261
- }
1262
- if (key === "style" && value && typeof value === "object") {
1263
- attrs.push(` style="${escape(styleToString(value))}"`);
1264
- continue;
1265
- }
1266
- if (value === true) {
1267
- attrs.push(` ${key}`);
1268
- continue;
1269
- }
1270
- if (value === false || value == null) continue;
1271
- attrs.push(` ${key}="${escape(String(value))}"`);
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
- const attrStr = attrs.join("");
1274
- if (VOID_TAGS.has(tag)) return `<${tag}${attrStr}>`;
1275
- if (innerHTML !== void 0) return `<${tag}${attrStr}>${innerHTML}</${tag}>`;
1276
- const children = await renderSsr(props.children, ctx);
1277
- return `<${tag}${attrStr}>${children}</${tag}>`;
1278
- }
1279
- async function ssrToString(Comp, props, serverCtx, opts = {}) {
1280
- const dataStore = opts.data ?? /* @__PURE__ */ new Map();
1281
- const ctx = createSsrContext(serverCtx, dataStore);
1282
- const html = await renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
1283
- return new HtmlSafe(html);
1284
- }
1285
- function serializeData(data) {
1286
- const json = JSON.stringify(Object.fromEntries(data)).replace(/</g, "\\u003c");
1287
- return `<script>window.__DATA__=${json};</script>`;
1288
- }
1289
-
1290
- // src/ui/index.ts
1291
- function escape2(s) {
1292
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- return true;
1314
- }
1315
- var cssCache = /* @__PURE__ */ new Map();
1316
- var postcssAvailable;
1317
- async function checkPostcss() {
1318
- if (postcssAvailable !== void 0) return postcssAvailable;
1319
- try {
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.ui = {
1351
- html: Object.assign(htmlTag, { unsafe }),
1352
- /** SSR 渲染组件 → HTML 片段(HtmlSafe:内联进 ctx.ui.html 不二次转义) */
1353
- async ssr(Comp, props, opts) {
1354
- const safe = await ssrToString(Comp, props ?? {}, ctx, opts);
1355
- return safe;
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
- /** 序列化 SSR 数据存储 → window.__DATA__ 脚本(HtmlSafe) */
1358
- ssrData(data) {
1359
- return new HtmlSafe(serializeData(data));
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 js(entryPath) {
1362
- const absPath = resolveEntry(entryPath);
1363
- const cached = jsCache.get(absPath);
1364
- if (cached && await jsCacheFresh(cached.inputs)) {
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
- async css(entryPath) {
1394
- const absPath = resolveEntry(entryPath);
1395
- const st = await import("node:fs").then((fs) => fs.promises.stat(absPath));
1396
- const cached = cssCache.get(absPath);
1397
- if (cached && cached.mtime === st.mtimeMs) {
1398
- return new Response(cached.code, {
1399
- headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
1400
- });
1401
- }
1402
- let code = await readFile(absPath, "utf-8");
1403
- if (await checkPostcss()) {
1404
- try {
1405
- const postcss = await import("postcss");
1406
- const tw = await import("@tailwindcss/postcss");
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(_req, ctx);
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/client/route-match.ts
1426
- function joinPaths(a, b) {
1427
- if (!b || b === "/") return a || "/";
1428
- const left = a.endsWith("/") ? a.slice(0, -1) : a;
1429
- const right = b.startsWith("/") ? b : "/" + b;
1430
- return left + right;
1431
- }
1432
- function compilePath(path) {
1433
- const keys = [];
1434
- const reStr = path.replace(/:(\w+)/g, (_, key) => {
1435
- keys.push(key);
1436
- return "([^/]+)";
1437
- }).replace(/\*/g, ".*");
1438
- return { re: new RegExp(`^${reStr}$`), keys };
1439
- }
1440
- function flattenRoutes(routes, basePath = "", chain = []) {
1441
- const result = [];
1442
- for (const route of routes) {
1443
- const fullPath = joinPaths(basePath, route.path);
1444
- const { re, keys } = compilePath(fullPath);
1445
- const fullChain = [...chain, route];
1446
- result.push({ re, keys, def: route, chain: fullChain });
1447
- if (route.children) {
1448
- result.push(...flattenRoutes(route.children, fullPath, fullChain));
1449
- }
1450
- }
1451
- return result;
1452
- }
1453
- function matchRoute(path, routes) {
1454
- let best = null;
1455
- for (const fr of routes) {
1456
- if (path.match(fr.re)) {
1457
- if (!best || fr.chain.length > best.chain.length) best = fr;
1458
- }
1459
- }
1460
- return best;
1461
- }
1462
- function extractParams(path, match) {
1463
- const params = {};
1464
- const m = path.match(match.re);
1465
- if (m) {
1466
- for (let i = 0; i < match.keys.length; i++) {
1467
- params[match.keys[i]] = decodeURIComponent(m[i + 1]);
1468
- }
1469
- }
1470
- return params;
1471
- }
1666
+ // src/queue/index.ts
1667
+ import { randomUUID } from "node:crypto";
1668
+ import { hostname } from "node:os";
1472
1669
 
1473
- // src/ui/ssr-page.ts
1474
- var DEFAULT_TEMPLATE = (p) => `<!DOCTYPE html>
1475
- <html lang="zh-CN">
1476
- <head>
1477
- <meta charset="UTF-8">
1478
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1479
- <title>${escapeHtml(p.title)}</title>
1480
- ${(p.styles ?? []).map((s) => `<link rel="stylesheet" href="${escapeHtml(s)}">`).join("\n ")}
1481
- </head>
1482
- <body>
1483
- <div id="root">${p.html}</div>
1484
- ${p.dataScript}
1485
- ${p.bundle ? `<script src="${escapeHtml(p.bundle)}"></script>` : ""}
1486
- </body>
1487
- </html>`;
1488
- function escapeHtml(s) {
1489
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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/postgres/connection.ts
1518
- import net from "node:net";
1519
- import crypto2 from "node:crypto";
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/postgres/protocol.ts
1522
- function encodeMessage(type, payload) {
1523
- const out = new Uint8Array(1 + 4 + payload.length);
1524
- out[0] = type.charCodeAt(0);
1525
- const len = 4 + payload.length;
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
- body.push(0);
1543
- const out = new Uint8Array(4 + body.length);
1544
- const len = 4 + body.length;
1545
- out[0] = len >> 24 & 255;
1546
- out[1] = len >> 16 & 255;
1547
- out[2] = len >> 8 & 255;
1548
- out[3] = len & 255;
1549
- out.set(body, 4);
1550
- return out;
1551
- }
1552
- function queryMessage(sql) {
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 bindMessage(statement, params, paramFormats = []) {
1570
- const body = [...utf8(""), 0, ...utf8(statement), 0];
1571
- body.push(0, paramFormats.length);
1572
- for (const f of paramFormats) {
1573
- body.push(f >> 8 & 255, f & 255);
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
- body.push(0, params.length);
1576
- for (const p of params) {
1577
- if (p === null) {
1578
- body.push(255, 255, 255, 255);
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
- const bytes = typeof p === "string" ? utf8(p) : p;
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
- body.push(0, 0);
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 passwordMessage(password) {
1612
- return encodeMessage("p", utf8(password));
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 MessageStream = class {
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
- const msg = this.tryRead();
1623
- if (!msg) {
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
- /** 尝试读一条完整消息;不完整返回 null(不消费) */
1657
- tryRead() {
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
- const payload = this.buf.subarray(this.off + 5, this.off + 1 + len);
1663
- this.off += 1 + len;
1664
- return { type, payload };
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 authCode(msg) {
1668
- if (msg.type !== "R" || msg.payload.length < 4) return -1;
1669
- return msg.payload[0] << 24 | msg.payload[1] << 16 | msg.payload[2] << 8 | msg.payload[3];
1670
- }
1671
- function parseRowDescription(payload) {
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 out;
1910
+ return -1;
1751
1911
  }
1752
1912
 
1753
- // src/db/errors.ts
1754
- var DbError = class extends Error {
1755
- kind;
1756
- code;
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
- stream = new MessageStream();
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
- waiters = [];
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 ?? 5432,
1805
- user: options.user ?? "postgres",
1806
- database: options.database ?? "postgres",
1807
- connectTimeoutMs: options.connectTimeoutMs ?? 1e4,
1808
- statementTimeoutMs: options.statementTimeoutMs ?? 0,
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
- get connected() {
1813
- return this.status === "ready";
1814
- }
1942
+ /** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
1815
1943
  connect() {
1816
- return new Promise((resolve3, reject) => {
1817
- this.status = "connecting";
1818
- const sock = net.connect(this.opts.port, this.opts.host);
1819
- this.socket = sock;
1820
- const timeout = setTimeout(() => {
1821
- sock.destroy();
1822
- reject(new ConnectionError(`postgres: connect timeout after ${this.opts.connectTimeoutMs}ms`));
1823
- }, this.opts.connectTimeoutMs);
1824
- sock.once("connect", () => {
1825
- sock.setNoDelay(true);
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
- onReady = null;
1861
- onAuthFail = null;
1862
- expectingAuth = true;
1863
- authStage = "start";
1864
- authCtx = null;
1865
- prepared = /* @__PURE__ */ new Map();
1866
- // sig → stmt + 列缓存
1867
- stmtSeq = 0;
1868
- currentQuery = null;
1869
- onData(chunk) {
1870
- const msgs = this.stream.push(new Uint8Array(chunk));
1871
- for (const msg of msgs) {
1872
- this.handle(msg);
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
- handle(msg) {
1876
- if (this.expectingAuth) {
1877
- if (msg.type === "R") {
1878
- const code = authCode(msg);
1879
- if (code === 0) {
1880
- } else if (code === 3) {
1881
- this.send(passwordMessage(this.opts.password ?? ""));
1882
- } else if (code === 5) {
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
- return;
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 (msg.type === "E") {
1926
- const fields = parseErrorFields(msg.payload);
1927
- this.expectingAuth = false;
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
- if (msg.type === "Z") {
1933
- this.expectingAuth = false;
1934
- if (this.opts.statementTimeoutMs > 0 && !this.timeoutSet) {
1935
- this.timeoutSet = true;
1936
- this.awaitingReady = true;
1937
- this.socket?.write(
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
- switch (msg.type) {
1948
- case "T": {
1949
- if (this.currentQuery) {
1950
- this.currentQuery.columns = parseRowDescription(msg.payload);
1951
- const entry = this.prepared.get(
1952
- `${this.currentQuery.sql}|${this.currentQuery.params?.length ?? 0}`
1953
- );
1954
- if (entry) entry.columns = this.currentQuery.columns;
1955
- }
1956
- break;
1957
- }
1958
- case "D": {
1959
- const values = parseDataRow(msg.payload);
1960
- if (this.currentQuery && this.currentQuery.columns) {
1961
- const row = {};
1962
- this.currentQuery.columns.forEach((col, i) => {
1963
- row[col.name] = convertValue(col.typeOid, values[i] ?? null);
1964
- });
1965
- this.currentQuery.rows.push(row);
1966
- }
1967
- break;
1968
- }
1969
- case "C":
1970
- break;
1971
- // CommandComplete——忽略 tag
1972
- case "t": {
1973
- if (this.currentQuery?.awaitingDescribe && this.currentQuery.sql !== void 0) {
1974
- this.currentQuery.awaitingDescribe = false;
1975
- if (this.currentQuery.prepKey && this.currentQuery.prepName) {
1976
- this.prepared.set(this.currentQuery.prepKey, {
1977
- name: this.currentQuery.prepName,
1978
- columns: this.currentQuery.columns
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
- this.send(bindMessage(this.currentQuery.prepName ?? "", this.currentQuery.params ?? []));
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
- case "N":
2016
- break;
2017
- // NoticeResponse 忽略
2018
- default:
2019
- break;
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
- /** SCRAM client-final 消息 */
2023
- scramFinal(ctx) {
2024
- const parts = new Map(
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
- scramServerSignature(ctx) {
2045
- const parts = new Map(
2046
- ctx.serverFirst.split(",").map((kv) => {
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
- /** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
2061
- async transaction(fn) {
2062
- await this.query("BEGIN");
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
- /** 查询:无参数走简单协议(Q),有参数走扩展查询(Parse/Bind/Execute/Sync) */
2074
- query(sql, params) {
2075
- return new Promise((resolve3, reject) => {
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
- send(data) {
2130
- this.socket?.write(Buffer.from(data));
2340
+ /** PUBLISH 消息到频道,返回收到消息的订阅者数 */
2341
+ async publish(channel, message) {
2342
+ await this.ensure();
2343
+ return Number(await this.next().command("PUBLISH", channel, message));
2131
2344
  }
2132
- notifyIdle() {
2133
- const ws = this.waiters;
2134
- this.waiters = [];
2135
- for (const w of ws) w();
2345
+ /** 创建独立订阅者连接(Pub/Sub 场景) */
2346
+ createSubscriber() {
2347
+ return new RedisSubscriber({ ...this.opts });
2136
2348
  }
2137
- /** 等待连接空闲(事务/多语句流程用) */
2138
- waitIdle() {
2139
- if (!this.currentQuery) return Promise.resolve();
2140
- return new Promise((resolve3) => this.waiters.push(resolve3));
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
- const sock = this.socket;
2145
- if (this.status === "ready" && sock) {
2146
- sock.write(Buffer.from(terminateMessage()));
2147
- }
2148
- this.status = "closed";
2149
- if (sock) sock.destroy();
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
- function utf82(s) {
2153
- return new TextEncoder().encode(s);
2154
- }
2155
- function encodeP(payload) {
2156
- const out = new Uint8Array(1 + 4 + payload.length);
2157
- out[0] = 112;
2158
- const len = 4 + payload.length;
2159
- out[1] = len >> 24 & 255;
2160
- out[2] = len >> 16 & 255;
2161
- out[3] = len >> 8 & 255;
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 convertValue(oid, value) {
2182
- if (value === null) return null;
2183
- switch (oid) {
2184
- case 114:
2185
- case 3802:
2186
- return JSON.parse(value);
2187
- case 20: {
2188
- const n = BigInt(value);
2189
- if (n > BigInt(Number.MAX_SAFE_INTEGER) || n < BigInt(-Number.MAX_SAFE_INTEGER)) {
2190
- return value;
2191
- }
2192
- return Number(n);
2193
- }
2194
- case 21:
2195
- case 23:
2196
- case 26:
2197
- return Number(value);
2198
- case 700:
2199
- case 701:
2200
- case 1700:
2201
- return parseFloat(value);
2202
- case 16:
2203
- return value === "t";
2204
- default:
2205
- return value;
2206
- }
2207
- }
2208
-
2209
- // src/db/postgres/schema.ts
2210
- function validateValue(def, value, column) {
2211
- if (value === void 0 || value === null) {
2212
- if (def.required) {
2213
- throw new ValidationError(`schema: '${column}' is required`);
2214
- }
2215
- return;
2216
- }
2217
- switch (def.type) {
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
- break;
2223
- case "int":
2224
- if (typeof value !== "number" || !Number.isInteger(value)) {
2225
- throw new ValidationError(`schema: '${column}' must be an integer, got ${typeof value}`);
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
- break;
2228
- case "jsonb":
2229
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2230
- throw new ValidationError(`schema: '${column}' must be a JSON object`);
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
- break;
2233
- case "enum":
2234
- if (!def.values?.includes(value)) {
2235
- throw new ValidationError(
2236
- `schema: '${column}' must be one of [${def.values?.join(", ")}], got ${String(value)}`
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
- break;
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 validateRow(schema, row) {
2243
- for (const [col, def] of Object.entries(schema)) {
2244
- if (def.required && (row[col] === void 0 || row[col] === null)) {
2245
- throw new ValidationError(`schema: '${col}' is required`);
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
- for (const [col, value] of Object.entries(row)) {
2249
- const def = schema[col];
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/db/postgres/pool.ts
2256
- var PgPool = class _PgPool {
2257
- all = [];
2258
- available = [];
2259
- waiters = [];
2260
- closed = false;
2261
- opts;
2262
- initPromise = null;
2263
- schemas = /* @__PURE__ */ new Map();
2264
- /** 懒连接:构造不连接,ensure() 首次初始化(中间件注入场景) */
2265
- constructor(options = {}) {
2266
- this.opts = options;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- static async create(options = {}) {
2269
- const pool = new _PgPool(options);
2270
- await pool.ensure();
2271
- return pool;
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
- readyPromise = null;
2274
- ensure() {
2275
- if (this.readyPromise) return this.readyPromise;
2276
- if (!this.initPromise) {
2277
- this.initPromise = this.init();
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
- return this.initPromise;
2280
- }
2281
- async init() {
2282
- const poolSize = this.opts.poolSize ?? 5;
2283
- const conns = await Promise.all(
2284
- Array.from({ length: poolSize }, async () => {
2285
- const c = new PgConnection(this.opts);
2286
- await c.connect();
2287
- return c;
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
- acquire() {
2296
- if (this.closed) return Promise.reject(new ConnectionError("postgres: pool is closed"));
2297
- if (this.available.length > 0) {
2298
- return Promise.resolve(this.available.pop());
2299
- }
2300
- const timeoutMs = this.opts.acquireTimeoutMs ?? 3e4;
2301
- return new Promise((resolve3, reject) => {
2302
- const waiter = {
2303
- resolve: resolve3,
2304
- reject
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
- this.waiters.push(waiter);
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
- release(conn) {
2317
- if (this.closed) {
2318
- conn.close();
2319
- return;
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 (!conn.connected) {
2322
- conn.close();
2323
- this.replenish();
2324
- return;
2757
+ if (key === "class" || key === "className") {
2758
+ const cls = classToString(value);
2759
+ if (cls) attrs.push(` class="${escape(cls)}"`);
2760
+ continue;
2325
2761
  }
2326
- this.dispatchAvailable(conn);
2327
- }
2328
- /** 连接可用事件统一入口:优先唤醒等待者,否则回空闲池(release 与 replenish 共用) */
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
- /** 坏连接剔除后异步重建(池容量保持)——就绪后走统一分发(唤醒 waiter) */
2339
- replenish() {
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
- register(table, schema) {
2367
- this.schemas.set(table, schema);
2368
- }
2369
- /** 写前校验 + 参数化插入(schema 驱动,脏数据源头拦截) */
2370
- async insert(table, row) {
2371
- const schema = this.schemas.get(table);
2372
- if (!schema) {
2373
- throw new ValidationError(`schema: table '${table}' not registered\u2014call register() first`);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- * tagged template: sql\`SELECT * FROM t WHERE id = \${id}\`
2384
- * 插值 = 参数(postgres.js 语义,防注入);表名必须硬编码(插值会被当参数)。
2385
- * 对象插值自动 JSON.stringify → jsonb。
2386
- */
2387
- tag(strings, ...values) {
2388
- const { sql, params } = parseTagged(strings, values);
2389
- return this.query(sql, params);
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
- /** postgres.js 兼容事务 API: begin(fn)——fn 收到 tagged template 事务 sql */
2392
- async begin(fn) {
2393
- await this.ensure();
2394
- return this.transaction(async (tx) => {
2395
- const txSql = (strings, ...values) => {
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
- /** 片段:可嵌套的 SQL 片段(postgres.js fragment 语义,条件过滤模式) */
2403
- frag(strings, ...values) {
2404
- const { sql, params } = parseTagged(strings, values);
2405
- return { __fragment: { sql, params } };
2832
+ try {
2833
+ return fileURLToPath(import.meta.resolve(entryPath));
2834
+ } catch {
2835
+ return resolve2(entryPath);
2406
2836
  }
2407
- /** 原生 SQL(DDL / 动态表名场景);$1 占位符 + 参数数组 */
2408
- async unsafe(sql, params) {
2409
- await this.ensure();
2410
- const conn = await this.acquire();
2411
- try {
2412
- return await conn.query(sql, params);
2413
- } finally {
2414
- this.release(conn);
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
- /** 事务:固定在单个连接上执行整个 BEGIN→fn→COMMIT/ROLLBACK */
2418
- async transaction(fn) {
2419
- await this.ensure();
2420
- const conn = await this.acquire();
2421
- try {
2422
- await conn.query("BEGIN");
2423
- try {
2424
- const result = await fn({ query: (sql, params) => conn.query(sql, params) });
2425
- await conn.query("COMMIT");
2426
- return result;
2427
- } catch (e) {
2428
- await conn.query("ROLLBACK").catch(() => {
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
- throw e;
2431
- }
2432
- } finally {
2433
- this.release(conn);
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
- async close() {
2437
- this.closed = true;
2438
- this.readyPromise = null;
2439
- this.available = [];
2440
- await Promise.all(this.all.map((c) => c.close()));
2441
- this.all = [];
2442
- const ws = this.waiters;
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
- get size() {
2450
- return this.available.length + this.waiters.length;
2451
- }
2452
- };
2453
- function parseTagged(strings, values) {
2454
- let sql = strings[0];
2455
- const params = [];
2456
- for (let i = 0; i < values.length; i++) {
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 { sql, params };
2970
+ return params;
2470
2971
  }
2471
2972
 
2472
- // src/postgres/client.ts
2473
- var MIGRATIONS_TABLE = "_weifuwu_migrations";
2474
- function postgres(options) {
2475
- const opts = typeof options === "string" ? { connection: options } : options ?? {};
2476
- const connection = opts.connection ?? process.env.DATABASE_URL;
2477
- if (!connection) {
2478
- throw new Error(
2479
- "postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
2480
- );
2481
- }
2482
- const u = new URL(connection);
2483
- const pool = new PgPool({
2484
- host: u.hostname,
2485
- port: Number(u.port || 5432),
2486
- user: decodeURIComponent(u.username),
2487
- password: decodeURIComponent(u.password),
2488
- database: u.pathname.replace(/^\//, ""),
2489
- poolSize: opts.max ?? opts.poolSize ?? 10,
2490
- acquireTimeoutMs: opts.acquireTimeoutMs,
2491
- statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
2492
- onQuery: opts.onQuery
2493
- });
2494
- const sql = makeSql(pool);
2495
- const mw = ((req, ctx, next) => {
2496
- ctx.sql = sql;
2497
- return next(req, ctx);
2498
- });
2499
- mw.__meta = { injects: ["sql"], depends: [] };
2500
- mw.sql = sql;
2501
- mw.migrate = async () => {
2502
- await sql.unsafe(`
2503
- CREATE TABLE IF NOT EXISTS "_weifuwu_migrations" (
2504
- name TEXT PRIMARY KEY,
2505
- applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
2506
- )
2507
- `);
2508
- };
2509
- mw.markMigrated = async (moduleName) => {
2510
- await sql.unsafe(`INSERT INTO "_weifuwu_migrations" (name) VALUES ($1) ON CONFLICT DO NOTHING`, [
2511
- moduleName
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- var TaggedQuery = class {
2526
- text;
2527
- params;
2528
- executor;
2529
- constructor(text, params, executor) {
2530
- this.text = text;
2531
- this.params = params;
2532
- this.executor = executor;
2533
- }
2534
- then(resolve3, reject) {
2535
- return this.executor(this.text, this.params).then(resolve3, reject);
2536
- }
2537
- catch(reject) {
2538
- return this.executor(this.text, this.params).catch(reject);
2539
- }
2540
- finally(fn) {
2541
- return this.executor(this.text, this.params).finally(fn);
2542
- }
2543
- /** 嵌套片段(agent-platform 条件过滤模式) */
2544
- get __fragment() {
2545
- return { sql: this.text, params: this.params };
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
- function makeSql(pool) {
2549
- const sql = ((strings, ...values) => {
2550
- const { sql: text, params } = parseTaggedFromPool(strings, values);
2551
- return new TaggedQuery(text, params, (s, p) => wrapError(pool.query(s, p)));
2552
- });
2553
- sql.unsafe = (query, params) => wrapError(pool.unsafe(query, params));
2554
- sql.query = (query, params) => wrapError(pool.query(query, params));
2555
- sql.begin = (fn) => pool.begin(fn);
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 parseTaggedFromPool(strings, values) {
2561
- let sql = strings[0];
2562
- const params = [];
2563
- for (let i = 0; i < values.length; i++) {
2564
- const frag = values[i]?.__fragment;
2565
- if (frag) {
2566
- const renumbered = frag.sql.replace(/\$(\d+)/g, (_m, idx) => {
2567
- params.push(frag.params[parseInt(idx, 10) - 1]);
2568
- return `$${params.length}`;
2569
- });
2570
- sql += renumbered + strings[i + 1];
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
- params.push(values[i]);
2573
- sql += `$${params.length}` + strings[i + 1];
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
- return { sql, params };
3119
+ body[off] = 0;
3120
+ body[off + 1] = 0;
3121
+ return encodeMessage("B", body);
2577
3122
  }
2578
- var PG_ERROR_MAP = {
2579
- "23505": 409,
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
- // src/db/redis/connection.ts
2603
- import net2 from "node:net";
2604
-
2605
- // src/db/redis/resp.ts
2606
- var RespError = class extends DbError {
2607
- constructor(message) {
2608
- super("protocol", message, { code: "RESP" });
2609
- this.name = "RespError";
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
- var _decoder = new TextDecoder();
2630
- var _encoder = new TextEncoder();
2631
- var RespParser = class {
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
- try {
2655
- out.push(this.parseValue());
2656
- } catch (e) {
3160
+ const msg = this.tryRead();
3161
+ if (!msg) {
2657
3162
  this.off = saved;
2658
- if (e instanceof IncompleteError) break;
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
- parseValue() {
2691
- if (this.buf.length - this.off < 3) throw new IncompleteError();
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
- switch (type) {
2695
- case "+": {
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
- /** 读一行(到 \r\n),返回行内容(不含 type 字节与 \r\n),并推进 off */
2724
- readLine() {
2725
- const idx = indexOfCRLF(this.buf, this.off);
2726
- if (idx === -1) throw new IncompleteError();
2727
- const line = _decoder.decode(this.buf.subarray(this.off, idx));
2728
- this.off = idx + 2;
2729
- return line;
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
- /** 读 len 字节的 bulk 内容 + \r\n */
2732
- readBulk(len) {
2733
- if (this.buf.length - this.off < len + 2) throw new IncompleteError();
2734
- const value = _decoder.decode(this.buf.subarray(this.off, this.off + len));
2735
- this.off += len + 2;
2736
- return value;
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
- function indexOfCRLF(buf, from = 0) {
2740
- for (let i = from; i < buf.length - 1; i++) {
2741
- if (buf[i] === 13 && buf[i + 1] === 10) return i;
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 -1;
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/redis/connection.ts
2747
- var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
2748
- var RedisConnection = class {
2749
- ready = false;
3293
+ // src/db/postgres/connection.ts
3294
+ var PgConnection = class _PgConnection {
2750
3295
  opts;
2751
- socket = null;
2752
- parser = new RespParser();
2753
- pending = [];
2754
- offlineQueue = [];
2755
- subs = /* @__PURE__ */ new Map();
2756
- psubs = /* @__PURE__ */ new Map();
2757
- status = "idle";
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 ?? 6379,
2767
- retryDelayMs: options.retryDelayMs ?? 100,
2768
- maxRetries: options.maxRetries ?? 10,
2769
- enableOfflineQueue: options.enableOfflineQueue ?? true
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
- /** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
3314
+ get connected() {
3315
+ return this.status === "ready";
3316
+ }
2773
3317
  connect() {
2774
- if (this.connectPromise) return this.connectPromise;
2775
- this.closed = false;
2776
- this.connectPromise = new Promise((resolve3, reject) => {
2777
- this.openSocket();
2778
- this.onceReady = () => resolve3();
2779
- this.onceFailed = (err) => reject(err);
2780
- }).finally(() => {
2781
- this.connectPromise = null;
2782
- this.onceReady = void 0;
2783
- this.onceFailed = void 0;
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
- onceReady;
2788
- onceFailed;
2789
- openSocket() {
2790
- this.status = "connecting";
2791
- const sock = net2.connect(this.opts.port, this.opts.host);
2792
- this.socket = sock;
2793
- sock.on("connect", () => {
2794
- sock.setNoDelay(true);
2795
- this.status = "ready";
2796
- this.retries = 0;
2797
- this.flushOffline();
2798
- this.onceReady?.();
2799
- if (this.connectedOnce) {
2800
- for (const ch of this.subs.keys()) this.sendNow("SUBSCRIBE", [ch]);
2801
- for (const pat of this.psubs.keys()) this.sendNow("PSUBSCRIBE", [pat]);
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
- this.connectedOnce = true;
2804
- });
2805
- sock.on("data", (chunk) => this.onData(new Uint8Array(chunk)));
2806
- sock.on("error", (err) => {
2807
- if (this.status === "connecting") {
2808
- this.handleDisconnect(err);
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
- sock.on("close", () => {
2812
- this.socket = null;
2813
- if (this.status !== "closed" && this.status !== "idle") {
2814
- this.handleDisconnect(new ConnectionError("redis: socket closed"));
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
- const delay = Math.min(this.opts.retryDelayMs * 2 ** (this.retries - 1), 5e3);
2832
- this.reconnectTimer = setTimeout(() => {
2833
- this.reconnectTimer = null;
2834
- this.openSocket();
2835
- }, delay);
2836
- }
2837
- onData(chunk) {
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
- const p = this.pending.shift();
2846
- if (!p) break;
2847
- if (value instanceof RespError) p.reject(value);
2848
- else p.resolve(value);
3449
+ break;
2849
3450
  }
2850
- } catch (e) {
2851
- const queue = this.pending;
2852
- this.pending = [];
2853
- for (const q of queue) q.reject(e);
2854
- this.socket?.destroy();
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
- /** 发送命令并等待响应(单连接严格有序)。未 ready 时入离线队列(enableOfflineQueue)或拒绝。 */
2858
- command(name, ...args) {
2859
- if (this.status === "ready" && this.socket) {
2860
- return this.sendNow(name, args);
2861
- }
2862
- if (this.closed || this.status === "closed" || !this.opts.enableOfflineQueue) {
2863
- return Promise.reject(new ConnectionError("redis: not connected"));
2864
- }
2865
- return new Promise((resolve3, reject) => {
2866
- this.offlineQueue.push({ name, args, resolve: resolve3, reject });
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
- sendNow(name, args) {
2870
- return new Promise((resolve3, reject) => {
2871
- this.pending.push({ resolve: resolve3, reject });
2872
- this.socket.write(encodeCommand([name, ...args]));
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
- flushOffline() {
2876
- const queue = this.offlineQueue;
2877
- this.offlineQueue = [];
2878
- for (const q of queue) {
2879
- this.sendNow(q.name, q.args).then(q.resolve, q.reject);
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
- /** 批量执行:一次 write 发送所有命令字节,响应按序路由(管道) */
2883
- batch(payload, count) {
2884
- if (this.status !== "ready" || !this.socket) {
2885
- return Promise.reject(new ConnectionError("redis: not connected"));
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
- const results = [];
2889
- const maybeResolve = () => {
2890
- if (results.length === count) resolve3(results);
2891
- };
2892
- for (let i = 0; i < count; i++) {
2893
- this.pending.push({
2894
- // 正常响应与错误响应(RespError)都作为结果值收集——管道语义
2895
- resolve: (v) => {
2896
- results.push(v);
2897
- maybeResolve();
2898
- },
2899
- reject: (e) => {
2900
- results.push(e);
2901
- maybeResolve();
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
- /** 订阅频道:回调式(channel, message) */
2909
- async subscribe(channel, fn) {
2910
- this.subs.set(channel, fn);
2911
- await this.command("SUBSCRIBE", channel);
3640
+ send(data) {
3641
+ this.socket?.write(data);
2912
3642
  }
2913
- /** 订阅模式:回调式(channel, message) */
2914
- async psubscribe(pattern, fn) {
2915
- this.psubs.set(pattern, fn);
2916
- await this.command("PSUBSCRIBE", pattern);
3643
+ notifyIdle() {
3644
+ const ws = this.waiters;
3645
+ this.waiters = [];
3646
+ for (const w of ws) w();
2917
3647
  }
2918
- /** 旁路分发订阅消息(RESP 数组路由到回调) */
2919
- dispatchSubscribe(value) {
2920
- const [type, a, b] = value;
2921
- if (type === "message") {
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.socket = null;
2942
- if (sock) {
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
- get connected() {
2948
- return this.status === "ready";
3659
+ this.status = "closed";
3660
+ if (sock) sock.destroy();
2949
3661
  }
2950
3662
  };
2951
-
2952
- // src/db/redis/client.ts
2953
- var RedisClient = class _RedisClient {
2954
- conn;
2955
- constructor(conn) {
2956
- this.conn = conn;
2957
- }
2958
- /** 建立连接并返回就绪的客户端 */
2959
- static async connect(options = {}) {
2960
- const conn = new RedisConnection(options);
2961
- await conn.connect();
2962
- return new _RedisClient(conn);
2963
- }
2964
- /** 底层命令透传(RESP 值) */
2965
- command(name, ...args) {
2966
- return this.conn.command(name, ...args);
2967
- }
2968
- // ── 基础命令 ──────────────────────────────
2969
- async get(key) {
2970
- const v = await this.conn.command("GET", key);
2971
- return v === null ? null : String(v);
2972
- }
2973
- /**
2974
- * SET。ttl 秒可省略;传入即安全生效(内部转 SET key val EX ttl)。
2975
- */
2976
- async set(key, value, ttl) {
2977
- const reply = ttl !== void 0 ? await this.conn.command("SET", key, value, "EX", ttl) : await this.conn.command("SET", key, value);
2978
- return String(reply);
2979
- }
2980
- /** DEL 多 key,返回删除数量 */
2981
- async del(...keys) {
2982
- const v = await this.conn.command("DEL", ...keys);
2983
- return Number(v);
2984
- }
2985
- /** INCR,返回自增后的值 */
2986
- async incr(key) {
2987
- return Number(await this.conn.command("INCR", key));
2988
- }
2989
- /** EXPIRE,返回 1=设置成功 0=key 不存在 */
2990
- async expire(key, seconds) {
2991
- return Number(await this.conn.command("EXPIRE", key, seconds));
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
- /** JSON.stringify 后写入,可选 TTL */
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/redis/subscriber.ts
3031
- var RedisSubscriber = class {
3032
- conn;
3033
- constructor(options = {}) {
3034
- this.conn = new RedisConnection(options);
3035
- }
3036
- async connect() {
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
- subscribe(channel, fn) {
3041
- return this.conn.subscribe(channel, fn);
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
- psubscribe(pattern, fn) {
3045
- return this.conn.psubscribe(pattern, fn);
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
- async close() {
3048
- await this.conn.close();
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/redis/pool.ts
3053
- var RedisPool = class _RedisPool {
3054
- clients = [];
3055
- rr = 0;
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
- constructor(opts = {}) {
3062
- this.keyPrefix = opts.keyPrefix ?? "";
3063
- this.opts = 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 _RedisPool(options);
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
- this.clients = await Promise.all(
3083
- Array.from({ length: poolSize }, () => RedisClient.connect(this.opts))
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
- /** 应用 key 前缀(command 透传不加) */
3088
- k(key) {
3089
- return this.keyPrefix + key;
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
- /** round-robin 选择连接 */
3092
- next() {
3093
- if (this.closed || this.clients.length === 0) {
3094
- throw new ConnectionError("redis: pool is closed");
3827
+ release(conn) {
3828
+ if (this.closed) {
3829
+ conn.close();
3830
+ return;
3095
3831
  }
3096
- const client = this.clients[this.rr % this.clients.length];
3097
- this.rr++;
3098
- return client;
3832
+ if (!conn.connected) {
3833
+ conn.close();
3834
+ this.replenish();
3835
+ return;
3836
+ }
3837
+ this.dispatchAvailable(conn);
3099
3838
  }
3100
- // ── 与 RedisClient 相同的方法面(代理到轮询连接) ──
3101
- async command(name, ...args) {
3102
- await this.ensure();
3103
- return this.next().command(name, ...args);
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
- async get(key) {
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
- return this.next().get(this.k(key));
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
- async set(key, value, ttl) {
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.next().set(this.k(key), value, ttl);
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
- async del(...keys) {
3918
+ /** 原生 SQL(DDL / 动态表名场景);$1 占位符 + 参数数组 */
3919
+ async unsafe(sql, params) {
3114
3920
  await this.ensure();
3115
- return this.next().del(...keys.map((k) => this.k(k)));
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
- async incr(key) {
3928
+ /** 事务:固定在单个连接上执行整个 BEGIN→fn→COMMIT/ROLLBACK */
3929
+ async transaction(fn) {
3118
3930
  await this.ensure();
3119
- return this.next().incr(this.k(key));
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 expire(key, seconds) {
3122
- await this.ensure();
3123
- return this.next().expire(this.k(key), seconds);
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
- async ttl(key) {
3126
- await this.ensure();
3127
- return this.next().ttl(this.k(key));
3960
+ get size() {
3961
+ return this.available.length + this.waiters.length;
3128
3962
  }
3129
- async jsonGet(key) {
3130
- await this.ensure();
3131
- return this.next().jsonGet(this.k(key));
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
- async jsonSet(key, value, ttl) {
3134
- await this.ensure();
3135
- return this.next().jsonSet(this.k(key), value, ttl);
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
- async cache(key, fn, ttl) {
3138
- await this.ensure();
3139
- return this.next().cache(this.k(key), fn, ttl);
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
- /** PUBLISH 消息到频道,返回收到消息的订阅者数 */
3142
- async publish(channel, message) {
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
- /** 创建独立订阅者连接(Pub/Sub 场景) */
3147
- createSubscriber() {
3148
- return new RedisSubscriber({ ...this.opts });
4048
+ catch(reject) {
4049
+ return this.executor(this.text, this.params).catch(reject);
3149
4050
  }
3150
- /** 清空当前库(测试/重置场景) */
3151
- async flushdb() {
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
- async close() {
3158
- this.closed = true;
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
- get size() {
3164
- return this.clients.length;
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
  };