weifuwu 0.61.2 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -11
- package/dist/client/diff.d.ts +13 -1
- package/dist/client/index.js +3 -3
- package/dist/client/popup.d.ts +22 -0
- package/dist/client/types.d.ts +63 -0
- package/dist/client/ui.d.ts +6 -0
- package/dist/components/Accordion/Accordion.d.ts +9 -0
- package/dist/components/Affix/Affix.d.ts +16 -0
- package/dist/components/AspectRatio/AspectRatio.d.ts +10 -0
- package/dist/components/AvatarGroup/AvatarGroup.d.ts +21 -0
- package/dist/components/BackTop/BackTop.d.ts +16 -0
- package/dist/components/Calendar/Calendar.d.ts +21 -0
- package/dist/components/Carousel/Carousel.d.ts +17 -0
- package/dist/components/Cascader/Cascader.d.ts +21 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +23 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +14 -0
- package/dist/components/Collapse/Collapse.d.ts +28 -0
- package/dist/components/ColorPicker/ColorPicker.d.ts +16 -0
- package/dist/components/Command/Command.d.ts +22 -0
- package/dist/components/ContextMenu/ContextMenu.d.ts +17 -0
- package/dist/components/CopyButton/CopyButton.d.ts +18 -0
- package/dist/components/Descriptions/Descriptions.d.ts +21 -0
- package/dist/components/Highlight/Highlight.d.ts +13 -0
- package/dist/components/HoverCard/HoverCard.d.ts +16 -0
- package/dist/components/Icon/Icon.d.ts +1 -1
- package/dist/components/Img/Img.d.ts +4 -0
- package/dist/components/InView/InView.d.ts +1 -1
- package/dist/components/InfiniteScroll/InfiniteScroll.d.ts +15 -0
- package/dist/components/InputNumber/InputNumber.d.ts +25 -0
- package/dist/components/Label/Label.d.ts +11 -0
- package/dist/components/List/List.d.ts +18 -0
- package/dist/components/Markdown/Markdown.d.ts +14 -0
- package/dist/components/Markdown/parser.d.ts +32 -0
- package/dist/components/Mentions/Mentions.d.ts +19 -0
- package/dist/components/Menu/Menu.d.ts +25 -0
- package/dist/components/Menubar/Menubar.d.ts +22 -0
- package/dist/components/MessageBubble/MessageBubble.d.ts +20 -0
- package/dist/components/Notification/Notification.d.ts +70 -0
- package/dist/components/PasswordInput/PasswordInput.d.ts +21 -0
- package/dist/components/PinInput/PinInput.d.ts +18 -0
- package/dist/components/QRCode/QRCode.d.ts +20 -0
- package/dist/components/QRCode/qr.d.ts +15 -0
- package/dist/components/Rate/Rate.d.ts +16 -0
- package/dist/components/Resizable/Resizable.d.ts +18 -0
- package/dist/components/Result/Result.d.ts +17 -0
- package/dist/components/Select/Select.d.ts +4 -2
- package/dist/components/Table/Table.d.ts +10 -0
- package/dist/components/TagsInput/TagsInput.d.ts +21 -0
- package/dist/components/Timeline/Timeline.d.ts +25 -0
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +33 -0
- package/dist/components/Transfer/Transfer.d.ts +18 -0
- package/dist/components/Tree/Tree.d.ts +27 -0
- package/dist/components/Typography/Typography.d.ts +39 -0
- package/dist/components/VirtualList/VirtualList.d.ts +21 -0
- package/dist/components/Watermark/Watermark.d.ts +19 -0
- package/dist/components/index.d.ts +85 -0
- package/dist/components/index.js +14 -1
- package/dist/components/style.css +4824 -2211
- package/dist/db/postgres/connection.d.ts +24 -3
- package/dist/db/postgres/pool.d.ts +30 -8
- package/dist/db/redis/client.d.ts +43 -0
- package/dist/db/redis/connection.d.ts +12 -0
- package/dist/db/redis/pipeline.d.ts +26 -0
- package/dist/db/redis/pool.d.ts +27 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1055 -94
- package/dist/layout/weifuwu-layout.css +11 -1
- package/dist/postgres/types.d.ts +2 -1
- package/dist/redis/types.d.ts +6 -0
- package/dist/scheduler/cron.d.ts +26 -0
- package/dist/scheduler/index.d.ts +69 -0
- package/package.json +2 -4
package/dist/index.js
CHANGED
|
@@ -1087,6 +1087,9 @@ var MIME_TYPES = {
|
|
|
1087
1087
|
|
|
1088
1088
|
// src/middleware/rate-limit.ts
|
|
1089
1089
|
var PREFIX = "rl:";
|
|
1090
|
+
function requestIp(req) {
|
|
1091
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
1092
|
+
}
|
|
1090
1093
|
function rateLimit(options) {
|
|
1091
1094
|
const opts = {
|
|
1092
1095
|
windowMs: options?.windowMs ?? 6e4,
|
|
@@ -1171,7 +1174,8 @@ function rateLimit(options) {
|
|
|
1171
1174
|
const mw = (async (req, ctx, next) => {
|
|
1172
1175
|
if (opts.store === "redis") requireRedis(ctx);
|
|
1173
1176
|
ctx.limit = async (name, manual) => {
|
|
1174
|
-
const
|
|
1177
|
+
const dim = (manual?.scope ?? "ip") === "global" ? "" : `:${requestIp(req)}`;
|
|
1178
|
+
const key = `${PREFIX}${name}${dim}`;
|
|
1175
1179
|
const max = manual?.max ?? opts.max;
|
|
1176
1180
|
const windowMs = manual?.windowMs ?? opts.windowMs;
|
|
1177
1181
|
const r2 = await check(key, max, windowMs);
|
|
@@ -1681,6 +1685,7 @@ function userSystem(options) {
|
|
|
1681
1685
|
}
|
|
1682
1686
|
|
|
1683
1687
|
// src/messager/index.ts
|
|
1688
|
+
var messagerSeq = 0;
|
|
1684
1689
|
var CONVERSATIONS = "_weifuwu_conversations";
|
|
1685
1690
|
var MEMBERS = "_weifuwu_conversation_members";
|
|
1686
1691
|
var MESSAGES = "_weifuwu_messages";
|
|
@@ -1835,16 +1840,19 @@ function messager(options) {
|
|
|
1835
1840
|
redisSub.psubscribe(`${REDIS_PREFIX}*`, (channel, message) => {
|
|
1836
1841
|
const room = channel.slice(REDIS_PREFIX.length);
|
|
1837
1842
|
const event = JSON.parse(message);
|
|
1843
|
+
if (event._pid === SELF_PID) return;
|
|
1838
1844
|
broadcastLocal(room, event);
|
|
1839
1845
|
});
|
|
1840
1846
|
}).catch((err) => {
|
|
1841
1847
|
console.error("[messager] redis subscriber init error:", err);
|
|
1842
1848
|
});
|
|
1843
1849
|
}
|
|
1850
|
+
const SELF_PID = `wf:${process.pid}:${++messagerSeq}`;
|
|
1844
1851
|
function broadcastLocal(room, event) {
|
|
1845
1852
|
const members = rooms.get(room);
|
|
1846
1853
|
if (!members) return;
|
|
1847
|
-
const
|
|
1854
|
+
const { _pid, ...clean } = event;
|
|
1855
|
+
const payload = JSON.stringify(clean);
|
|
1848
1856
|
for (const ws of [...members]) {
|
|
1849
1857
|
if (ws.readyState === ws.OPEN) {
|
|
1850
1858
|
try {
|
|
@@ -1858,7 +1866,7 @@ function messager(options) {
|
|
|
1858
1866
|
broadcastLocal(room, event);
|
|
1859
1867
|
const pool = options.redis?.redis;
|
|
1860
1868
|
if (pool) {
|
|
1861
|
-
pool.publish(`${REDIS_PREFIX}${room}`, JSON.stringify(event)).catch((err) => {
|
|
1869
|
+
pool.publish(`${REDIS_PREFIX}${room}`, JSON.stringify({ ...event, _pid: SELF_PID })).catch((err) => {
|
|
1862
1870
|
console.error("[messager] redis publish error:", err);
|
|
1863
1871
|
});
|
|
1864
1872
|
}
|
|
@@ -2326,6 +2334,7 @@ function indexOfCRLF(buf, from = 0) {
|
|
|
2326
2334
|
|
|
2327
2335
|
// src/db/redis/connection.ts
|
|
2328
2336
|
var PUSH_TYPES = /* @__PURE__ */ new Set(["message", "pmessage"]);
|
|
2337
|
+
var BLOCKING_COMMANDS = /* @__PURE__ */ new Set(["BLPOP", "BRPOP", "BLMPOP", "BRPOPLPUSH", "BZPOPMIN", "BZPOPMAX", "WAIT", "XREAD", "XREADGROUP"]);
|
|
2329
2338
|
var RedisConnection = class {
|
|
2330
2339
|
ready = false;
|
|
2331
2340
|
opts;
|
|
@@ -2340,6 +2349,7 @@ var RedisConnection = class {
|
|
|
2340
2349
|
status = "idle";
|
|
2341
2350
|
retries = 0;
|
|
2342
2351
|
reconnectTimer = null;
|
|
2352
|
+
socketTimeoutTimer = null;
|
|
2343
2353
|
connectPromise = null;
|
|
2344
2354
|
closed = false;
|
|
2345
2355
|
connectedOnce = false;
|
|
@@ -2350,7 +2360,10 @@ var RedisConnection = class {
|
|
|
2350
2360
|
retryDelayMs: options.retryDelayMs ?? 100,
|
|
2351
2361
|
maxRetries: options.maxRetries ?? 10,
|
|
2352
2362
|
enableOfflineQueue: options.enableOfflineQueue ?? true,
|
|
2353
|
-
maxOfflineQueue: options.maxOfflineQueue ?? 5e3
|
|
2363
|
+
maxOfflineQueue: options.maxOfflineQueue ?? 5e3,
|
|
2364
|
+
commandTimeoutMs: options.commandTimeoutMs ?? 0,
|
|
2365
|
+
socketTimeoutMs: options.socketTimeoutMs ?? 0,
|
|
2366
|
+
onCommand: options.onCommand
|
|
2354
2367
|
};
|
|
2355
2368
|
}
|
|
2356
2369
|
/** 建立连接并等待 ready。重连失败(超过 maxRetries)抛 ConnectionError。 */
|
|
@@ -2404,8 +2417,10 @@ var RedisConnection = class {
|
|
|
2404
2417
|
this.pending = [];
|
|
2405
2418
|
this.pendingHead = 0;
|
|
2406
2419
|
for (const p of queue2) p.reject(err);
|
|
2420
|
+
this.clearSocketTimeout();
|
|
2407
2421
|
if (this.closed || this.status === "closed") return;
|
|
2408
2422
|
this.retries++;
|
|
2423
|
+
this.status = "connecting";
|
|
2409
2424
|
if (this.opts.maxRetries > 0 && this.retries > this.opts.maxRetries) {
|
|
2410
2425
|
this.status = "closed";
|
|
2411
2426
|
const failErr = err instanceof ConnectionError ? err : new ConnectionError(`redis: connect to ${this.opts.host}:${this.opts.port} failed`, this.retries, err);
|
|
@@ -2429,6 +2444,7 @@ var RedisConnection = class {
|
|
|
2429
2444
|
}
|
|
2430
2445
|
const p = this.pending[this.pendingHead++];
|
|
2431
2446
|
if (!p) break;
|
|
2447
|
+
if (p.timedOut) continue;
|
|
2432
2448
|
if (raw instanceof RespError) p.reject(raw);
|
|
2433
2449
|
else p.resolve(decodeValue(raw, !p.asBuffer));
|
|
2434
2450
|
}
|
|
@@ -2436,6 +2452,10 @@ var RedisConnection = class {
|
|
|
2436
2452
|
this.pending = this.pending.slice(this.pendingHead);
|
|
2437
2453
|
this.pendingHead = 0;
|
|
2438
2454
|
}
|
|
2455
|
+
if (this.opts.socketTimeoutMs > 0) {
|
|
2456
|
+
if (this.pendingHead < this.pending.length) this.armSocketTimeout();
|
|
2457
|
+
else this.clearSocketTimeout();
|
|
2458
|
+
}
|
|
2439
2459
|
} catch (e) {
|
|
2440
2460
|
const queue2 = this.pending.slice(this.pendingHead);
|
|
2441
2461
|
this.pending = [];
|
|
@@ -2465,10 +2485,60 @@ var RedisConnection = class {
|
|
|
2465
2485
|
});
|
|
2466
2486
|
}
|
|
2467
2487
|
sendNow(name, args, asBuffer) {
|
|
2488
|
+
const start = performance.now();
|
|
2468
2489
|
return new Promise((resolve3, reject) => {
|
|
2469
|
-
|
|
2490
|
+
const p = {
|
|
2491
|
+
resolve: resolve3,
|
|
2492
|
+
reject,
|
|
2493
|
+
asBuffer,
|
|
2494
|
+
blocking: BLOCKING_COMMANDS.has(name.toUpperCase())
|
|
2495
|
+
};
|
|
2496
|
+
this.armTimeout(p);
|
|
2497
|
+
this.pending.push(p);
|
|
2498
|
+
this.armSocketTimeout();
|
|
2470
2499
|
this.socket.write(encodeCommand([name, ...args]));
|
|
2471
|
-
})
|
|
2500
|
+
}).then(
|
|
2501
|
+
(v) => {
|
|
2502
|
+
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
2503
|
+
return v;
|
|
2504
|
+
},
|
|
2505
|
+
(e) => {
|
|
2506
|
+
if (this.opts.onCommand) this.opts.onCommand(name, args, performance.now() - start);
|
|
2507
|
+
throw e;
|
|
2508
|
+
}
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
/** 命令超时(commandTimeoutMs > 0):超时标记 timedOut + 处理(阻塞命令 resolve(null),其余 reject) */
|
|
2512
|
+
armTimeout(p) {
|
|
2513
|
+
const ms = this.opts.commandTimeoutMs;
|
|
2514
|
+
if (ms <= 0) return;
|
|
2515
|
+
p.timer = setTimeout(() => {
|
|
2516
|
+
p.timedOut = true;
|
|
2517
|
+
if (p.blocking) p.resolve(null);
|
|
2518
|
+
else p.reject(new TimeoutError("redis: command timeout", ms));
|
|
2519
|
+
}, ms);
|
|
2520
|
+
}
|
|
2521
|
+
/** socket 响应超时(socketTimeoutMs > 0):期望数据但超时未达 → 僵尸连接 → 主动断开走标准重连 */
|
|
2522
|
+
armSocketTimeout() {
|
|
2523
|
+
const ms = this.opts.socketTimeoutMs;
|
|
2524
|
+
if (ms <= 0) return;
|
|
2525
|
+
this.clearSocketTimeout();
|
|
2526
|
+
this.socketTimeoutTimer = setTimeout(() => {
|
|
2527
|
+
this.socketTimeoutTimer = null;
|
|
2528
|
+
const err = new ConnectionError(`redis: socket timeout (no data in ${ms}ms)`);
|
|
2529
|
+
const queue2 = this.pending.slice(this.pendingHead);
|
|
2530
|
+
this.pending = [];
|
|
2531
|
+
this.pendingHead = 0;
|
|
2532
|
+
for (const q of queue2) q.reject(err);
|
|
2533
|
+
this.clearSocketTimeout();
|
|
2534
|
+
this.socket?.destroy();
|
|
2535
|
+
}, ms);
|
|
2536
|
+
}
|
|
2537
|
+
clearSocketTimeout() {
|
|
2538
|
+
if (this.socketTimeoutTimer) {
|
|
2539
|
+
clearTimeout(this.socketTimeoutTimer);
|
|
2540
|
+
this.socketTimeoutTimer = null;
|
|
2541
|
+
}
|
|
2472
2542
|
}
|
|
2473
2543
|
flushOffline() {
|
|
2474
2544
|
const queue2 = this.offlineQueue;
|
|
@@ -2484,11 +2554,18 @@ var RedisConnection = class {
|
|
|
2484
2554
|
}
|
|
2485
2555
|
return new Promise((resolve3, reject) => {
|
|
2486
2556
|
const results = [];
|
|
2557
|
+
const batchPending = [];
|
|
2558
|
+
let settled = false;
|
|
2487
2559
|
const maybeResolve = () => {
|
|
2488
|
-
if (
|
|
2560
|
+
if (settled) return;
|
|
2561
|
+
if (results.length === count) {
|
|
2562
|
+
settled = true;
|
|
2563
|
+
if (batchTimer) clearTimeout(batchTimer);
|
|
2564
|
+
resolve3(results);
|
|
2565
|
+
}
|
|
2489
2566
|
};
|
|
2490
2567
|
for (let i = 0; i < count; i++) {
|
|
2491
|
-
|
|
2568
|
+
const p = {
|
|
2492
2569
|
// 正常响应与错误响应(RespError)都作为结果值收集——管道语义
|
|
2493
2570
|
resolve: (v) => {
|
|
2494
2571
|
results.push(v);
|
|
@@ -2498,9 +2575,21 @@ var RedisConnection = class {
|
|
|
2498
2575
|
results.push(e);
|
|
2499
2576
|
maybeResolve();
|
|
2500
2577
|
}
|
|
2501
|
-
}
|
|
2578
|
+
};
|
|
2579
|
+
batchPending.push(p);
|
|
2580
|
+
this.pending.push(p);
|
|
2581
|
+
}
|
|
2582
|
+
let batchTimer;
|
|
2583
|
+
const ms = this.opts.commandTimeoutMs;
|
|
2584
|
+
if (ms > 0) {
|
|
2585
|
+
batchTimer = setTimeout(() => {
|
|
2586
|
+
settled = true;
|
|
2587
|
+
for (const p of batchPending) p.timedOut = true;
|
|
2588
|
+
reject(new TimeoutError("redis: batch timeout", ms));
|
|
2589
|
+
}, ms);
|
|
2502
2590
|
}
|
|
2503
2591
|
this.socket.write(payload);
|
|
2592
|
+
this.armSocketTimeout();
|
|
2504
2593
|
});
|
|
2505
2594
|
}
|
|
2506
2595
|
/** 订阅频道:回调式(channel, message) */
|
|
@@ -2532,6 +2621,7 @@ var RedisConnection = class {
|
|
|
2532
2621
|
clearTimeout(this.reconnectTimer);
|
|
2533
2622
|
this.reconnectTimer = null;
|
|
2534
2623
|
}
|
|
2624
|
+
this.clearSocketTimeout();
|
|
2535
2625
|
const queue2 = this.pending.slice(this.pendingHead);
|
|
2536
2626
|
this.pending = [];
|
|
2537
2627
|
this.pendingHead = 0;
|
|
@@ -2548,6 +2638,55 @@ var RedisConnection = class {
|
|
|
2548
2638
|
}
|
|
2549
2639
|
};
|
|
2550
2640
|
|
|
2641
|
+
// src/db/redis/pipeline.ts
|
|
2642
|
+
var RedisPipeline = class {
|
|
2643
|
+
cmds = [];
|
|
2644
|
+
conn;
|
|
2645
|
+
constructor(conn) {
|
|
2646
|
+
this.conn = conn;
|
|
2647
|
+
}
|
|
2648
|
+
set(key, value, ttl) {
|
|
2649
|
+
this.cmds.push(ttl !== void 0 ? { name: "SET", args: [key, value, "EX", ttl] } : { name: "SET", args: [key, value] });
|
|
2650
|
+
return this;
|
|
2651
|
+
}
|
|
2652
|
+
get(key) {
|
|
2653
|
+
this.cmds.push({ name: "GET", args: [key] });
|
|
2654
|
+
return this;
|
|
2655
|
+
}
|
|
2656
|
+
del(...keys) {
|
|
2657
|
+
this.cmds.push({ name: "DEL", args: keys });
|
|
2658
|
+
return this;
|
|
2659
|
+
}
|
|
2660
|
+
incr(key) {
|
|
2661
|
+
this.cmds.push({ name: "INCR", args: [key] });
|
|
2662
|
+
return this;
|
|
2663
|
+
}
|
|
2664
|
+
expire(key, seconds) {
|
|
2665
|
+
this.cmds.push({ name: "EXPIRE", args: [key, seconds] });
|
|
2666
|
+
return this;
|
|
2667
|
+
}
|
|
2668
|
+
ttl(key) {
|
|
2669
|
+
this.cmds.push({ name: "TTL", args: [key] });
|
|
2670
|
+
return this;
|
|
2671
|
+
}
|
|
2672
|
+
/** 任意命令透传 */
|
|
2673
|
+
raw(name, ...args) {
|
|
2674
|
+
this.cmds.push({ name, args });
|
|
2675
|
+
return this;
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* 一次往返执行所有命令。结果按命令顺序;错误命令对应位置为 Error 实例,
|
|
2679
|
+
* 其余命令不受影响(Redis 管道语义)。
|
|
2680
|
+
*/
|
|
2681
|
+
async exec() {
|
|
2682
|
+
const cmds = this.cmds;
|
|
2683
|
+
this.cmds = [];
|
|
2684
|
+
if (cmds.length === 0) return [];
|
|
2685
|
+
const payload = Buffer.concat(cmds.map((c) => Buffer.from(encodeCommand([c.name, ...c.args]))));
|
|
2686
|
+
return this.conn.batch(payload, cmds.length);
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
|
|
2551
2690
|
// src/db/redis/client.ts
|
|
2552
2691
|
var RedisClient = class _RedisClient {
|
|
2553
2692
|
conn;
|
|
@@ -2564,6 +2703,10 @@ var RedisClient = class _RedisClient {
|
|
|
2564
2703
|
command(name, ...args) {
|
|
2565
2704
|
return this.conn.command(name, ...args);
|
|
2566
2705
|
}
|
|
2706
|
+
/** 连接是否就绪(池坏连接剔除用) */
|
|
2707
|
+
get connected() {
|
|
2708
|
+
return this.conn.connected;
|
|
2709
|
+
}
|
|
2567
2710
|
// ── 基础命令 ──────────────────────────────
|
|
2568
2711
|
async get(key) {
|
|
2569
2712
|
const v = await this.conn.command("GET", key);
|
|
@@ -2602,6 +2745,104 @@ var RedisClient = class _RedisClient {
|
|
|
2602
2745
|
async ttl(key) {
|
|
2603
2746
|
return Number(await this.conn.command("TTL", key));
|
|
2604
2747
|
}
|
|
2748
|
+
// ── 批量 / 存在 / 计数 ────────────────────────
|
|
2749
|
+
/** 批量读:返回与 key 一一对应的值(缺失为 null) */
|
|
2750
|
+
async mget(...keys) {
|
|
2751
|
+
const v = await this.conn.command("MGET", ...keys);
|
|
2752
|
+
return v.map((x) => x === null ? null : String(x));
|
|
2753
|
+
}
|
|
2754
|
+
/** 批量写:mset(k1, v1, k2, v2, ...)(偶数参数) */
|
|
2755
|
+
async mset(...kv) {
|
|
2756
|
+
const reply = await this.conn.command("MSET", ...kv);
|
|
2757
|
+
return String(reply);
|
|
2758
|
+
}
|
|
2759
|
+
/** 存在性:返回存在的 key 数 */
|
|
2760
|
+
async exists(...keys) {
|
|
2761
|
+
return Number(await this.conn.command("EXISTS", ...keys));
|
|
2762
|
+
}
|
|
2763
|
+
/** 原子设置(仅 key 不存在时):1=设置成功 0=已存在(分布式锁基础) */
|
|
2764
|
+
async setnx(key, value) {
|
|
2765
|
+
return Number(await this.conn.command("SETNX", key, value));
|
|
2766
|
+
}
|
|
2767
|
+
/** 原子加增量,返回新值 */
|
|
2768
|
+
async incrby(key, delta) {
|
|
2769
|
+
return Number(await this.conn.command("INCRBY", key, delta));
|
|
2770
|
+
}
|
|
2771
|
+
// ── hash ─────────────────────────────────────
|
|
2772
|
+
/** 设置字段,返回新增字段数 */
|
|
2773
|
+
async hset(key, field, value) {
|
|
2774
|
+
return Number(await this.conn.command("HSET", key, field, value));
|
|
2775
|
+
}
|
|
2776
|
+
/** 读字段(缺失 null) */
|
|
2777
|
+
async hget(key, field) {
|
|
2778
|
+
const v = await this.conn.command("HGET", key, field);
|
|
2779
|
+
return v === null ? null : String(v);
|
|
2780
|
+
}
|
|
2781
|
+
/** 读整个 hash → Record(key 不存在 → {}) */
|
|
2782
|
+
async hgetall(key) {
|
|
2783
|
+
const v = await this.conn.command("HGETALL", key);
|
|
2784
|
+
const arr = v;
|
|
2785
|
+
const out = {};
|
|
2786
|
+
for (let i = 0; i + 1 < arr.length; i += 2) out[String(arr[i])] = String(arr[i + 1]);
|
|
2787
|
+
return out;
|
|
2788
|
+
}
|
|
2789
|
+
/** 删除字段,返回删除数 */
|
|
2790
|
+
async hdel(key, ...fields) {
|
|
2791
|
+
return Number(await this.conn.command("HDEL", key, ...fields));
|
|
2792
|
+
}
|
|
2793
|
+
// ── list ─────────────────────────────────────
|
|
2794
|
+
/** 头插,返回长度 */
|
|
2795
|
+
async lpush(key, ...values) {
|
|
2796
|
+
return Number(await this.conn.command("LPUSH", key, ...values));
|
|
2797
|
+
}
|
|
2798
|
+
/** 尾插,返回长度 */
|
|
2799
|
+
async rpush(key, ...values) {
|
|
2800
|
+
return Number(await this.conn.command("RPUSH", key, ...values));
|
|
2801
|
+
}
|
|
2802
|
+
/** 头弹(缺失 null) */
|
|
2803
|
+
async lpop(key) {
|
|
2804
|
+
const v = await this.conn.command("LPOP", key);
|
|
2805
|
+
return v === null ? null : String(v);
|
|
2806
|
+
}
|
|
2807
|
+
/** 尾弹(缺失 null) */
|
|
2808
|
+
async rpop(key) {
|
|
2809
|
+
const v = await this.conn.command("RPOP", key);
|
|
2810
|
+
return v === null ? null : String(v);
|
|
2811
|
+
}
|
|
2812
|
+
/** 范围读(start/stop 支持负数,如 0 -1 全量) */
|
|
2813
|
+
async lrange(key, start, stop) {
|
|
2814
|
+
const v = await this.conn.command("LRANGE", key, start, stop);
|
|
2815
|
+
return v.map(String);
|
|
2816
|
+
}
|
|
2817
|
+
// ── set ──────────────────────────────────────
|
|
2818
|
+
/** 加成员,返回新增数(重复不加) */
|
|
2819
|
+
async sadd(key, ...members) {
|
|
2820
|
+
return Number(await this.conn.command("SADD", key, ...members));
|
|
2821
|
+
}
|
|
2822
|
+
/** 删成员,返回删除数 */
|
|
2823
|
+
async srem(key, ...members) {
|
|
2824
|
+
return Number(await this.conn.command("SREM", key, ...members));
|
|
2825
|
+
}
|
|
2826
|
+
/** 全部成员(无序) */
|
|
2827
|
+
async smembers(key) {
|
|
2828
|
+
const v = await this.conn.command("SMEMBERS", key);
|
|
2829
|
+
return v.map(String);
|
|
2830
|
+
}
|
|
2831
|
+
// ── zset ─────────────────────────────────────
|
|
2832
|
+
/** 加成员(score 排序),返回新增数 */
|
|
2833
|
+
async zadd(key, score, member) {
|
|
2834
|
+
return Number(await this.conn.command("ZADD", key, score, member));
|
|
2835
|
+
}
|
|
2836
|
+
/** 范围读(按 score 升序;start/stop 为排名,支持负数) */
|
|
2837
|
+
async zrange(key, start, stop) {
|
|
2838
|
+
const v = await this.conn.command("ZRANGE", key, start, stop);
|
|
2839
|
+
return v.map(String);
|
|
2840
|
+
}
|
|
2841
|
+
// ── pipeline ─────────────────────────────────
|
|
2842
|
+
/** 创建管道(批量命令一次往返;结果按序) */
|
|
2843
|
+
pipeline() {
|
|
2844
|
+
return new RedisPipeline(this.conn);
|
|
2845
|
+
}
|
|
2605
2846
|
// ── JSON 存取(自动序列化) ────────────────
|
|
2606
2847
|
/** 读取并 JSON.parse;key 不存在返回 null */
|
|
2607
2848
|
async jsonGet(key) {
|
|
@@ -2661,6 +2902,7 @@ var RedisSubscriber = class {
|
|
|
2661
2902
|
var RedisPool = class _RedisPool {
|
|
2662
2903
|
clients = [];
|
|
2663
2904
|
rr = 0;
|
|
2905
|
+
replenishWaiters = [];
|
|
2664
2906
|
closed = false;
|
|
2665
2907
|
keyPrefix;
|
|
2666
2908
|
opts;
|
|
@@ -2696,65 +2938,218 @@ var RedisPool = class _RedisPool {
|
|
|
2696
2938
|
k(key) {
|
|
2697
2939
|
return this.keyPrefix + key;
|
|
2698
2940
|
}
|
|
2699
|
-
/** round-robin
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2941
|
+
/** round-robin 选择健康连接;跳过不可用连接(断线/重连中/closed)——剔除并异步重建,防分发到死连接。
|
|
2942
|
+
* 全部不可用时等待 replenish 补位(Redis 真宕机 1s 后明确报错,不无限挂起)。 */
|
|
2943
|
+
async acquireHealthy() {
|
|
2944
|
+
if (this.closed) throw new ConnectionError("redis: pool is closed");
|
|
2945
|
+
while (this.clients.length > 0) {
|
|
2946
|
+
const idx = this.rr % this.clients.length;
|
|
2947
|
+
this.rr++;
|
|
2948
|
+
const c = this.clients[idx];
|
|
2949
|
+
if (c.connected) return c;
|
|
2950
|
+
this.clients.splice(idx, 1);
|
|
2951
|
+
c.close().catch(() => {
|
|
2952
|
+
});
|
|
2953
|
+
this.replenish();
|
|
2703
2954
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2955
|
+
return new Promise((resolve3, reject) => {
|
|
2956
|
+
const waiter = () => {
|
|
2957
|
+
if (this.clients.length > 0 && this.clients[0].connected) resolve3(this.clients[0]);
|
|
2958
|
+
else reject(new ConnectionError("redis: pool exhausted after reconnect wait"));
|
|
2959
|
+
};
|
|
2960
|
+
this.replenishWaiters.push(waiter);
|
|
2961
|
+
setTimeout(() => {
|
|
2962
|
+
const i = this.replenishWaiters.indexOf(waiter);
|
|
2963
|
+
if (i >= 0) this.replenishWaiters.splice(i, 1);
|
|
2964
|
+
reject(new ConnectionError("redis: no healthy connection available"));
|
|
2965
|
+
}, 1e3);
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
/** 剔除后补位:异步重建连接,就绪后回池并唤醒等待者 */
|
|
2969
|
+
replenish() {
|
|
2970
|
+
if (this.closed) return;
|
|
2971
|
+
RedisClient.connect(this.opts).then((c) => {
|
|
2972
|
+
if (this.closed) {
|
|
2973
|
+
c.close();
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
this.clients.push(c);
|
|
2977
|
+
const ws = this.replenishWaiters;
|
|
2978
|
+
this.replenishWaiters = [];
|
|
2979
|
+
for (const w of ws) w();
|
|
2980
|
+
}).catch(() => {
|
|
2981
|
+
setTimeout(() => this.replenish(), 500);
|
|
2982
|
+
});
|
|
2707
2983
|
}
|
|
2708
2984
|
// ── 与 RedisClient 相同的方法面(代理到轮询连接) ──
|
|
2709
2985
|
async command(name, ...args) {
|
|
2710
2986
|
await this.ensure();
|
|
2711
|
-
|
|
2987
|
+
const c = await this.acquireHealthy();
|
|
2988
|
+
return c.command(name, ...args);
|
|
2712
2989
|
}
|
|
2713
2990
|
async get(key) {
|
|
2714
2991
|
await this.ensure();
|
|
2715
|
-
|
|
2992
|
+
const c = await this.acquireHealthy();
|
|
2993
|
+
return c.get(this.k(key));
|
|
2716
2994
|
}
|
|
2717
2995
|
/** 二进制安全读取(原始字节,不解码) */
|
|
2718
2996
|
async getBuffer(key) {
|
|
2719
2997
|
await this.ensure();
|
|
2720
|
-
|
|
2998
|
+
const c = await this.acquireHealthy();
|
|
2999
|
+
return c.getBuffer(this.k(key));
|
|
2721
3000
|
}
|
|
2722
3001
|
async set(key, value, ttl) {
|
|
2723
3002
|
await this.ensure();
|
|
2724
|
-
|
|
3003
|
+
const c = await this.acquireHealthy();
|
|
3004
|
+
return c.set(this.k(key), value, ttl);
|
|
2725
3005
|
}
|
|
2726
3006
|
async del(...keys) {
|
|
2727
3007
|
await this.ensure();
|
|
2728
|
-
|
|
3008
|
+
const c = await this.acquireHealthy();
|
|
3009
|
+
return c.del(...keys.map((k) => this.k(k)));
|
|
2729
3010
|
}
|
|
2730
3011
|
async incr(key) {
|
|
2731
3012
|
await this.ensure();
|
|
2732
|
-
|
|
3013
|
+
const c = await this.acquireHealthy();
|
|
3014
|
+
return c.incr(this.k(key));
|
|
2733
3015
|
}
|
|
2734
3016
|
async expire(key, seconds) {
|
|
2735
3017
|
await this.ensure();
|
|
2736
|
-
|
|
3018
|
+
const c = await this.acquireHealthy();
|
|
3019
|
+
return c.expire(this.k(key), seconds);
|
|
2737
3020
|
}
|
|
2738
3021
|
async ttl(key) {
|
|
2739
3022
|
await this.ensure();
|
|
2740
|
-
|
|
3023
|
+
const c = await this.acquireHealthy();
|
|
3024
|
+
return c.ttl(this.k(key));
|
|
2741
3025
|
}
|
|
2742
3026
|
async jsonGet(key) {
|
|
2743
3027
|
await this.ensure();
|
|
2744
|
-
|
|
3028
|
+
const c = await this.acquireHealthy();
|
|
3029
|
+
return c.jsonGet(this.k(key));
|
|
2745
3030
|
}
|
|
2746
3031
|
async jsonSet(key, value, ttl) {
|
|
2747
3032
|
await this.ensure();
|
|
2748
|
-
|
|
3033
|
+
const c = await this.acquireHealthy();
|
|
3034
|
+
return c.jsonSet(this.k(key), value, ttl);
|
|
2749
3035
|
}
|
|
2750
3036
|
async cache(key, fn, ttl) {
|
|
2751
3037
|
await this.ensure();
|
|
2752
|
-
|
|
3038
|
+
const c = await this.acquireHealthy();
|
|
3039
|
+
return c.cache(this.k(key), fn, ttl);
|
|
3040
|
+
}
|
|
3041
|
+
// ── 批量 / 存在 / 计数 ────────────────────────
|
|
3042
|
+
async mget(...keys) {
|
|
3043
|
+
await this.ensure();
|
|
3044
|
+
const c = await this.acquireHealthy();
|
|
3045
|
+
return c.mget(...keys.map((k) => this.k(k)));
|
|
3046
|
+
}
|
|
3047
|
+
async mset(...kv) {
|
|
3048
|
+
await this.ensure();
|
|
3049
|
+
const c = await this.acquireHealthy();
|
|
3050
|
+
const prefixed = kv.map((x, i) => i % 2 === 0 ? this.k(String(x)) : x);
|
|
3051
|
+
return c.mset(...prefixed);
|
|
3052
|
+
}
|
|
3053
|
+
async exists(...keys) {
|
|
3054
|
+
await this.ensure();
|
|
3055
|
+
const c = await this.acquireHealthy();
|
|
3056
|
+
return c.exists(...keys.map((k) => this.k(k)));
|
|
3057
|
+
}
|
|
3058
|
+
async setnx(key, value) {
|
|
3059
|
+
await this.ensure();
|
|
3060
|
+
const c = await this.acquireHealthy();
|
|
3061
|
+
return c.setnx(this.k(key), value);
|
|
3062
|
+
}
|
|
3063
|
+
async incrby(key, delta) {
|
|
3064
|
+
await this.ensure();
|
|
3065
|
+
const c = await this.acquireHealthy();
|
|
3066
|
+
return c.incrby(this.k(key), delta);
|
|
3067
|
+
}
|
|
3068
|
+
// ── hash ─────────────────────────────────────
|
|
3069
|
+
async hset(key, field, value) {
|
|
3070
|
+
await this.ensure();
|
|
3071
|
+
const c = await this.acquireHealthy();
|
|
3072
|
+
return c.hset(this.k(key), field, value);
|
|
3073
|
+
}
|
|
3074
|
+
async hget(key, field) {
|
|
3075
|
+
await this.ensure();
|
|
3076
|
+
const c = await this.acquireHealthy();
|
|
3077
|
+
return c.hget(this.k(key), field);
|
|
3078
|
+
}
|
|
3079
|
+
async hgetall(key) {
|
|
3080
|
+
await this.ensure();
|
|
3081
|
+
const c = await this.acquireHealthy();
|
|
3082
|
+
return c.hgetall(this.k(key));
|
|
3083
|
+
}
|
|
3084
|
+
async hdel(key, ...fields) {
|
|
3085
|
+
await this.ensure();
|
|
3086
|
+
const c = await this.acquireHealthy();
|
|
3087
|
+
return c.hdel(this.k(key), ...fields);
|
|
3088
|
+
}
|
|
3089
|
+
// ── list ─────────────────────────────────────
|
|
3090
|
+
async lpush(key, ...values) {
|
|
3091
|
+
await this.ensure();
|
|
3092
|
+
const c = await this.acquireHealthy();
|
|
3093
|
+
return c.lpush(this.k(key), ...values);
|
|
3094
|
+
}
|
|
3095
|
+
async rpush(key, ...values) {
|
|
3096
|
+
await this.ensure();
|
|
3097
|
+
const c = await this.acquireHealthy();
|
|
3098
|
+
return c.rpush(this.k(key), ...values);
|
|
3099
|
+
}
|
|
3100
|
+
async lpop(key) {
|
|
3101
|
+
await this.ensure();
|
|
3102
|
+
const c = await this.acquireHealthy();
|
|
3103
|
+
return c.lpop(this.k(key));
|
|
3104
|
+
}
|
|
3105
|
+
async rpop(key) {
|
|
3106
|
+
await this.ensure();
|
|
3107
|
+
const c = await this.acquireHealthy();
|
|
3108
|
+
return c.rpop(this.k(key));
|
|
3109
|
+
}
|
|
3110
|
+
async lrange(key, start, stop) {
|
|
3111
|
+
await this.ensure();
|
|
3112
|
+
const c = await this.acquireHealthy();
|
|
3113
|
+
return c.lrange(this.k(key), start, stop);
|
|
3114
|
+
}
|
|
3115
|
+
// ── set ──────────────────────────────────────
|
|
3116
|
+
async sadd(key, ...members) {
|
|
3117
|
+
await this.ensure();
|
|
3118
|
+
const c = await this.acquireHealthy();
|
|
3119
|
+
return c.sadd(this.k(key), ...members);
|
|
3120
|
+
}
|
|
3121
|
+
async srem(key, ...members) {
|
|
3122
|
+
await this.ensure();
|
|
3123
|
+
const c = await this.acquireHealthy();
|
|
3124
|
+
return c.srem(this.k(key), ...members);
|
|
3125
|
+
}
|
|
3126
|
+
async smembers(key) {
|
|
3127
|
+
await this.ensure();
|
|
3128
|
+
const c = await this.acquireHealthy();
|
|
3129
|
+
return c.smembers(this.k(key));
|
|
3130
|
+
}
|
|
3131
|
+
// ── zset ─────────────────────────────────────
|
|
3132
|
+
async zadd(key, score, member) {
|
|
3133
|
+
await this.ensure();
|
|
3134
|
+
const c = await this.acquireHealthy();
|
|
3135
|
+
return c.zadd(this.k(key), score, member);
|
|
3136
|
+
}
|
|
3137
|
+
async zrange(key, start, stop) {
|
|
3138
|
+
await this.ensure();
|
|
3139
|
+
const c = await this.acquireHealthy();
|
|
3140
|
+
return c.zrange(this.k(key), start, stop);
|
|
3141
|
+
}
|
|
3142
|
+
// ── pipeline(池级:选一健康连接;key 自动加前缀) ──
|
|
3143
|
+
async pipeline() {
|
|
3144
|
+
await this.ensure();
|
|
3145
|
+
const c = await this.acquireHealthy();
|
|
3146
|
+
return c.pipeline();
|
|
2753
3147
|
}
|
|
2754
3148
|
/** PUBLISH 消息到频道,返回收到消息的订阅者数 */
|
|
2755
3149
|
async publish(channel, message) {
|
|
2756
3150
|
await this.ensure();
|
|
2757
|
-
|
|
3151
|
+
const c = await this.acquireHealthy();
|
|
3152
|
+
return Number(await c.command("PUBLISH", channel, message));
|
|
2758
3153
|
}
|
|
2759
3154
|
/** 创建独立订阅者连接(Pub/Sub 场景) */
|
|
2760
3155
|
createSubscriber() {
|
|
@@ -2763,7 +3158,8 @@ var RedisPool = class _RedisPool {
|
|
|
2763
3158
|
/** 清空当前库(测试/重置场景) */
|
|
2764
3159
|
async flushdb() {
|
|
2765
3160
|
await this.ensure();
|
|
2766
|
-
const
|
|
3161
|
+
const c = await this.acquireHealthy();
|
|
3162
|
+
const v = await c.command("FLUSHDB");
|
|
2767
3163
|
return String(v);
|
|
2768
3164
|
}
|
|
2769
3165
|
/** 关闭所有池内连接 */
|
|
@@ -2780,18 +3176,18 @@ var RedisPool = class _RedisPool {
|
|
|
2780
3176
|
|
|
2781
3177
|
// src/queue/index.ts
|
|
2782
3178
|
var GROUP = "workers";
|
|
2783
|
-
function
|
|
3179
|
+
function parseUrl(options) {
|
|
2784
3180
|
const url = options?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
|
|
2785
3181
|
const u = new URL(url);
|
|
2786
|
-
|
|
3182
|
+
const opts = {
|
|
2787
3183
|
host: u.hostname,
|
|
2788
|
-
port: Number(u.port || 6379)
|
|
2789
|
-
|
|
2790
|
-
});
|
|
3184
|
+
port: Number(u.port || 6379)
|
|
3185
|
+
};
|
|
3186
|
+
return { conn: opts, pool: new RedisPool({ ...opts, poolSize: options?.poolSize }) };
|
|
2791
3187
|
}
|
|
2792
3188
|
function queue(options) {
|
|
2793
3189
|
const prefix = options?.prefix ?? "q:";
|
|
2794
|
-
const pool =
|
|
3190
|
+
const { conn: connOpts, pool } = parseUrl(options);
|
|
2795
3191
|
const stream = (name) => `${prefix}${name}`;
|
|
2796
3192
|
const deadStream = (name) => `${prefix}${name}:dead`;
|
|
2797
3193
|
async function pushJob(name, payload) {
|
|
@@ -2820,21 +3216,48 @@ function queue(options) {
|
|
|
2820
3216
|
const dead = deadStream(name);
|
|
2821
3217
|
const delayed = `${prefix}${name}:delayed`;
|
|
2822
3218
|
let running = false;
|
|
3219
|
+
let lastErrAt = 0;
|
|
3220
|
+
let epoch = 0;
|
|
3221
|
+
let conn = null;
|
|
3222
|
+
let connEpoch = -1;
|
|
3223
|
+
const loops = /* @__PURE__ */ new Map();
|
|
2823
3224
|
const inflight = /* @__PURE__ */ new Set();
|
|
3225
|
+
async function getConn(myEpoch) {
|
|
3226
|
+
if (conn && connEpoch === myEpoch) return conn;
|
|
3227
|
+
if (conn) {
|
|
3228
|
+
const c2 = conn;
|
|
3229
|
+
conn = null;
|
|
3230
|
+
connEpoch = -1;
|
|
3231
|
+
await c2.close().catch(() => {
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
const c = new RedisConnection(connOpts);
|
|
3235
|
+
await c.connect();
|
|
3236
|
+
conn = c;
|
|
3237
|
+
connEpoch = myEpoch;
|
|
3238
|
+
return c;
|
|
3239
|
+
}
|
|
2824
3240
|
async function ensureGroup() {
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
3241
|
+
let lastErr;
|
|
3242
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3243
|
+
try {
|
|
3244
|
+
await conn.command("XGROUP", "CREATE", s, GROUP, "0", "MKSTREAM");
|
|
3245
|
+
return;
|
|
3246
|
+
} catch (e) {
|
|
3247
|
+
if (String(e.message).includes("BUSYGROUP")) return;
|
|
3248
|
+
lastErr = e;
|
|
3249
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 50));
|
|
3250
|
+
}
|
|
2829
3251
|
}
|
|
3252
|
+
throw lastErr;
|
|
2830
3253
|
}
|
|
2831
3254
|
async function processEntry(entryId, fields) {
|
|
2832
3255
|
let payload;
|
|
2833
3256
|
try {
|
|
2834
3257
|
payload = JSON.parse(fields.payload ?? "{}");
|
|
2835
3258
|
} catch {
|
|
2836
|
-
await
|
|
2837
|
-
await
|
|
3259
|
+
await conn.command("XACK", s, GROUP, entryId);
|
|
3260
|
+
await conn.command("XADD", dead, "*", "payload", JSON.stringify({
|
|
2838
3261
|
originalId: entryId,
|
|
2839
3262
|
name,
|
|
2840
3263
|
error: "unparseable payload",
|
|
@@ -2851,12 +3274,12 @@ function queue(options) {
|
|
|
2851
3274
|
data: payload.data,
|
|
2852
3275
|
attempts
|
|
2853
3276
|
});
|
|
2854
|
-
await
|
|
3277
|
+
await conn.command("XACK", s, GROUP, entryId);
|
|
2855
3278
|
} catch (err) {
|
|
2856
3279
|
const nextAttempt = attempts + 1;
|
|
2857
|
-
await
|
|
3280
|
+
await conn.command("XACK", s, GROUP, entryId);
|
|
2858
3281
|
if (nextAttempt >= maxAttempts) {
|
|
2859
|
-
await
|
|
3282
|
+
await conn.command("XADD", dead, "*", "payload", JSON.stringify({
|
|
2860
3283
|
originalId: entryId,
|
|
2861
3284
|
name,
|
|
2862
3285
|
data: payload.data,
|
|
@@ -2865,7 +3288,7 @@ function queue(options) {
|
|
|
2865
3288
|
}));
|
|
2866
3289
|
console.error(`[queue] ${name} job failed permanently (attempt ${nextAttempt}/${maxAttempts}):`, err);
|
|
2867
3290
|
} else {
|
|
2868
|
-
await
|
|
3291
|
+
await conn.command("ZADD", delayed, String(Date.now() + visibilityTimeout), JSON.stringify({
|
|
2869
3292
|
...payload,
|
|
2870
3293
|
attempts: nextAttempt
|
|
2871
3294
|
}));
|
|
@@ -2880,25 +3303,27 @@ function queue(options) {
|
|
|
2880
3303
|
const now = Date.now();
|
|
2881
3304
|
let due;
|
|
2882
3305
|
try {
|
|
2883
|
-
due = await
|
|
3306
|
+
due = await conn.command("ZRANGEBYSCORE", delayed, 0, now);
|
|
2884
3307
|
} catch (e) {
|
|
2885
|
-
if (
|
|
3308
|
+
if (isConnClosed(e)) {
|
|
2886
3309
|
running = false;
|
|
2887
3310
|
return;
|
|
2888
3311
|
}
|
|
2889
|
-
if (running
|
|
3312
|
+
if (running && shouldLogError()) {
|
|
3313
|
+
console.error("[queue] ZRANGEBYSCORE:", e instanceof Error ? e.message : e);
|
|
3314
|
+
}
|
|
2890
3315
|
return;
|
|
2891
3316
|
}
|
|
2892
3317
|
for (const member of due) {
|
|
2893
|
-
const removed = await
|
|
3318
|
+
const removed = await conn.command("ZREM", delayed, member);
|
|
2894
3319
|
if (removed) {
|
|
2895
|
-
await
|
|
3320
|
+
await conn.command("XADD", s, "*", "payload", member);
|
|
2896
3321
|
}
|
|
2897
3322
|
}
|
|
2898
3323
|
}
|
|
2899
3324
|
async function claimStale() {
|
|
2900
3325
|
try {
|
|
2901
|
-
const result = await
|
|
3326
|
+
const result = await conn.command(
|
|
2902
3327
|
"XAUTOCLAIM",
|
|
2903
3328
|
s,
|
|
2904
3329
|
GROUP,
|
|
@@ -2913,21 +3338,41 @@ function queue(options) {
|
|
|
2913
3338
|
await processEntry(entryId, flatFieldsToRecord(fields));
|
|
2914
3339
|
}
|
|
2915
3340
|
} catch (e) {
|
|
2916
|
-
if (
|
|
3341
|
+
if (isConnClosed(e)) {
|
|
2917
3342
|
running = false;
|
|
2918
3343
|
return;
|
|
2919
3344
|
}
|
|
2920
|
-
if (
|
|
3345
|
+
if (await recoverGroupIfMissing(e)) return;
|
|
3346
|
+
if (running && shouldLogError()) {
|
|
3347
|
+
console.error("[queue] XAUTOCLAIM:", e instanceof Error ? e.message : e);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
async function recoverGroupIfMissing(e) {
|
|
3352
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3353
|
+
if (!/NOGROUP/.test(msg)) return false;
|
|
3354
|
+
try {
|
|
3355
|
+
await ensureGroup();
|
|
3356
|
+
return true;
|
|
3357
|
+
} catch {
|
|
3358
|
+
return false;
|
|
2921
3359
|
}
|
|
2922
3360
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3361
|
+
function shouldLogError() {
|
|
3362
|
+
const now = Date.now();
|
|
3363
|
+
if (now - lastErrAt > 5e3) {
|
|
3364
|
+
lastErrAt = now;
|
|
3365
|
+
return true;
|
|
3366
|
+
}
|
|
3367
|
+
return false;
|
|
3368
|
+
}
|
|
3369
|
+
async function loop(myEpoch) {
|
|
3370
|
+
while (running && myEpoch === epoch) {
|
|
2926
3371
|
await claimStale();
|
|
2927
3372
|
await requeueDelayed();
|
|
2928
3373
|
let result;
|
|
2929
3374
|
try {
|
|
2930
|
-
result = await
|
|
3375
|
+
result = await conn.command(
|
|
2931
3376
|
"XREADGROUP",
|
|
2932
3377
|
"GROUP",
|
|
2933
3378
|
GROUP,
|
|
@@ -2941,11 +3386,14 @@ function queue(options) {
|
|
|
2941
3386
|
">"
|
|
2942
3387
|
);
|
|
2943
3388
|
} catch (e) {
|
|
2944
|
-
if (
|
|
3389
|
+
if (isConnClosed(e)) {
|
|
2945
3390
|
running = false;
|
|
2946
3391
|
return;
|
|
2947
3392
|
}
|
|
2948
|
-
if (
|
|
3393
|
+
if (await recoverGroupIfMissing(e)) continue;
|
|
3394
|
+
if (running && shouldLogError()) {
|
|
3395
|
+
console.error("[queue] XREADGROUP:", e instanceof Error ? e.message : e);
|
|
3396
|
+
}
|
|
2949
3397
|
continue;
|
|
2950
3398
|
}
|
|
2951
3399
|
if (!result) continue;
|
|
@@ -2963,11 +3411,42 @@ function queue(options) {
|
|
|
2963
3411
|
start: async () => {
|
|
2964
3412
|
if (running) return;
|
|
2965
3413
|
running = true;
|
|
2966
|
-
|
|
3414
|
+
const myEpoch = epoch;
|
|
3415
|
+
try {
|
|
3416
|
+
await getConn(myEpoch);
|
|
3417
|
+
await ensureGroup();
|
|
3418
|
+
} catch (e) {
|
|
3419
|
+
running = false;
|
|
3420
|
+
if (conn) {
|
|
3421
|
+
const c = conn;
|
|
3422
|
+
conn = null;
|
|
3423
|
+
connEpoch = -1;
|
|
3424
|
+
await c.close().catch(() => {
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
throw e;
|
|
3428
|
+
}
|
|
3429
|
+
const lp = loop(myEpoch);
|
|
3430
|
+
loops.set(myEpoch, lp);
|
|
3431
|
+
lp.finally(() => loops.delete(myEpoch)).catch(
|
|
3432
|
+
(e) => console.error("[queue] worker loop crashed:", e)
|
|
3433
|
+
);
|
|
2967
3434
|
},
|
|
2968
3435
|
stop: async () => {
|
|
3436
|
+
if (!running) {
|
|
3437
|
+
await Promise.allSettled([...loops.values()]);
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
epoch++;
|
|
2969
3441
|
running = false;
|
|
3442
|
+
const oldConn = conn;
|
|
3443
|
+
conn = null;
|
|
3444
|
+
connEpoch = -1;
|
|
3445
|
+
const oldLoops = [...loops.entries()].filter(([e]) => e < epoch).map(([, p]) => p);
|
|
2970
3446
|
await Promise.allSettled([...inflight]);
|
|
3447
|
+
await Promise.allSettled(oldLoops);
|
|
3448
|
+
await oldConn?.close().catch(() => {
|
|
3449
|
+
});
|
|
2971
3450
|
}
|
|
2972
3451
|
};
|
|
2973
3452
|
}
|
|
@@ -2988,14 +3467,292 @@ function flatFieldsToRecord(fields) {
|
|
|
2988
3467
|
}
|
|
2989
3468
|
return rec;
|
|
2990
3469
|
}
|
|
2991
|
-
function
|
|
3470
|
+
function isConnClosed(e) {
|
|
2992
3471
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2993
3472
|
return /pool is closed|connection closed/.test(msg);
|
|
2994
3473
|
}
|
|
2995
3474
|
|
|
2996
|
-
// src/
|
|
3475
|
+
// src/scheduler/index.ts
|
|
2997
3476
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2998
3477
|
|
|
3478
|
+
// src/scheduler/cron.ts
|
|
3479
|
+
var RANGES = [
|
|
3480
|
+
[0, 59],
|
|
3481
|
+
// 分
|
|
3482
|
+
[0, 23],
|
|
3483
|
+
// 时
|
|
3484
|
+
[1, 31],
|
|
3485
|
+
// 日
|
|
3486
|
+
[1, 12],
|
|
3487
|
+
// 月
|
|
3488
|
+
[0, 6]
|
|
3489
|
+
// 周
|
|
3490
|
+
];
|
|
3491
|
+
function parseField(field, idx) {
|
|
3492
|
+
const [min, max] = RANGES[idx];
|
|
3493
|
+
const name = ["minute", "hour", "day", "month", "weekday"][idx];
|
|
3494
|
+
if (field === "*") return null;
|
|
3495
|
+
if (field.startsWith("*/")) {
|
|
3496
|
+
const n = Number(field.slice(2));
|
|
3497
|
+
if (!Number.isInteger(n) || n <= 0 || n > max) {
|
|
3498
|
+
throw new Error(`cron: invalid step '${field}' in ${name} field (must be 1..${max})`);
|
|
3499
|
+
}
|
|
3500
|
+
const set2 = /* @__PURE__ */ new Set();
|
|
3501
|
+
for (let v = min; v <= max; v += n) set2.add(v);
|
|
3502
|
+
return set2;
|
|
3503
|
+
}
|
|
3504
|
+
const set = /* @__PURE__ */ new Set();
|
|
3505
|
+
for (const part of field.split(",")) {
|
|
3506
|
+
if (part === "") throw new Error(`cron: empty item in '${field}' (${name})`);
|
|
3507
|
+
if (part.includes("-")) {
|
|
3508
|
+
const [rangePart, stepPart] = part.split("/");
|
|
3509
|
+
const step = stepPart !== void 0 ? Number(stepPart) : 1;
|
|
3510
|
+
const [a, b] = rangePart.split("-").map(Number);
|
|
3511
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || a < min || b > max || a > b) {
|
|
3512
|
+
throw new Error(`cron: invalid range '${part}' in ${name} field (${min}..${max})`);
|
|
3513
|
+
}
|
|
3514
|
+
if (!Number.isInteger(step) || step <= 0) throw new Error(`cron: invalid step in '${part}'`);
|
|
3515
|
+
for (let v = a; v <= b; v += step) set.add(v);
|
|
3516
|
+
} else {
|
|
3517
|
+
const v = Number(part);
|
|
3518
|
+
if (!Number.isInteger(v) || v < min || v > max) {
|
|
3519
|
+
throw new Error(`cron: value '${part}' out of range ${min}..${max} in ${name} field`);
|
|
3520
|
+
}
|
|
3521
|
+
set.add(v);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
return set;
|
|
3525
|
+
}
|
|
3526
|
+
function parseCron(expr) {
|
|
3527
|
+
const parts = expr.trim().split(/\s+/);
|
|
3528
|
+
if (parts.length !== 5) {
|
|
3529
|
+
throw new Error(`cron: expected 5 fields (min hour dom month dow), got ${parts.length}: '${expr}'`);
|
|
3530
|
+
}
|
|
3531
|
+
const fields = parts.map((p, i) => parseField(p, i));
|
|
3532
|
+
return {
|
|
3533
|
+
fields: fields.map((values) => ({ values })),
|
|
3534
|
+
domExplicit: fields[2] !== null,
|
|
3535
|
+
dowExplicit: fields[4] !== null
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
function fieldMatches(values, v) {
|
|
3539
|
+
if (values === null) return true;
|
|
3540
|
+
return values.has(v);
|
|
3541
|
+
}
|
|
3542
|
+
function dayMatches(expr, dom, dow) {
|
|
3543
|
+
const domOk = fieldMatches(expr.fields[2].values, dom);
|
|
3544
|
+
const dowOk = fieldMatches(expr.fields[4].values, dow);
|
|
3545
|
+
if (expr.domExplicit && expr.dowExplicit) return domOk || dowOk;
|
|
3546
|
+
if (expr.domExplicit) return domOk;
|
|
3547
|
+
if (expr.dowExplicit) return dowOk;
|
|
3548
|
+
return true;
|
|
3549
|
+
}
|
|
3550
|
+
function nextMatch(values, v, min, max) {
|
|
3551
|
+
if (values === null) return v;
|
|
3552
|
+
for (let x = v; x <= max; x++) if (values.has(x)) return x;
|
|
3553
|
+
return null;
|
|
3554
|
+
}
|
|
3555
|
+
function nextRun(expr, from) {
|
|
3556
|
+
const d = new Date(from.getTime());
|
|
3557
|
+
d.setSeconds(0, 0);
|
|
3558
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
3559
|
+
const deadline = from.getTime() + 5 * 366 * 24 * 3600 * 1e3;
|
|
3560
|
+
while (d.getTime() <= deadline) {
|
|
3561
|
+
const year = d.getFullYear();
|
|
3562
|
+
const month = d.getMonth() + 1;
|
|
3563
|
+
const dom = d.getDate();
|
|
3564
|
+
const dow = d.getDay();
|
|
3565
|
+
if (!fieldMatches(expr.fields[3].values, month)) {
|
|
3566
|
+
d.setMonth(month, 1);
|
|
3567
|
+
d.setHours(0, 0, 0, 0);
|
|
3568
|
+
d.setMonth(month);
|
|
3569
|
+
continue;
|
|
3570
|
+
}
|
|
3571
|
+
if (!dayMatches(expr, dom, dow)) {
|
|
3572
|
+
d.setDate(dom + 1);
|
|
3573
|
+
d.setHours(0, 0, 0, 0);
|
|
3574
|
+
continue;
|
|
3575
|
+
}
|
|
3576
|
+
const hour = d.getHours();
|
|
3577
|
+
const nextHour = nextMatch(expr.fields[1].values, hour, 0, 23);
|
|
3578
|
+
if (nextHour === null) {
|
|
3579
|
+
d.setDate(dom + 1);
|
|
3580
|
+
d.setHours(0, 0, 0, 0);
|
|
3581
|
+
continue;
|
|
3582
|
+
}
|
|
3583
|
+
if (nextHour !== hour) {
|
|
3584
|
+
d.setHours(nextHour, 0, 0, 0);
|
|
3585
|
+
continue;
|
|
3586
|
+
}
|
|
3587
|
+
const minute = d.getMinutes();
|
|
3588
|
+
const nextMinute = nextMatch(expr.fields[0].values, minute, 0, 59);
|
|
3589
|
+
if (nextMinute === null) {
|
|
3590
|
+
d.setHours(hour + 1, 0, 0, 0);
|
|
3591
|
+
continue;
|
|
3592
|
+
}
|
|
3593
|
+
if (nextMinute !== minute) {
|
|
3594
|
+
d.setMinutes(nextMinute, 0, 0);
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
return d;
|
|
3598
|
+
}
|
|
3599
|
+
throw new Error("cron: no next run within 5 years (invalid expression?)");
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
// src/scheduler/index.ts
|
|
3603
|
+
function parseUrl2(options) {
|
|
3604
|
+
const url = options?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
|
|
3605
|
+
const u = new URL(url);
|
|
3606
|
+
return { host: u.hostname, port: Number(u.port || 6379) };
|
|
3607
|
+
}
|
|
3608
|
+
function scheduler(options) {
|
|
3609
|
+
const prefix = options.prefix ?? "wf:sched:";
|
|
3610
|
+
const tickMs = options.tickMs ?? 1e3;
|
|
3611
|
+
const delayedKey = `${prefix}delayed`;
|
|
3612
|
+
const cronsKey = `${prefix}crons`;
|
|
3613
|
+
const connOpts = parseUrl2(options);
|
|
3614
|
+
const queueModule = options.queue;
|
|
3615
|
+
const conn = new RedisConnection(connOpts);
|
|
3616
|
+
let running = false;
|
|
3617
|
+
let tickTimer = null;
|
|
3618
|
+
async function tick() {
|
|
3619
|
+
await tickCrons();
|
|
3620
|
+
let due;
|
|
3621
|
+
try {
|
|
3622
|
+
due = await conn.command("ZRANGEBYSCORE", delayedKey, 0, Date.now());
|
|
3623
|
+
} catch {
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
for (const member of due) {
|
|
3627
|
+
let removed;
|
|
3628
|
+
try {
|
|
3629
|
+
removed = await conn.command("ZREM", delayedKey, member);
|
|
3630
|
+
} catch {
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
if (removed !== 1) continue;
|
|
3634
|
+
try {
|
|
3635
|
+
const task = JSON.parse(member);
|
|
3636
|
+
await queueModule.queue.add(task.name, task.data);
|
|
3637
|
+
} catch (e) {
|
|
3638
|
+
console.error("[scheduler] enqueue:", e instanceof Error ? e.message : e);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
async function tickCrons() {
|
|
3643
|
+
let crons;
|
|
3644
|
+
try {
|
|
3645
|
+
crons = await conn.command("HGETALL", cronsKey);
|
|
3646
|
+
} catch {
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
const now = Date.now();
|
|
3650
|
+
const entries = crons;
|
|
3651
|
+
for (let i = 0; i + 1 < entries.length; i += 2) {
|
|
3652
|
+
const field = entries[i];
|
|
3653
|
+
const value = entries[i + 1];
|
|
3654
|
+
let def;
|
|
3655
|
+
try {
|
|
3656
|
+
def = JSON.parse(value);
|
|
3657
|
+
} catch {
|
|
3658
|
+
continue;
|
|
3659
|
+
}
|
|
3660
|
+
const parsed = parseCron(def.expr);
|
|
3661
|
+
let next = def.nextRunAt;
|
|
3662
|
+
while (next <= now) {
|
|
3663
|
+
const ts = next;
|
|
3664
|
+
const member = JSON.stringify({ id: `cron:${field}:${ts}`, name: def.name, data: def.data });
|
|
3665
|
+
try {
|
|
3666
|
+
await conn.command("ZADD", delayedKey, "NX", ts, member);
|
|
3667
|
+
} catch {
|
|
3668
|
+
break;
|
|
3669
|
+
}
|
|
3670
|
+
next = nextRun(parsed, new Date(ts)).getTime();
|
|
3671
|
+
}
|
|
3672
|
+
if (next !== def.nextRunAt) {
|
|
3673
|
+
await conn.command("HSET", cronsKey, field, JSON.stringify({ ...def, nextRunAt: next }));
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
const cron = async (expr, name, data) => {
|
|
3678
|
+
const parsed = parseCron(expr);
|
|
3679
|
+
const firstRun = nextRun(parsed, /* @__PURE__ */ new Date());
|
|
3680
|
+
const def = JSON.stringify({ expr, name, data, nextRunAt: firstRun.getTime() });
|
|
3681
|
+
await conn.command("HSET", cronsKey, name, def);
|
|
3682
|
+
};
|
|
3683
|
+
const cancelCron = async (name) => {
|
|
3684
|
+
const removed = await conn.command("HDEL", cronsKey, name);
|
|
3685
|
+
try {
|
|
3686
|
+
const pending = await conn.command("ZRANGE", delayedKey, 0, -1);
|
|
3687
|
+
for (const member of pending) {
|
|
3688
|
+
if (member.includes(`"id":"cron:${name}:`)) {
|
|
3689
|
+
await conn.command("ZREM", delayedKey, member);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
} catch {
|
|
3693
|
+
}
|
|
3694
|
+
return removed === 1;
|
|
3695
|
+
};
|
|
3696
|
+
async function start() {
|
|
3697
|
+
if (running) return;
|
|
3698
|
+
running = true;
|
|
3699
|
+
await conn.connect();
|
|
3700
|
+
await tick();
|
|
3701
|
+
tickTimer = setInterval(() => {
|
|
3702
|
+
tick().catch((e) => console.error("[scheduler] tick:", e instanceof Error ? e.message : e));
|
|
3703
|
+
}, tickMs);
|
|
3704
|
+
tickTimer.unref?.();
|
|
3705
|
+
}
|
|
3706
|
+
const schedule = async (name, data, opts = {}) => {
|
|
3707
|
+
const delayMs = opts.delayMs ?? 0;
|
|
3708
|
+
const runAt = opts.when ? opts.when.getTime() : Date.now() + delayMs;
|
|
3709
|
+
if (!Number.isFinite(runAt)) throw new Error("scheduler: invalid when/delayMs");
|
|
3710
|
+
const id = randomUUID2();
|
|
3711
|
+
const member = JSON.stringify({ id, name, data });
|
|
3712
|
+
await conn.command("ZADD", delayedKey, runAt, member);
|
|
3713
|
+
return { id };
|
|
3714
|
+
};
|
|
3715
|
+
const cancelSchedule = async (id) => {
|
|
3716
|
+
try {
|
|
3717
|
+
const pending = await conn.command("ZRANGE", delayedKey, 0, -1);
|
|
3718
|
+
for (const member of pending) {
|
|
3719
|
+
if (member.includes(`"id":"${id}"`)) {
|
|
3720
|
+
const removed = await conn.command("ZREM", delayedKey, member);
|
|
3721
|
+
if (removed === 1) return true;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
} catch {
|
|
3725
|
+
}
|
|
3726
|
+
return false;
|
|
3727
|
+
};
|
|
3728
|
+
const mw = (async (req, ctx, next) => {
|
|
3729
|
+
ctx.schedule = schedule;
|
|
3730
|
+
ctx.cron = cron;
|
|
3731
|
+
ctx.cancelCron = cancelCron;
|
|
3732
|
+
ctx.cancelSchedule = cancelSchedule;
|
|
3733
|
+
return next(req, ctx);
|
|
3734
|
+
});
|
|
3735
|
+
mw.__meta = { injects: ["schedule", "cron", "cancelCron", "cancelSchedule"], depends: ["queue"] };
|
|
3736
|
+
mw.schedule = schedule;
|
|
3737
|
+
mw.cron = cron;
|
|
3738
|
+
mw.cancelCron = cancelCron;
|
|
3739
|
+
mw.cancelSchedule = cancelSchedule;
|
|
3740
|
+
mw.close = async () => {
|
|
3741
|
+
running = false;
|
|
3742
|
+
if (tickTimer) {
|
|
3743
|
+
clearInterval(tickTimer);
|
|
3744
|
+
tickTimer = null;
|
|
3745
|
+
}
|
|
3746
|
+
await conn.close().catch(() => {
|
|
3747
|
+
});
|
|
3748
|
+
};
|
|
3749
|
+
start().catch((e) => console.error("[scheduler] start:", e instanceof Error ? e.message : e));
|
|
3750
|
+
return mw;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// src/ai/client.ts
|
|
3754
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3755
|
+
|
|
2999
3756
|
// src/ai/sse.ts
|
|
3000
3757
|
function sseResponse(run, options) {
|
|
3001
3758
|
const encoder = new TextEncoder();
|
|
@@ -3200,7 +3957,7 @@ function createAiClient(opts) {
|
|
|
3200
3957
|
}
|
|
3201
3958
|
return sseResponse(
|
|
3202
3959
|
async (emit) => {
|
|
3203
|
-
emit("wf:message_start", { id: options?.traceId ??
|
|
3960
|
+
emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
|
|
3204
3961
|
await streamStep(params, {
|
|
3205
3962
|
emit,
|
|
3206
3963
|
signal: controller.signal,
|
|
@@ -3294,7 +4051,7 @@ function safeParseArgs(raw) {
|
|
|
3294
4051
|
}
|
|
3295
4052
|
|
|
3296
4053
|
// src/ai/agent.ts
|
|
3297
|
-
import { randomUUID as
|
|
4054
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3298
4055
|
function createAgent(client, config) {
|
|
3299
4056
|
const maxSteps = config.maxSteps ?? 10;
|
|
3300
4057
|
const toolDefs = config.tools.map((t) => ({
|
|
@@ -3313,14 +4070,14 @@ function createAgent(client, config) {
|
|
|
3313
4070
|
const controller = createController(options?.signal);
|
|
3314
4071
|
const emit = options?.emit ?? (() => {
|
|
3315
4072
|
});
|
|
3316
|
-
emit("wf:message_start", { id: options?.traceId ??
|
|
4073
|
+
emit("wf:message_start", { id: options?.traceId ?? randomUUID4() });
|
|
3317
4074
|
await loop(messages, emit, controller.signal);
|
|
3318
4075
|
}
|
|
3319
4076
|
function run(messages, options) {
|
|
3320
4077
|
const controller = createController(options?.signal);
|
|
3321
4078
|
return sseResponse(
|
|
3322
4079
|
async (emit) => {
|
|
3323
|
-
emit("wf:message_start", { id: options?.traceId ??
|
|
4080
|
+
emit("wf:message_start", { id: options?.traceId ?? randomUUID4() });
|
|
3324
4081
|
await loop(messages, emit, controller.signal);
|
|
3325
4082
|
},
|
|
3326
4083
|
{ onAbort: () => controller.abort() }
|
|
@@ -3399,7 +4156,7 @@ function createAgent(client, config) {
|
|
|
3399
4156
|
let execArgs = args;
|
|
3400
4157
|
if (config.humanInTheLoop) {
|
|
3401
4158
|
const decision = await client.waitApproval(
|
|
3402
|
-
{ id:
|
|
4159
|
+
{ id: randomUUID4(), toolCallId: tc.id, name, args },
|
|
3403
4160
|
emit,
|
|
3404
4161
|
config.approvalTimeoutMs
|
|
3405
4162
|
);
|
|
@@ -4210,6 +4967,9 @@ function concat(...parts) {
|
|
|
4210
4967
|
}
|
|
4211
4968
|
|
|
4212
4969
|
// src/db/postgres/connection.ts
|
|
4970
|
+
function isCacheInvalidationError(message) {
|
|
4971
|
+
return message.includes("cached plan must not change result type") || message.includes("cache lookup failed for type");
|
|
4972
|
+
}
|
|
4213
4973
|
var PgConnection = class _PgConnection {
|
|
4214
4974
|
opts;
|
|
4215
4975
|
timeoutSet = false;
|
|
@@ -4233,6 +4993,8 @@ var PgConnection = class _PgConnection {
|
|
|
4233
4993
|
get connected() {
|
|
4234
4994
|
return this.status === "ready";
|
|
4235
4995
|
}
|
|
4996
|
+
/** 池空闲回收用:最近一次查询完成时间戳(pool release 时更新) */
|
|
4997
|
+
lastUsed = 0;
|
|
4236
4998
|
connect() {
|
|
4237
4999
|
return new Promise((resolve3, reject) => {
|
|
4238
5000
|
this.status = "connecting";
|
|
@@ -4282,7 +5044,13 @@ var PgConnection = class _PgConnection {
|
|
|
4282
5044
|
// sig → stmt + 列缓存(LRU)
|
|
4283
5045
|
static PREPARED_MAX = 128;
|
|
4284
5046
|
stmtSeq = 0;
|
|
5047
|
+
/** LRU 淘汰的 statement 名——连接空闲时批量 DEALLOCATE(服务端同步释放,防 plan 缓存膨胀) */
|
|
5048
|
+
deallocQueue = [];
|
|
5049
|
+
/** DEALLOCATE 批量的响应 Z——仅消费(不 resolve 查询) */
|
|
5050
|
+
awaitingDeallocZ = false;
|
|
4285
5051
|
currentQuery = null;
|
|
5052
|
+
/** DDL 失效重试:缓存语句被 DROP 后服务器拒绝执行,Sync 复位后以新语句名重 Parse */
|
|
5053
|
+
pendingRetry = null;
|
|
4286
5054
|
onData(chunk) {
|
|
4287
5055
|
const msgs = this.stream.push(new Uint8Array(chunk));
|
|
4288
5056
|
for (const msg of msgs) {
|
|
@@ -4378,9 +5146,15 @@ var PgConnection = class _PgConnection {
|
|
|
4378
5146
|
}
|
|
4379
5147
|
break;
|
|
4380
5148
|
}
|
|
4381
|
-
case "C":
|
|
5149
|
+
case "C": {
|
|
5150
|
+
const q = this.currentQuery;
|
|
5151
|
+
if (q) {
|
|
5152
|
+
const tag = new TextDecoder().decode(msg.payload).replace(/\0+$/, "");
|
|
5153
|
+
const m = tag.match(/^(?:INSERT|UPDATE|DELETE|MERGE)\s+(?:\d+\s+)?(\d+)$/);
|
|
5154
|
+
if (m) q.affectedRows = parseInt(m[1], 10);
|
|
5155
|
+
}
|
|
4382
5156
|
break;
|
|
4383
|
-
|
|
5157
|
+
}
|
|
4384
5158
|
case "t": {
|
|
4385
5159
|
if (this.currentQuery?.awaitingDescribe && this.currentQuery.sql !== void 0) {
|
|
4386
5160
|
this.currentQuery.awaitingDescribe = false;
|
|
@@ -4399,11 +5173,32 @@ var PgConnection = class _PgConnection {
|
|
|
4399
5173
|
case "Z": {
|
|
4400
5174
|
if (this.pendingErrorZ) {
|
|
4401
5175
|
this.pendingErrorZ = false;
|
|
5176
|
+
if (this.pendingRetry) {
|
|
5177
|
+
const r = this.pendingRetry;
|
|
5178
|
+
this.pendingRetry = null;
|
|
5179
|
+
this.issueExtendedQuery(r.sql, r.params ?? [], r.resolve, r.reject, r.retried);
|
|
5180
|
+
}
|
|
5181
|
+
break;
|
|
5182
|
+
}
|
|
5183
|
+
if (this.awaitingDeallocZ) {
|
|
5184
|
+
this.awaitingDeallocZ = false;
|
|
4402
5185
|
break;
|
|
4403
5186
|
}
|
|
4404
5187
|
const q = this.currentQuery;
|
|
4405
5188
|
this.currentQuery = null;
|
|
4406
|
-
if (q)
|
|
5189
|
+
if (q) {
|
|
5190
|
+
const rows = q.rows;
|
|
5191
|
+
if (q.affectedRows !== void 0) {
|
|
5192
|
+
Object.defineProperty(rows, "affectedRows", {
|
|
5193
|
+
value: q.affectedRows,
|
|
5194
|
+
enumerable: false,
|
|
5195
|
+
writable: true,
|
|
5196
|
+
configurable: true
|
|
5197
|
+
});
|
|
5198
|
+
}
|
|
5199
|
+
q.resolve(rows);
|
|
5200
|
+
}
|
|
5201
|
+
this.flushDealloc();
|
|
4407
5202
|
if (this.awaitingReady) {
|
|
4408
5203
|
this.awaitingReady = false;
|
|
4409
5204
|
this.onReady?.();
|
|
@@ -4417,9 +5212,24 @@ var PgConnection = class _PgConnection {
|
|
|
4417
5212
|
this.currentQuery = null;
|
|
4418
5213
|
if (q) {
|
|
4419
5214
|
this.pendingErrorZ = true;
|
|
4420
|
-
if (q.awaitingDescribe) this.send(syncMessage());
|
|
4421
5215
|
const err = new Error(fields.message ?? "postgres query error");
|
|
4422
5216
|
err.code = fields.code;
|
|
5217
|
+
if (q.prepKey && isCacheInvalidationError(fields.message ?? "")) {
|
|
5218
|
+
this.prepared.clear();
|
|
5219
|
+
if (!q.retried) {
|
|
5220
|
+
this.pendingRetry = {
|
|
5221
|
+
sql: q.sql ?? "",
|
|
5222
|
+
params: q.params,
|
|
5223
|
+
columns: q.columns,
|
|
5224
|
+
resolve: q.resolve,
|
|
5225
|
+
reject: q.reject,
|
|
5226
|
+
retried: true
|
|
5227
|
+
};
|
|
5228
|
+
if (q.awaitingDescribe) this.send(syncMessage());
|
|
5229
|
+
break;
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
if (q.awaitingDescribe) this.send(syncMessage());
|
|
4423
5233
|
q.reject(err);
|
|
4424
5234
|
}
|
|
4425
5235
|
break;
|
|
@@ -4478,15 +5288,28 @@ var PgConnection = class _PgConnection {
|
|
|
4478
5288
|
}
|
|
4479
5289
|
return entry;
|
|
4480
5290
|
}
|
|
4481
|
-
/** LRU
|
|
5291
|
+
/** LRU 写入:刷新位置;超上限淘汰最旧(长运行服务防无限累积)——淘汰的 statement 推入 deallocQueue,连接空闲时服务端同步释放 */
|
|
4482
5292
|
setPrepared(sig, entry) {
|
|
4483
5293
|
this.prepared.delete(sig);
|
|
4484
5294
|
this.prepared.set(sig, entry);
|
|
4485
5295
|
if (this.prepared.size > _PgConnection.PREPARED_MAX) {
|
|
4486
5296
|
const oldest = this.prepared.keys().next().value;
|
|
4487
|
-
if (oldest !== void 0)
|
|
5297
|
+
if (oldest !== void 0) {
|
|
5298
|
+
const evicted = this.prepared.get(oldest);
|
|
5299
|
+
this.prepared.delete(oldest);
|
|
5300
|
+
if (evicted) this.deallocQueue.push(evicted.name);
|
|
5301
|
+
}
|
|
4488
5302
|
}
|
|
4489
5303
|
}
|
|
5304
|
+
/** 连接空闲时批量 DEALLOCATE:LRU 淘汰的 statement 不残留服务端(plan 缓存膨胀防线)。
|
|
5305
|
+
* 同步 write 先于后续查询(调用点在 Z handler 的同步段内,resolve 的微任务后至)。
|
|
5306
|
+
* 其响应 C+Z 在无 currentQuery 时到达——awaitingDeallocZ 消费首个 Z,后续查询 Z 正常。 */
|
|
5307
|
+
flushDealloc() {
|
|
5308
|
+
if (this.deallocQueue.length === 0 || !this.socket) return;
|
|
5309
|
+
const names = this.deallocQueue.splice(0);
|
|
5310
|
+
this.awaitingDeallocZ = true;
|
|
5311
|
+
this.socket.write(queryMessage(names.map((n) => `DEALLOCATE "${n}"`).join("; ")));
|
|
5312
|
+
}
|
|
4490
5313
|
/** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
|
|
4491
5314
|
async transaction(fn) {
|
|
4492
5315
|
await this.query("BEGIN");
|
|
@@ -4524,21 +5347,7 @@ var PgConnection = class _PgConnection {
|
|
|
4524
5347
|
let stmtEntry = this.getPrepared(sig);
|
|
4525
5348
|
const encoded = encodeParams(params);
|
|
4526
5349
|
if (!stmtEntry) {
|
|
4527
|
-
|
|
4528
|
-
this.currentQuery = {
|
|
4529
|
-
columns: [],
|
|
4530
|
-
rows: [],
|
|
4531
|
-
resolve: resolve3,
|
|
4532
|
-
reject,
|
|
4533
|
-
sql,
|
|
4534
|
-
params: encoded,
|
|
4535
|
-
awaitingDescribe: true,
|
|
4536
|
-
prepKey: sig,
|
|
4537
|
-
prepName: name
|
|
4538
|
-
};
|
|
4539
|
-
this.socket.write(parseMessage(name, sql, params.map(() => 0)));
|
|
4540
|
-
this.socket.write(describeMessage("S", name));
|
|
4541
|
-
this.socket.write(flushMessage());
|
|
5350
|
+
this.issueExtendedQuery(sql, encoded, resolve3, reject);
|
|
4542
5351
|
} else {
|
|
4543
5352
|
this.currentQuery = {
|
|
4544
5353
|
columns: [...stmtEntry.columns],
|
|
@@ -4547,7 +5356,9 @@ var PgConnection = class _PgConnection {
|
|
|
4547
5356
|
reject,
|
|
4548
5357
|
sql,
|
|
4549
5358
|
params: encoded,
|
|
4550
|
-
awaitingDescribe: false
|
|
5359
|
+
awaitingDescribe: false,
|
|
5360
|
+
prepKey: sig,
|
|
5361
|
+
prepName: stmtEntry.name
|
|
4551
5362
|
};
|
|
4552
5363
|
this.socket.write(bindMessage(stmtEntry.name, encoded));
|
|
4553
5364
|
this.socket.write(executeMessage());
|
|
@@ -4556,6 +5367,28 @@ var PgConnection = class _PgConnection {
|
|
|
4556
5367
|
}
|
|
4557
5368
|
});
|
|
4558
5369
|
}
|
|
5370
|
+
/**
|
|
5371
|
+
* 扩展查询首次准备:Parse + Describe + Flush(无 Sync——错误时由调用方发 Sync 复位)。
|
|
5372
|
+
* retry 路径复用:新语句名强制服务器按当前 schema 重新解析(DDL 失效恢复)。
|
|
5373
|
+
*/
|
|
5374
|
+
issueExtendedQuery(sql, params, resolve3, reject, retried) {
|
|
5375
|
+
const name = `wf_s${++this.stmtSeq}`;
|
|
5376
|
+
this.currentQuery = {
|
|
5377
|
+
columns: [],
|
|
5378
|
+
rows: [],
|
|
5379
|
+
resolve: resolve3,
|
|
5380
|
+
reject,
|
|
5381
|
+
sql,
|
|
5382
|
+
params,
|
|
5383
|
+
awaitingDescribe: true,
|
|
5384
|
+
prepKey: `${sql}|${params.length}`,
|
|
5385
|
+
prepName: name,
|
|
5386
|
+
retried
|
|
5387
|
+
};
|
|
5388
|
+
this.socket?.write(parseMessage(name, sql, params.map(() => 0)));
|
|
5389
|
+
this.socket?.write(describeMessage("S", name));
|
|
5390
|
+
this.socket?.write(flushMessage());
|
|
5391
|
+
}
|
|
4559
5392
|
send(data) {
|
|
4560
5393
|
this.socket?.write(data);
|
|
4561
5394
|
}
|
|
@@ -4631,6 +5464,8 @@ function convertValue(oid, value) {
|
|
|
4631
5464
|
return parseFloat(value);
|
|
4632
5465
|
case 16:
|
|
4633
5466
|
return value === "t";
|
|
5467
|
+
case 1184:
|
|
5468
|
+
return new Date(value);
|
|
4634
5469
|
default:
|
|
4635
5470
|
return value;
|
|
4636
5471
|
}
|
|
@@ -4691,6 +5526,7 @@ var PgPool = class _PgPool {
|
|
|
4691
5526
|
opts;
|
|
4692
5527
|
initPromise = null;
|
|
4693
5528
|
schemas = /* @__PURE__ */ new Map();
|
|
5529
|
+
idleTimer = null;
|
|
4694
5530
|
/** 懒连接:构造不连接,ensure() 首次初始化(中间件注入场景) */
|
|
4695
5531
|
constructor(options = {}) {
|
|
4696
5532
|
this.opts = options;
|
|
@@ -4720,13 +5556,54 @@ var PgPool = class _PgPool {
|
|
|
4720
5556
|
this.all = conns;
|
|
4721
5557
|
this.available = [...conns];
|
|
4722
5558
|
this.readyPromise = Promise.resolve();
|
|
5559
|
+
this.startIdleReaper();
|
|
5560
|
+
}
|
|
5561
|
+
/** 空闲回收定时器(idleTimeoutMs > 0 时启动;unref 不阻塞进程退出) */
|
|
5562
|
+
startIdleReaper() {
|
|
5563
|
+
const ms = this.opts.idleTimeoutMs ?? 0;
|
|
5564
|
+
if (ms <= 0 || this.idleTimer) return;
|
|
5565
|
+
this.idleTimer = setInterval(() => this.reapIdle(), ms);
|
|
5566
|
+
this.idleTimer.unref?.();
|
|
5567
|
+
}
|
|
5568
|
+
/** 扫描空闲连接:超时未使用 → close + 从池移除(容量收缩;acquire 时自动重建) */
|
|
5569
|
+
reapIdle() {
|
|
5570
|
+
if (this.closed) return;
|
|
5571
|
+
const now = Date.now();
|
|
5572
|
+
const ms = this.opts.idleTimeoutMs ?? 0;
|
|
5573
|
+
for (let i = this.available.length - 1; i >= 0; i--) {
|
|
5574
|
+
const conn = this.available[i];
|
|
5575
|
+
if (conn.lastUsed > 0 && now - conn.lastUsed >= ms) {
|
|
5576
|
+
this.available.splice(i, 1);
|
|
5577
|
+
const idx = this.all.indexOf(conn);
|
|
5578
|
+
if (idx >= 0) this.all.splice(idx, 1);
|
|
5579
|
+
conn.close();
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
4723
5582
|
}
|
|
4724
|
-
/**
|
|
5583
|
+
/** 当前打开的连接数(含借出;测试/观测用) */
|
|
5584
|
+
get open() {
|
|
5585
|
+
return this.all.length;
|
|
5586
|
+
}
|
|
5587
|
+
/** 获取一个空闲连接(全忙则排队等待;池被空闲回收收缩时自动新建恢复容量) */
|
|
4725
5588
|
acquire() {
|
|
4726
5589
|
if (this.closed) return Promise.reject(new ConnectionError("postgres: pool is closed"));
|
|
4727
5590
|
if (this.available.length > 0) {
|
|
4728
5591
|
return Promise.resolve(this.available.pop());
|
|
4729
5592
|
}
|
|
5593
|
+
const poolSize = this.opts.poolSize ?? 5;
|
|
5594
|
+
if (this.all.length < poolSize) {
|
|
5595
|
+
const c = new PgConnection(this.opts);
|
|
5596
|
+
return c.connect().then(
|
|
5597
|
+
() => {
|
|
5598
|
+
this.all.push(c);
|
|
5599
|
+
return c;
|
|
5600
|
+
},
|
|
5601
|
+
() => {
|
|
5602
|
+
setTimeout(() => this.replenish(), 500);
|
|
5603
|
+
throw new ConnectionError("postgres: failed to establish connection");
|
|
5604
|
+
}
|
|
5605
|
+
);
|
|
5606
|
+
}
|
|
4730
5607
|
const timeoutMs = this.opts.acquireTimeoutMs ?? 3e4;
|
|
4731
5608
|
return new Promise((resolve3, reject) => {
|
|
4732
5609
|
const waiter = {
|
|
@@ -4753,6 +5630,7 @@ var PgPool = class _PgPool {
|
|
|
4753
5630
|
this.replenish();
|
|
4754
5631
|
return;
|
|
4755
5632
|
}
|
|
5633
|
+
conn.lastUsed = Date.now();
|
|
4756
5634
|
this.dispatchAvailable(conn);
|
|
4757
5635
|
}
|
|
4758
5636
|
/** 连接可用事件统一入口:优先唤醒等待者,否则回空闲池(release 与 replenish 共用) */
|
|
@@ -4796,6 +5674,69 @@ var PgPool = class _PgPool {
|
|
|
4796
5674
|
register(table, schema) {
|
|
4797
5675
|
this.schemas.set(table, schema);
|
|
4798
5676
|
}
|
|
5677
|
+
/**
|
|
5678
|
+
* 批量插入:多行 VALUES 单次往返(种子/批量写场景 N 往返 → 1)。
|
|
5679
|
+
* 诚实裁剪:所有行必须键集合一致(否则抛 ValidationError);batchSize 分批(默认 500)。
|
|
5680
|
+
*/
|
|
5681
|
+
async insertMany(table, rows, opts = {}) {
|
|
5682
|
+
const empty = [];
|
|
5683
|
+
if (rows.length === 0) {
|
|
5684
|
+
Object.defineProperty(empty, "affectedRows", { value: 0, enumerable: false, writable: true });
|
|
5685
|
+
return empty;
|
|
5686
|
+
}
|
|
5687
|
+
const schema = this.schemas.get(table);
|
|
5688
|
+
const keys = Object.keys(rows[0]).sort();
|
|
5689
|
+
const batchSize = Math.max(1, opts.batchSize ?? 500);
|
|
5690
|
+
let total = 0;
|
|
5691
|
+
let last = empty;
|
|
5692
|
+
for (let i = 0; i < rows.length; i += batchSize) {
|
|
5693
|
+
const batch = rows.slice(i, i + batchSize);
|
|
5694
|
+
for (let r2 = 0; r2 < batch.length; r2++) {
|
|
5695
|
+
const row = batch[r2];
|
|
5696
|
+
const rowKeys = Object.keys(row).sort();
|
|
5697
|
+
if (rowKeys.length !== keys.length || rowKeys.some((k, j) => k !== keys[j])) {
|
|
5698
|
+
throw new ValidationError(
|
|
5699
|
+
`schema: insertMany requires identical keys across rows (row ${i + r2} has [${rowKeys}], expected [${keys}])`
|
|
5700
|
+
);
|
|
5701
|
+
}
|
|
5702
|
+
if (schema) validateRow(schema, row);
|
|
5703
|
+
}
|
|
5704
|
+
const vals = batch.map((_, r2) => `(${keys.map((_2, c) => `$${r2 * keys.length + c + 1}`).join(", ")})`).join(", ");
|
|
5705
|
+
const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${vals}`;
|
|
5706
|
+
const params = batch.flatMap((row) => keys.map((k) => row[k]));
|
|
5707
|
+
const r = await this.query(sql, params);
|
|
5708
|
+
total += r.affectedRows ?? batch.length;
|
|
5709
|
+
if (r.length > 0) last = r;
|
|
5710
|
+
}
|
|
5711
|
+
Object.defineProperty(last, "affectedRows", { value: total, enumerable: false, writable: true });
|
|
5712
|
+
return last;
|
|
5713
|
+
}
|
|
5714
|
+
/** 参数化 UPDATE:SET + WHERE 全部参数化(防注入);返回 affectedRows;returning 可回读修改行 */
|
|
5715
|
+
async update(table, set, where, opts = {}) {
|
|
5716
|
+
const schema = this.schemas.get(table);
|
|
5717
|
+
if (schema) validateRow(schema, { ...set, ...where });
|
|
5718
|
+
const setCols = Object.keys(set);
|
|
5719
|
+
if (setCols.length === 0) throw new ValidationError("schema: update requires at least one SET column");
|
|
5720
|
+
const whereCols = Object.keys(where);
|
|
5721
|
+
if (whereCols.length === 0) {
|
|
5722
|
+
throw new ValidationError("schema: update requires a WHERE clause (full-table update via unsafe)");
|
|
5723
|
+
}
|
|
5724
|
+
const setSql = setCols.map((c, i) => `${c} = $${i + 1}`).join(", ");
|
|
5725
|
+
const whereSql = whereCols.map((c, i) => `${c} = $${setCols.length + i + 1}`).join(" AND ");
|
|
5726
|
+
const returning = opts.returning?.length ? ` RETURNING ${opts.returning.join(", ")}` : "";
|
|
5727
|
+
const sql = `UPDATE ${table} SET ${setSql} WHERE ${whereSql}${returning}`;
|
|
5728
|
+
const params = [...setCols.map((c) => set[c]), ...whereCols.map((c) => where[c])];
|
|
5729
|
+
return this.query(sql, params);
|
|
5730
|
+
}
|
|
5731
|
+
/** 参数化 DELETE:WHERE 参数化;返回 affectedRows。WHERE 必填(防全表误删) */
|
|
5732
|
+
async delete(table, where) {
|
|
5733
|
+
const schema = this.schemas.get(table);
|
|
5734
|
+
if (schema) validateRow(schema, where);
|
|
5735
|
+
const whereCols = Object.keys(where);
|
|
5736
|
+
if (whereCols.length === 0) throw new ValidationError("schema: delete requires a WHERE clause");
|
|
5737
|
+
const sql = `DELETE FROM ${table} WHERE ${whereCols.map((c, i) => `${c} = $${i + 1}`).join(" AND ")}`;
|
|
5738
|
+
return this.query(sql, whereCols.map((c) => where[c]));
|
|
5739
|
+
}
|
|
4799
5740
|
/** 写前校验 + 参数化插入(schema 驱动,脏数据源头拦截) */
|
|
4800
5741
|
async insert(table, row) {
|
|
4801
5742
|
const schema = this.schemas.get(table);
|
|
@@ -4865,6 +5806,10 @@ var PgPool = class _PgPool {
|
|
|
4865
5806
|
}
|
|
4866
5807
|
async close() {
|
|
4867
5808
|
this.closed = true;
|
|
5809
|
+
if (this.idleTimer) {
|
|
5810
|
+
clearInterval(this.idleTimer);
|
|
5811
|
+
this.idleTimer = null;
|
|
5812
|
+
}
|
|
4868
5813
|
this.readyPromise = null;
|
|
4869
5814
|
this.available = [];
|
|
4870
5815
|
await Promise.all(this.all.map((c) => c.close()));
|
|
@@ -4900,7 +5845,9 @@ function parseTagged(strings, values) {
|
|
|
4900
5845
|
}
|
|
4901
5846
|
|
|
4902
5847
|
// src/postgres/client.ts
|
|
5848
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4903
5849
|
var MIGRATIONS_TABLE = "_weifuwu_migrations";
|
|
5850
|
+
var traceStore = new AsyncLocalStorage();
|
|
4904
5851
|
function postgres(options) {
|
|
4905
5852
|
const opts = typeof options === "string" ? { connection: options } : options ?? {};
|
|
4906
5853
|
const connection = opts.connection ?? process.env.DATABASE_URL;
|
|
@@ -4919,12 +5866,16 @@ function postgres(options) {
|
|
|
4919
5866
|
poolSize: opts.max ?? opts.poolSize ?? 10,
|
|
4920
5867
|
acquireTimeoutMs: opts.acquireTimeoutMs,
|
|
4921
5868
|
statementTimeoutMs: opts.statementTimeoutMs ?? opts.statementTimeout,
|
|
4922
|
-
onQuery
|
|
5869
|
+
// onQuery 包装:从 ALS 读请求级 traceId 追加到第 4 参数(后端兼容——不传时不注入)
|
|
5870
|
+
onQuery: opts.onQuery ? (sql2, durationMs, rowCount) => {
|
|
5871
|
+
const tid = traceStore.getStore();
|
|
5872
|
+
opts.onQuery?.(sql2, durationMs, rowCount, tid || void 0);
|
|
5873
|
+
} : void 0
|
|
4923
5874
|
});
|
|
4924
5875
|
const sql = makeSql(pool);
|
|
4925
5876
|
const mw = ((req, ctx, next) => {
|
|
4926
5877
|
ctx.sql = sql;
|
|
4927
|
-
return next(req, ctx);
|
|
5878
|
+
return traceStore.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
|
|
4928
5879
|
});
|
|
4929
5880
|
mw.__meta = { injects: ["sql"], depends: [] };
|
|
4930
5881
|
mw.sql = sql;
|
|
@@ -5030,6 +5981,8 @@ function wrapError(promise) {
|
|
|
5030
5981
|
}
|
|
5031
5982
|
|
|
5032
5983
|
// src/redis/client.ts
|
|
5984
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
5985
|
+
var traceStore2 = new AsyncLocalStorage2();
|
|
5033
5986
|
function redis(options) {
|
|
5034
5987
|
const opts = typeof options === "string" ? { url: options } : options ?? {};
|
|
5035
5988
|
const url = opts.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
|
|
@@ -5039,11 +5992,18 @@ function redis(options) {
|
|
|
5039
5992
|
port: Number(u.port || 6379),
|
|
5040
5993
|
poolSize: opts.poolSize,
|
|
5041
5994
|
keyPrefix: opts.keyPrefix,
|
|
5042
|
-
enableOfflineQueue: opts.enableOfflineQueue
|
|
5995
|
+
enableOfflineQueue: opts.enableOfflineQueue,
|
|
5996
|
+
commandTimeoutMs: opts.commandTimeoutMs,
|
|
5997
|
+
socketTimeoutMs: opts.socketTimeoutMs,
|
|
5998
|
+
// onCommand 包装:从 ALS 读请求级 traceId 追加到第 4 参数(无头不注入)
|
|
5999
|
+
onCommand: opts.onCommand ? (cmd, args, dur) => {
|
|
6000
|
+
const tid = traceStore2.getStore();
|
|
6001
|
+
opts.onCommand?.(cmd, args, dur, tid || void 0);
|
|
6002
|
+
} : void 0
|
|
5043
6003
|
});
|
|
5044
6004
|
const mw = (async (req, ctx, next) => {
|
|
5045
6005
|
ctx.redis = pool;
|
|
5046
|
-
return next(req, ctx);
|
|
6006
|
+
return traceStore2.run(req.headers.get("x-trace-id") ?? "", () => next(req, ctx));
|
|
5047
6007
|
});
|
|
5048
6008
|
mw.__meta = { injects: ["redis"], depends: [] };
|
|
5049
6009
|
mw.redis = pool;
|
|
@@ -5085,6 +6045,7 @@ export {
|
|
|
5085
6045
|
rateLimit,
|
|
5086
6046
|
redirect,
|
|
5087
6047
|
redis,
|
|
6048
|
+
scheduler,
|
|
5088
6049
|
sendSmtp,
|
|
5089
6050
|
serve,
|
|
5090
6051
|
serveStatic,
|