weifuwu 0.17.13 → 0.17.17

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
@@ -571,12 +571,15 @@ import { pathToFileURL } from "node:url";
571
571
  import { createHash } from "node:crypto";
572
572
  import vm from "node:vm";
573
573
  import { createRequire } from "node:module";
574
+ import { AsyncLocalStorage } from "node:async_hooks";
574
575
  import chokidar from "chokidar";
575
576
 
576
577
  // tsx-context.ts
577
578
  import { useSyncExternalStore, createContext } from "react";
578
579
  var fallbackT = (key, _params, fallback) => fallback ?? key;
579
- var _ctx = { params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} };
580
+ var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} };
581
+ var _ctx = DEFAULT_CTX;
582
+ var _snapshot = { params: _ctx.params, query: _ctx.query, user: _ctx.user, parsed: _ctx.parsed, prefs: _ctx.prefs, env: _ctx.env };
580
583
  var _listeners = /* @__PURE__ */ new Set();
581
584
  var subscribe = (cb) => {
582
585
  _listeners.add(cb);
@@ -584,16 +587,26 @@ var subscribe = (cb) => {
584
587
  _listeners.delete(cb);
585
588
  };
586
589
  };
587
- var getSnapshot = () => ({ params: _ctx.params, query: _ctx.query, user: _ctx.user, parsed: _ctx.parsed, prefs: _ctx.prefs, env: _ctx.env });
590
+ var getSnapshot = () => _snapshot;
588
591
  var getServerSnapshot = getSnapshot;
592
+ var _alsGetStore = null;
593
+ function __registerAls(getStore) {
594
+ _alsGetStore = getStore;
595
+ }
589
596
  function setCtx(value) {
590
597
  _ctx = { ..._ctx, ...value };
598
+ _snapshot = { params: _ctx.params, query: _ctx.query, user: _ctx.user, parsed: _ctx.parsed, prefs: _ctx.prefs, env: _ctx.env };
591
599
  _listeners.forEach((fn) => fn());
592
600
  }
601
+ var _cachedT = null;
593
602
  function _buildT() {
603
+ if (_cachedT) return _cachedT;
594
604
  const messages2 = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
595
- if (!messages2) return fallbackT;
596
- return (key, params, fallback) => {
605
+ if (!messages2) {
606
+ _cachedT = fallbackT;
607
+ return fallbackT;
608
+ }
609
+ _cachedT = (key, params, fallback) => {
597
610
  const msg = key.split(".").reduce((o, k) => o?.[k], messages2);
598
611
  if (msg === void 0 || msg === null) return fallback ?? key;
599
612
  if (!params) return String(msg);
@@ -601,16 +614,27 @@ function _buildT() {
601
614
  for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
602
615
  return result;
603
616
  };
617
+ return _cachedT;
618
+ }
619
+ function _readCtx() {
620
+ const alsStore = _alsGetStore?.();
621
+ const base = alsStore ?? _ctx;
622
+ const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
623
+ const t = typeof base.t === "function" && base.t !== fallbackT ? base.t : _buildT();
624
+ return { ...base, ...data, t };
604
625
  }
605
626
  function useCtx() {
606
627
  useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
607
- const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
608
- const t = _ctx.t !== fallbackT ? _ctx.t : _buildT();
609
- return { ..._ctx, ...data, t };
628
+ return _readCtx();
629
+ }
630
+ function getCtx() {
631
+ return _readCtx();
610
632
  }
611
- var TsxContext = createContext({ params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} });
633
+ var TsxContext = createContext(DEFAULT_CTX);
612
634
 
613
635
  // tsx-instance.ts
636
+ var als = new AsyncLocalStorage();
637
+ __registerAls(() => als.getStore());
614
638
  var liveReloadClients = /* @__PURE__ */ new Set();
615
639
  function broadcastReload() {
616
640
  for (const ws of liveReloadClients) {
@@ -625,13 +649,13 @@ var isDev = process.env.NODE_ENV !== "production";
625
649
  var _tailwindPlugin = null;
626
650
  var _postcss = null;
627
651
  var _cjsRequire = createRequire(import.meta.url);
628
- var _vmCtx = vm.createContext(Object.create(globalThis));
629
652
  function loadSSRModule(code) {
653
+ const ctx = vm.createContext(Object.create(globalThis));
630
654
  const mod = { exports: {} };
631
- _vmCtx.require = (name) => _cjsRequire(name);
632
- _vmCtx.module = mod;
633
- _vmCtx.exports = mod.exports;
634
- new vm.Script(code).runInContext(_vmCtx);
655
+ ctx.require = (name) => _cjsRequire(name);
656
+ ctx.module = mod;
657
+ ctx.exports = mod.exports;
658
+ new vm.Script(code).runInContext(ctx);
635
659
  return mod.exports;
636
660
  }
637
661
  function id(s) {
@@ -794,6 +818,10 @@ var TsxInstance = class {
794
818
  clientBundleCache = /* @__PURE__ */ new Map();
795
819
  clientBuildParams = /* @__PURE__ */ new Map();
796
820
  clientRouteLog = /* @__PURE__ */ new Set();
821
+ // file watchers (dev mode, stored for cleanup)
822
+ watcher = null;
823
+ twWatcher = null;
824
+ debounceTimer = null;
797
825
  constructor(options) {
798
826
  this.uiDir = resolve2(options.dir);
799
827
  this.pagesDir = existsSync2(join(this.uiDir, "pages")) ? join(this.uiDir, "pages") : this.uiDir;
@@ -802,7 +830,7 @@ var TsxInstance = class {
802
830
  }
803
831
  async build() {
804
832
  const pages = scanPages(this.pagesDir);
805
- if (pages.length === 0) return this.router;
833
+ if (pages.length === 0) return attachStop(this.router, this);
806
834
  const allFiles = /* @__PURE__ */ new Set();
807
835
  for (const p of pages) {
808
836
  if (p.entryPath) allFiles.add(p.entryPath);
@@ -892,28 +920,35 @@ var TsxInstance = class {
892
920
  const nfMod = this.pageModules.get(nfPath);
893
921
  if (!nfMod) return new Response("Not Found", { status: 404 });
894
922
  const NfComponent = nfMod.default;
895
- setCtx({
923
+ const ctxValue = {
896
924
  params: ctx.params,
897
925
  query: ctx.query,
898
- user: ctx.user,
899
- parsed: ctx.parsed,
900
- prefs: ctx.prefs,
901
- t: ctx.t,
902
- env: ctx.env
903
- });
904
- let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
905
- for (let i = rootLayouts.length - 1; i >= 0; i--) {
906
- const LMod = this.layoutModules.get(rootLayouts[i]);
907
- if (!LMod) continue;
908
- element = createElement(LMod.default, { children: element });
909
- }
910
- const stream = await renderToReadableStream(element);
911
- return streamResponse(stream, {
912
- ctx,
913
- base,
914
- compiledTailwindCss: this.compiledTailwindCss,
915
- isDev,
916
- status: 404
926
+ user: ctx.user ?? {},
927
+ parsed: ctx.parsed ?? {},
928
+ prefs: ctx.prefs ?? {},
929
+ t: ctx.t ?? ((key) => key),
930
+ env: ctx.env ?? {}
931
+ };
932
+ return als.run(ctxValue, async () => {
933
+ setCtx(ctxValue);
934
+ let element = createElement(
935
+ TsxContext.Provider,
936
+ { value: ctxValue },
937
+ createElement(NfComponent, { params: ctx.params, query: ctx.query })
938
+ );
939
+ for (let i = rootLayouts.length - 1; i >= 0; i--) {
940
+ const LMod = this.layoutModules.get(rootLayouts[i]);
941
+ if (!LMod) continue;
942
+ element = createElement(LMod.default, { children: element });
943
+ }
944
+ const stream = await renderToReadableStream(element);
945
+ return streamResponse(stream, {
946
+ ctx,
947
+ base,
948
+ compiledTailwindCss: this.compiledTailwindCss,
949
+ isDev,
950
+ status: 404
951
+ });
917
952
  });
918
953
  };
919
954
  this.router.all("/*", handler);
@@ -935,7 +970,17 @@ var TsxInstance = class {
935
970
  });
936
971
  this.startFileWatcher();
937
972
  }
938
- return this.router;
973
+ return attachStop(this.router, this);
974
+ }
975
+ /**
976
+ * Clean up file watchers and pending timers. Call when shutting down
977
+ * to prevent resource leaks.
978
+ */
979
+ stop() {
980
+ this.watcher?.close();
981
+ this.twWatcher?.close();
982
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
983
+ this.debounceTimer = null;
939
984
  }
940
985
  // ── Tailwind CSS ──────────────────────────────────────────────────────────
941
986
  async compileTailwind() {
@@ -972,7 +1017,8 @@ ${src}`;
972
1017
  }));
973
1018
  if (isDev) {
974
1019
  const inputFile = resolve2(this.uiDir, "app.css");
975
- chokidar.watch(inputFile, { persistent: false }).on("change", async () => {
1020
+ this.twWatcher = chokidar.watch(inputFile, { persistent: false });
1021
+ this.twWatcher.on("change", async () => {
976
1022
  this.compiledTailwindCss = "";
977
1023
  await this.compileTailwind();
978
1024
  broadcastReload();
@@ -1068,77 +1114,84 @@ ${src}`;
1068
1114
  const pageMod = this.pageModules.get(entryPath);
1069
1115
  if (!pageMod) return new Response("", { status: 500 });
1070
1116
  const Component = pageMod.default;
1071
- const loadMod = loadPath ? this.loadModules.get(loadPath) : void 0;
1072
- const loadFn = loadMod?.default;
1073
- const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
1074
- const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
1075
- setCtx({
1117
+ const ctxValue = {
1076
1118
  params: ctx.params,
1077
1119
  query: ctx.query,
1078
- user: ctx.user,
1079
- parsed: ctx.parsed,
1080
- prefs: ctx.prefs,
1081
- t: ctx.t,
1082
- env: ctx.env
1083
- });
1084
- let element = createElement(
1085
- "div",
1086
- { id: "__weifuwu_root" },
1087
- createElement(Component, allProps)
1088
- );
1089
- if (layoutPaths.length === 0) {
1090
- element = createElement(
1091
- "html",
1092
- { lang: "en" },
1120
+ user: ctx.user ?? {},
1121
+ parsed: ctx.parsed ?? {},
1122
+ prefs: ctx.prefs ?? {},
1123
+ t: ctx.t ?? ((key) => key),
1124
+ env: ctx.env ?? {}
1125
+ };
1126
+ return als.run(ctxValue, async () => {
1127
+ setCtx(ctxValue);
1128
+ const loadMod = loadPath ? this.loadModules.get(loadPath) : void 0;
1129
+ const loadFn = loadMod?.default;
1130
+ const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
1131
+ const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
1132
+ let element = createElement(
1133
+ TsxContext.Provider,
1134
+ { value: ctxValue },
1093
1135
  createElement(
1094
- "head",
1095
- null,
1096
- createElement("meta", { charSet: "utf-8" }),
1097
- createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
1098
- createElement("title", null, "weifuwu")
1099
- ),
1100
- createElement("body", null, element)
1136
+ "div",
1137
+ { id: "__weifuwu_root" },
1138
+ createElement(Component, allProps)
1139
+ )
1101
1140
  );
1102
- } else {
1103
- for (let i = layoutPaths.length - 1; i >= 0; i--) {
1104
- const lp = layoutPaths[i];
1105
- const LMod = this.layoutModules.get(lp);
1106
- if (!LMod) continue;
1107
- const Layout = LMod.default;
1108
- const isRoot = i === 0;
1141
+ if (layoutPaths.length === 0) {
1109
1142
  element = createElement(
1110
- Layout,
1111
- isRoot ? { children: element, req } : { children: element }
1143
+ "html",
1144
+ { lang: "en" },
1145
+ createElement(
1146
+ "head",
1147
+ null,
1148
+ createElement("meta", { charSet: "utf-8" }),
1149
+ createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
1150
+ createElement("title", null, "weifuwu")
1151
+ ),
1152
+ createElement("body", null, element)
1112
1153
  );
1154
+ } else {
1155
+ for (let i = layoutPaths.length - 1; i >= 0; i--) {
1156
+ const lp = layoutPaths[i];
1157
+ const LMod = this.layoutModules.get(lp);
1158
+ if (!LMod) continue;
1159
+ const Layout = LMod.default;
1160
+ const isRoot = i === 0;
1161
+ element = createElement(
1162
+ Layout,
1163
+ isRoot ? { children: element, req } : { children: element }
1164
+ );
1165
+ }
1113
1166
  }
1114
- }
1115
- const bundle = await this.getOrBuildClientBundle(entryPath, layoutPaths, this.pagesDir);
1116
- const stream = await renderToReadableStream(element);
1117
- return streamResponse(stream, {
1118
- ctx,
1119
- base,
1120
- compiledTailwindCss: this.compiledTailwindCss,
1121
- isDev,
1122
- bundle,
1123
- allProps
1167
+ const bundle = await this.getOrBuildClientBundle(entryPath, layoutPaths, this.pagesDir);
1168
+ const stream = await renderToReadableStream(element);
1169
+ return streamResponse(stream, {
1170
+ ctx,
1171
+ base,
1172
+ compiledTailwindCss: this.compiledTailwindCss,
1173
+ isDev,
1174
+ bundle,
1175
+ allProps
1176
+ });
1124
1177
  });
1125
1178
  };
1126
1179
  }
1127
1180
  // ── dev file watcher ──────────────────────────────────────────────────────
1128
1181
  startFileWatcher() {
1129
- let timeout = null;
1130
1182
  const pending = /* @__PURE__ */ new Set();
1131
- chokidar.watch(this.uiDir, {
1183
+ this.watcher = chokidar.watch(this.uiDir, {
1132
1184
  ignored: /(^|[/\\])\.(?!\.)|node_modules|[/\\]\.weifuwu[/\\]|[/\\]dist[/\\]/,
1133
1185
  persistent: false,
1134
1186
  ignoreInitial: true
1135
- }).on("all", async (event, filePath) => {
1187
+ });
1188
+ this.watcher.on("all", async (event, filePath) => {
1136
1189
  if (event !== "change" && event !== "add") return;
1137
1190
  if (!/\.tsx?$/.test(filePath)) return;
1138
1191
  pending.add(filePath);
1139
- if (timeout) clearTimeout(timeout);
1140
- timeout = setTimeout(async () => {
1141
- timeout = null;
1192
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
1193
+ this.debounceTimer = setTimeout(async () => {
1194
+ this.debounceTimer = null;
1142
1195
  const files = [...pending];
1143
1196
  pending.clear();
1144
1197
  const exists = files.filter((f) => existsSync2(f));
@@ -1286,6 +1339,11 @@ ${src}`;
1286
1339
  }
1287
1340
  }
1288
1341
  };
1342
+ function attachStop(router, instance) {
1343
+ ;
1344
+ router.stop = () => instance.stop();
1345
+ return router;
1346
+ }
1289
1347
  function streamResponse(reactStream, opts) {
1290
1348
  const decoder = new TextDecoder();
1291
1349
  const encoder2 = new TextEncoder();
@@ -1295,47 +1353,54 @@ function streamResponse(reactStream, opts) {
1295
1353
  let extractedHead = "";
1296
1354
  const output = new ReadableStream({
1297
1355
  async start(controller) {
1298
- const reader = reactStream.getReader();
1299
- async function push(chunk) {
1300
- buffer += decoder.decode(chunk, { stream: true });
1301
- if (!extractedHead) {
1302
- const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
1303
- if (m) {
1304
- extractedHead = m[1];
1305
- buffer = buffer.replace(m[0], "");
1356
+ try {
1357
+ const reader = reactStream.getReader();
1358
+ async function push(chunk) {
1359
+ buffer += decoder.decode(chunk, { stream: true });
1360
+ if (!extractedHead) {
1361
+ const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
1362
+ if (m) {
1363
+ extractedHead = m[1];
1364
+ buffer = buffer.replace(m[0], "");
1365
+ }
1306
1366
  }
1307
- }
1308
- if (!headFlushed) {
1309
- const idx = buffer.indexOf("</head>");
1310
- if (idx !== -1) {
1311
- const before = buffer.slice(0, idx);
1312
- let injection = "";
1313
- if (extractedHead) injection += "\n" + extractedHead;
1314
- injection += headPayload;
1315
- controller.enqueue(encoder2.encode(before + injection));
1316
- buffer = buffer.slice(idx);
1317
- headFlushed = true;
1367
+ if (!headFlushed) {
1368
+ const idx = buffer.indexOf("</head>");
1369
+ if (idx !== -1) {
1370
+ const before = buffer.slice(0, idx);
1371
+ let injection = "";
1372
+ if (extractedHead) injection += "\n" + extractedHead;
1373
+ injection += headPayload;
1374
+ controller.enqueue(encoder2.encode(before + injection));
1375
+ buffer = buffer.slice(idx);
1376
+ headFlushed = true;
1377
+ }
1378
+ return;
1318
1379
  }
1319
- return;
1380
+ controller.enqueue(encoder2.encode(buffer));
1381
+ buffer = "";
1320
1382
  }
1321
- controller.enqueue(encoder2.encode(buffer));
1322
- buffer = "";
1323
- }
1324
- while (true) {
1325
- const { done, value } = await reader.read();
1326
- if (done) break;
1327
- await push(value);
1328
- }
1329
- if (buffer) controller.enqueue(encoder2.encode(buffer));
1330
- const body = buildBodyScripts(opts);
1331
- if (body) controller.enqueue(encoder2.encode("\n" + body));
1332
- if (opts.isDev) {
1333
- controller.enqueue(encoder2.encode(
1334
- `
1383
+ while (true) {
1384
+ const { done, value } = await reader.read();
1385
+ if (done) break;
1386
+ await push(value);
1387
+ }
1388
+ buffer = buffer.replace(/<template id="__wfw_head">[\s\S]*?<\/template>/g, "");
1389
+ if (buffer) controller.enqueue(encoder2.encode(buffer));
1390
+ const body = buildBodyScripts(opts);
1391
+ if (body) controller.enqueue(encoder2.encode("\n" + body));
1392
+ if (opts.isDev) {
1393
+ controller.enqueue(encoder2.encode(
1394
+ `
1335
1395
  <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${opts.base}/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`
1336
- ));
1396
+ ));
1397
+ }
1398
+ } catch (err) {
1399
+ const fallback = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500 - Internal Server Error</h1></body></html>`;
1400
+ controller.enqueue(encoder2.encode(fallback));
1401
+ } finally {
1402
+ controller.close();
1337
1403
  }
1338
- controller.close();
1339
1404
  }
1340
1405
  });
1341
1406
  return new Response(output, {
@@ -1343,6 +1408,17 @@ function streamResponse(reactStream, opts) {
1343
1408
  headers: { "content-type": "text/html; charset=utf-8" }
1344
1409
  });
1345
1410
  }
1411
+ var _publicEnv = null;
1412
+ function getPublicEnv() {
1413
+ if (_publicEnv) return _publicEnv;
1414
+ _publicEnv = {};
1415
+ for (const key of Object.keys(process.env)) {
1416
+ if (key.startsWith("WEIFUWU_PUBLIC_")) {
1417
+ _publicEnv[key] = process.env[key];
1418
+ }
1419
+ }
1420
+ return _publicEnv;
1421
+ }
1346
1422
  function buildHeadPayload(opts) {
1347
1423
  const { ctx, base, compiledTailwindCss } = opts;
1348
1424
  let result = "";
@@ -1354,7 +1430,7 @@ function buildHeadPayload(opts) {
1354
1430
  result += `<link rel="stylesheet" href="${base}/__wfw/style.css" />
1355
1431
  `;
1356
1432
  }
1357
- const localeData = globalThis.__LOCALE_DATA__;
1433
+ const localeData = ctx.parsed?.__localeData;
1358
1434
  if (localeData && Object.keys(localeData).length > 0) {
1359
1435
  result += `<script>window.__LOCALE_DATA__=${JSON.stringify(localeData)}</script>
1360
1436
  `;
@@ -1366,12 +1442,7 @@ function buildHeadPayload(opts) {
1366
1442
  parsed: ctx.parsed,
1367
1443
  prefs: ctx.prefs
1368
1444
  };
1369
- const publicEnv = {};
1370
- for (const key of Object.keys(process.env)) {
1371
- if (key.startsWith("WEIFUWU_PUBLIC_")) {
1372
- publicEnv[key] = process.env[key];
1373
- }
1374
- }
1445
+ const publicEnv = getPublicEnv();
1375
1446
  if (Object.keys(publicEnv).length > 0) {
1376
1447
  ctxData.env = publicEnv;
1377
1448
  }
@@ -1400,14 +1471,20 @@ function logger(options) {
1400
1471
  return async (req, ctx, next) => {
1401
1472
  const start = Date.now();
1402
1473
  const url = new URL(req.url);
1403
- const res = await next(req, ctx);
1404
- const ms = Date.now() - start;
1405
- if (options?.format === "combined") {
1406
- console.log(`${req.method} ${url.pathname}${url.search} ${res.status} ${ms}ms`);
1407
- } else {
1408
- console.log(`${req.method} ${url.pathname} ${res.status} ${ms}ms`);
1474
+ try {
1475
+ const res = await next(req, ctx);
1476
+ const ms = Date.now() - start;
1477
+ if (options?.format === "combined") {
1478
+ console.log(`${req.method} ${url.pathname}${url.search} ${res.status} ${ms}ms`);
1479
+ } else {
1480
+ console.log(`${req.method} ${url.pathname} ${res.status} ${ms}ms`);
1481
+ }
1482
+ return res;
1483
+ } catch (err) {
1484
+ const ms = Date.now() - start;
1485
+ console.log(`${req.method} ${url.pathname} 500 ${ms}ms`);
1486
+ throw err;
1409
1487
  }
1410
- return res;
1411
1488
  };
1412
1489
  }
1413
1490
  function cors(options) {
@@ -1547,6 +1624,7 @@ function auth(options) {
1547
1624
  import { createHash as createHash2 } from "node:crypto";
1548
1625
  import { open, realpath } from "node:fs/promises";
1549
1626
  import { extname, resolve as resolve3, normalize, sep as sep2 } from "node:path";
1627
+ import { Readable } from "node:stream";
1550
1628
  function serveStatic(root, options) {
1551
1629
  const rootDir = resolve3(root);
1552
1630
  const opts = options ?? {};
@@ -1602,8 +1680,13 @@ function serveStatic(root, options) {
1602
1680
  "Last-Modified": stat.mtime.toUTCString(),
1603
1681
  "Cache-Control": opts.immutable ? `public, max-age=${opts.maxAge ?? 31536e3}, immutable` : `public, max-age=${opts.maxAge ?? 0}`
1604
1682
  };
1605
- const stream = fileHandle.readableWebStream();
1606
- return new Response(stream, { headers });
1683
+ const readStream = fileHandle.createReadStream();
1684
+ const cleanup = () => fileHandle.close().catch(() => {
1685
+ });
1686
+ readStream.on("close", cleanup);
1687
+ readStream.on("error", cleanup);
1688
+ const webStream = Readable.toWeb(readStream);
1689
+ return new Response(webStream, { headers });
1607
1690
  } catch (err) {
1608
1691
  if (fileHandle) await fileHandle.close().catch(() => {
1609
1692
  });
@@ -1900,20 +1983,26 @@ function rateLimit(options) {
1900
1983
  return req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "global";
1901
1984
  });
1902
1985
  const message = options?.message ?? "Too Many Requests";
1986
+ const MAX_ENTRIES = 1e4;
1903
1987
  const hits = /* @__PURE__ */ new Map();
1904
1988
  const interval = setInterval(() => {
1905
1989
  const now = Date.now();
1906
1990
  for (const [key, entry] of hits) {
1907
1991
  if (entry.reset < now) hits.delete(key);
1908
1992
  }
1909
- }, window2);
1993
+ if (hits.size > MAX_ENTRIES) {
1994
+ const toDelete = [...hits.entries()].sort((a, b) => a[1].reset - b[1].reset).slice(0, hits.size - MAX_ENTRIES);
1995
+ for (const [k] of toDelete) hits.delete(k);
1996
+ }
1997
+ }, Math.min(window2, 3e4));
1910
1998
  if (interval.unref) interval.unref();
1911
1999
  const mw = async (req, ctx, next) => {
1912
2000
  const key = getKey(req);
1913
2001
  const now = Date.now();
1914
- const entry = hits.get(key);
2002
+ let entry = hits.get(key);
1915
2003
  if (!entry || entry.reset < now) {
1916
2004
  hits.set(key, { count: 1, reset: now + window2 });
2005
+ entry = { count: 1, reset: now + window2 };
1917
2006
  const res2 = await next(req, ctx);
1918
2007
  const headers2 = new Headers(res2.headers);
1919
2008
  headers2.set("X-RateLimit-Limit", String(max));
@@ -1949,24 +2038,19 @@ function rateLimit(options) {
1949
2038
  }
1950
2039
 
1951
2040
  // compress.ts
1952
- import { gzipSync, brotliCompressSync, deflateSync, constants } from "node:zlib";
2041
+ import { constants, gzipSync, brotliCompressSync, deflateSync } from "node:zlib";
1953
2042
  function compress(options) {
1954
2043
  const level = options?.level ?? 6;
1955
2044
  const threshold = options?.threshold ?? 1024;
1956
2045
  return async (req, ctx, next) => {
1957
2046
  const accept = req.headers.get("accept-encoding") ?? "";
1958
- const useBrotli = accept.includes("br");
1959
- const useGzip = !useBrotli && accept.includes("gzip");
1960
- const useDeflate = !useBrotli && !useGzip && accept.includes("deflate");
1961
- if (!useBrotli && !useGzip && !useDeflate) {
1962
- return next(req, ctx);
1963
- }
2047
+ const encoding = accept.includes("br") ? "br" : accept.includes("gzip") ? "gzip" : accept.includes("deflate") ? "deflate" : "";
2048
+ if (!encoding) return next(req, ctx);
1964
2049
  const res = await next(req, ctx);
1965
2050
  if (res.status === 304 || res.status === 204 || res.status === 206 || res.status < 200 || res.status >= 300) {
1966
2051
  return res;
1967
2052
  }
1968
- const ce = res.headers.get("content-encoding");
1969
- if (ce) return res;
2053
+ if (res.headers.get("content-encoding")) return res;
1970
2054
  const ct = res.headers.get("content-type") ?? "";
1971
2055
  if (!ct || ct.startsWith("audio/") || ct.startsWith("video/") || ct.startsWith("image/") || ct === "application/zip") {
1972
2056
  return res;
@@ -1974,21 +2058,19 @@ function compress(options) {
1974
2058
  const body = await res.bytes();
1975
2059
  if (body.byteLength < threshold) return res;
1976
2060
  let compressed;
1977
- let encoding;
1978
- if (useBrotli) {
1979
- compressed = brotliCompressSync(body, {
1980
- params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) }
1981
- });
1982
- encoding = "br";
1983
- } else if (useGzip) {
2061
+ let enc;
2062
+ if (encoding === "br") {
2063
+ compressed = brotliCompressSync(body, { params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) } });
2064
+ enc = "br";
2065
+ } else if (encoding === "gzip") {
1984
2066
  compressed = gzipSync(body, { level: Math.min(level, 9) });
1985
- encoding = "gzip";
2067
+ enc = "gzip";
1986
2068
  } else {
1987
2069
  compressed = deflateSync(body, { level: Math.min(level, 9) });
1988
- encoding = "deflate";
2070
+ enc = "deflate";
1989
2071
  }
1990
2072
  const headers = new Headers(res.headers);
1991
- headers.set("Content-Encoding", encoding);
2073
+ headers.set("Content-Encoding", enc);
1992
2074
  headers.set("Content-Length", String(compressed.byteLength));
1993
2075
  headers.delete("Content-Range");
1994
2076
  const existingVary = headers.get("Vary");
@@ -2155,7 +2237,7 @@ async function executeQuery(schema, params, options, req, ctx) {
2155
2237
  variableValues: params.variables,
2156
2238
  operationName: params.operationName
2157
2239
  });
2158
- return Response.json(result, { status: result.errors ? 200 : 200 });
2240
+ return Response.json(result, { status: result.errors ? 400 : 200 });
2159
2241
  }
2160
2242
  function graphiqlHTML(endpoint) {
2161
2243
  const safeEndpoint = endpoint.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\x3C");
@@ -3596,8 +3678,7 @@ function redis(opts) {
3596
3678
  const options = typeof opts === "string" ? { url: opts } : opts ?? {};
3597
3679
  const url = options.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
3598
3680
  const client = new IORedis(url, options);
3599
- client.on("error", () => {
3600
- });
3681
+ client.on("error", (err) => console.error("[redis]", err.message));
3601
3682
  const mw = ((req, ctx, next) => {
3602
3683
  ctx.redis = client;
3603
3684
  return next(req, ctx);
@@ -3610,7 +3691,8 @@ function redis(opts) {
3610
3691
  // hub.ts
3611
3692
  function createHub(opts) {
3612
3693
  const prefix = opts?.prefix ?? "hub:";
3613
- const channels2 = /* @__PURE__ */ new Map();
3694
+ const channels = /* @__PURE__ */ new Map();
3695
+ const wsKeys = /* @__PURE__ */ new Map();
3614
3696
  let redisPub;
3615
3697
  let redisSub = null;
3616
3698
  if (opts?.redis) {
@@ -3619,7 +3701,7 @@ function createHub(opts) {
3619
3701
  redisSub.on("message", (rawChannel, rawData) => {
3620
3702
  if (!rawChannel.startsWith(prefix)) return;
3621
3703
  const key = rawChannel.slice(prefix.length);
3622
- const members = channels2.get(key);
3704
+ const members = channels.get(key);
3623
3705
  if (!members) return;
3624
3706
  for (const ws of members) {
3625
3707
  try {
@@ -3630,20 +3712,30 @@ function createHub(opts) {
3630
3712
  });
3631
3713
  }
3632
3714
  function join5(key, ws) {
3633
- if (!channels2.has(key)) {
3634
- channels2.set(key, /* @__PURE__ */ new Set());
3715
+ if (!channels.has(key)) {
3716
+ channels.set(key, /* @__PURE__ */ new Set());
3635
3717
  redisSub?.subscribe(`${prefix}${key}`);
3636
3718
  }
3637
- channels2.get(key).add(ws);
3719
+ channels.get(key).add(ws);
3720
+ let keys = wsKeys.get(ws);
3721
+ if (!keys) {
3722
+ keys = /* @__PURE__ */ new Set();
3723
+ wsKeys.set(ws, keys);
3724
+ }
3725
+ keys.add(key);
3638
3726
  }
3639
3727
  function leave(ws) {
3640
- for (const [, members] of channels2) {
3641
- members.delete(ws);
3728
+ const keys = wsKeys.get(ws);
3729
+ if (keys) {
3730
+ for (const key of keys) {
3731
+ channels.get(key)?.delete(ws);
3732
+ }
3733
+ wsKeys.delete(ws);
3642
3734
  }
3643
3735
  }
3644
3736
  function broadcast(key, data) {
3645
3737
  const msg = JSON.stringify(data);
3646
- const members = channels2.get(key);
3738
+ const members = channels.get(key);
3647
3739
  if (members) {
3648
3740
  for (const ws of members) {
3649
3741
  try {
@@ -3655,7 +3747,7 @@ function createHub(opts) {
3655
3747
  redisPub?.publish(`${prefix}${key}`, msg);
3656
3748
  }
3657
3749
  async function close() {
3658
- channels2.clear();
3750
+ channels.clear();
3659
3751
  if (redisSub) {
3660
3752
  await redisSub.quit();
3661
3753
  }
@@ -3725,17 +3817,40 @@ function queue(opts) {
3725
3817
  const handlers = /* @__PURE__ */ new Map();
3726
3818
  let running = false;
3727
3819
  let pollTimer = null;
3820
+ let epoch = 0;
3728
3821
  const jobKey = `${prefix}:jobs`;
3729
3822
  const mw = ((req, ctx, next) => {
3730
3823
  ctx.queue = q;
3731
3824
  return next(req, ctx);
3732
3825
  });
3733
3826
  const q = mw;
3827
+ const MAX_CONCURRENT = 16;
3828
+ let inflight = 0;
3829
+ async function processJob(job, jobHandler) {
3830
+ inflight++;
3831
+ try {
3832
+ await jobHandler(job);
3833
+ } catch (e) {
3834
+ console.error("[queue] handler error:", e.message);
3835
+ } finally {
3836
+ inflight--;
3837
+ }
3838
+ if (job.schedule) {
3839
+ try {
3840
+ const nextRun = cronNext(job.schedule);
3841
+ const nextJob = { ...job, id: crypto4.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3842
+ await redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob));
3843
+ } catch (e) {
3844
+ console.error("[queue] cron re-queue failed:", e.message);
3845
+ }
3846
+ }
3847
+ }
3734
3848
  async function poll() {
3849
+ const currentEpoch = epoch;
3735
3850
  if (!running) return;
3736
3851
  try {
3737
3852
  const now = Date.now();
3738
- while (true) {
3853
+ while (running && inflight < MAX_CONCURRENT) {
3739
3854
  const result = await redis2.zpopmin(jobKey);
3740
3855
  if (result.length < 2) break;
3741
3856
  const raw = result[0];
@@ -3750,26 +3865,15 @@ function queue(opts) {
3750
3865
  } catch {
3751
3866
  continue;
3752
3867
  }
3753
- const handler = handlers.get(job.type);
3754
- if (handler) {
3755
- handler(job).then(() => {
3756
- if (job.schedule) {
3757
- try {
3758
- const nextRun = cronNext(job.schedule);
3759
- const nextJob = { ...job, id: crypto4.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3760
- redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob)).catch(() => {
3761
- });
3762
- } catch {
3763
- }
3764
- }
3765
- }).catch((e) => {
3766
- console.error("[queue] handler error:", e);
3767
- });
3868
+ const jobHandler = handlers.get(job.type);
3869
+ if (jobHandler) {
3870
+ processJob(job, jobHandler);
3768
3871
  }
3769
3872
  }
3770
- } catch {
3873
+ } catch (e) {
3874
+ console.error("[queue] poll error:", e.message);
3771
3875
  }
3772
- if (running) {
3876
+ if (running && currentEpoch === epoch) {
3773
3877
  pollTimer = setTimeout(poll, pollInterval);
3774
3878
  }
3775
3879
  }
@@ -3797,6 +3901,7 @@ function queue(opts) {
3797
3901
  };
3798
3902
  mw.stop = function stop() {
3799
3903
  running = false;
3904
+ epoch++;
3800
3905
  if (pollTimer) {
3801
3906
  clearTimeout(pollTimer);
3802
3907
  pollTimer = null;
@@ -3804,6 +3909,7 @@ function queue(opts) {
3804
3909
  };
3805
3910
  mw.close = async function close() {
3806
3911
  mw.stop();
3912
+ while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
3807
3913
  redis2.disconnect();
3808
3914
  };
3809
3915
  return q;
@@ -4375,9 +4481,8 @@ function graphqlType(field, required) {
4375
4481
  function inputGraphqlType(field) {
4376
4482
  return graphqlType(field, false);
4377
4483
  }
4378
- var typeCache = /* @__PURE__ */ new Map();
4379
4484
  function buildObjectType(table, ctx) {
4380
- const cached = typeCache.get(table.id);
4485
+ const cached = ctx.typeCache.get(table.id);
4381
4486
  if (cached) return cached;
4382
4487
  const typeName = pascalCase(table.slug);
4383
4488
  const fieldsThunk = () => {
@@ -4453,7 +4558,7 @@ function buildObjectType(table, ctx) {
4453
4558
  return fields;
4454
4559
  };
4455
4560
  const type = new GraphQLObjectType({ name: typeName, fields: fieldsThunk });
4456
- typeCache.set(table.id, type);
4561
+ ctx.typeCache.set(table.id, type);
4457
4562
  return type;
4458
4563
  }
4459
4564
  function buildInputType(table, prefix) {
@@ -4569,7 +4674,7 @@ function buildGraphQLHandler(sql2) {
4569
4674
  WHERE tenant_id = ${ctx.tenant.id}
4570
4675
  ORDER BY created_at ASC
4571
4676
  `;
4572
- const buildCtx = { sql: sql2, tenantId: ctx.tenant.id, tables };
4677
+ const buildCtx = { sql: sql2, tenantId: ctx.tenant.id, tables, typeCache: /* @__PURE__ */ new Map() };
4573
4678
  const schema = new GraphQLSchema({
4574
4679
  query: new GraphQLObjectType({
4575
4680
  name: "Query",
@@ -4609,7 +4714,7 @@ function buildGraphQLHandler(sql2) {
4609
4714
  WHERE tenant_id = ${_ctx2.tenant.id}
4610
4715
  ORDER BY created_at ASC
4611
4716
  `;
4612
- const buildCtx = { sql: sql2, tenantId: _ctx2.tenant.id, tables };
4717
+ const buildCtx = { sql: sql2, tenantId: _ctx2.tenant.id, tables, typeCache: /* @__PURE__ */ new Map() };
4613
4718
  const schema = new GraphQLSchema({
4614
4719
  query: new GraphQLObjectType({
4615
4720
  name: "Query",
@@ -4957,16 +5062,12 @@ function agent(options) {
4957
5062
  const model = options.model;
4958
5063
  const embeddingModel = options.embeddingModel;
4959
5064
  const dimension = options.embeddingDimension ?? 1024;
4960
- let defaultModels = null;
5065
+ const defaultModels = !model || !embeddingModel ? createModelsFromEnv() : null;
4961
5066
  function getModel() {
4962
- if (model) return model;
4963
- if (!defaultModels) defaultModels = createModelsFromEnv();
4964
- return defaultModels.model;
5067
+ return model ?? defaultModels.model;
4965
5068
  }
4966
5069
  function getEmbeddingModel() {
4967
- if (embeddingModel) return embeddingModel;
4968
- if (!defaultModels) defaultModels = createModelsFromEnv();
4969
- return defaultModels.embeddingModel;
5070
+ return embeddingModel ?? defaultModels.embeddingModel;
4970
5071
  }
4971
5072
  const agentsTable = pg.table("_agents", {
4972
5073
  id: serial("id").primaryKey(),
@@ -5006,127 +5107,151 @@ function agent(options) {
5006
5107
  };
5007
5108
  }
5008
5109
 
5110
+ // messager/agent.ts
5111
+ async function runAgentRouting(sql2, messages2, agents, hub, channelId, content) {
5112
+ if (!agents) return;
5113
+ const agentMembers = await sql2`
5114
+ SELECT member_id FROM "_channel_members"
5115
+ WHERE channel_id = ${channelId} AND member_type = 'agent'
5116
+ `;
5117
+ for (const am of agentMembers) {
5118
+ agents.run(am.member_id, { input: content, stream: false }).then((result) => {
5119
+ if ("output" in result && result.output) {
5120
+ messages2.insert({
5121
+ channel_id: channelId,
5122
+ sender_id: am.member_id,
5123
+ sender_type: "agent",
5124
+ content: result.output
5125
+ }).then((r) => {
5126
+ hub.broadcast(`messager:${channelId}`, { type: "message", data: r });
5127
+ }).catch((e) => {
5128
+ console.error("[messager] agent reply insert failed:", e);
5129
+ });
5130
+ }
5131
+ }).catch((e) => {
5132
+ console.error("[messager] agent run failed:", e);
5133
+ });
5134
+ }
5135
+ }
5136
+
5009
5137
  // messager/ws.ts
5010
- var userConnections = /* @__PURE__ */ new Map();
5011
- var hub;
5012
- function broadcastToChannel(channelId, data) {
5138
+ function broadcastToChannel(hub, channelId, data) {
5013
5139
  hub?.broadcast(`messager:${channelId}`, data);
5014
5140
  }
5015
5141
  function createWSHandler(deps) {
5016
5142
  const { sql: sql2, agents } = deps;
5017
- hub = createHub({
5143
+ const hub = createHub({
5018
5144
  redis: deps.redis,
5019
5145
  prefix: "messager:"
5020
5146
  });
5147
+ const userConnections = /* @__PURE__ */ new Map();
5148
+ function trackConnection(userId, ws) {
5149
+ let conns = userConnections.get(userId);
5150
+ if (!conns) {
5151
+ conns = /* @__PURE__ */ new Set();
5152
+ userConnections.set(userId, conns);
5153
+ }
5154
+ conns.add(ws);
5155
+ }
5156
+ function untrackConnection(ws) {
5157
+ for (const [userId, conns] of userConnections) {
5158
+ conns.delete(ws);
5159
+ if (conns.size === 0) userConnections.delete(userId);
5160
+ }
5161
+ }
5021
5162
  return {
5022
- open(ws, ctx) {
5023
- const userId = ctx.user?.id;
5024
- if (!userId) {
5025
- ws.close(4001, "Unauthorized");
5026
- return;
5027
- }
5028
- },
5029
- async message(ws, ctx, data) {
5030
- const userId = ctx.user?.id;
5031
- if (!userId) return;
5032
- let msg;
5033
- try {
5034
- msg = JSON.parse(data.toString());
5035
- } catch {
5036
- ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" }));
5037
- return;
5038
- }
5039
- const { type, channel_id, content, is_typing, last_message_id } = msg;
5040
- switch (type) {
5041
- case "message": {
5042
- if (!content || !channel_id) return;
5043
- const [row] = await sql2`
5044
- INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "content")
5045
- VALUES (${channel_id}, ${userId}, 'user', ${content})
5046
- RETURNING *
5047
- `;
5048
- const message = row;
5049
- hub.join(`messager:${channel_id}`, ws);
5050
- if (!userConnections.has(userId)) userConnections.set(userId, /* @__PURE__ */ new Set());
5051
- userConnections.get(userId).add(ws);
5052
- broadcastToChannel(channel_id, { type: "message", data: message });
5053
- if (agents) {
5054
- const agentMembers = await sql2`
5055
- SELECT member_id FROM "_channel_members"
5056
- WHERE channel_id = ${channel_id} AND member_type = 'agent'
5163
+ handler: {
5164
+ open(ws, ctx) {
5165
+ const userId = ctx.user?.id;
5166
+ if (!userId) {
5167
+ ws.close(4001, "Unauthorized");
5168
+ return;
5169
+ }
5170
+ },
5171
+ async message(ws, ctx, data) {
5172
+ const userId = ctx.user?.id;
5173
+ if (!userId) return;
5174
+ let msg;
5175
+ try {
5176
+ msg = JSON.parse(data.toString());
5177
+ } catch {
5178
+ ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" }));
5179
+ return;
5180
+ }
5181
+ const { type, channel_id, content, is_typing, last_message_id } = msg;
5182
+ switch (type) {
5183
+ case "message": {
5184
+ if (!content || !channel_id) return;
5185
+ const [row] = await sql2`
5186
+ INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "content")
5187
+ VALUES (${channel_id}, ${userId}, 'user', ${content})
5188
+ RETURNING *
5057
5189
  `;
5058
- for (const am of agentMembers) {
5059
- agents.run(am.member_id, { input: content, stream: false }).then((result) => {
5060
- if ("output" in result && result.output) {
5061
- sql2`
5062
- INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "content")
5063
- VALUES (${channel_id}, ${am.member_id}, 'agent', ${result.output})
5064
- `.then(([r]) => {
5065
- broadcastToChannel(channel_id, { type: "message", data: r });
5066
- }).catch((e) => {
5067
- console.error("[messager] agent reply insert failed:", e);
5068
- });
5069
- }
5070
- }).catch((e) => {
5071
- console.error("[messager] agent run failed:", e);
5072
- });
5190
+ const message = row;
5191
+ hub.join(`messager:${channel_id}`, ws);
5192
+ trackConnection(userId, ws);
5193
+ broadcastToChannel(hub, channel_id, { type: "message", data: message });
5194
+ if (agents) {
5195
+ const insertMsg = (data2) => sql2`
5196
+ INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "content")
5197
+ VALUES (${data2.channel_id}, ${data2.sender_id}, ${data2.sender_type}, ${data2.content})
5198
+ RETURNING *
5199
+ `.then(([r]) => r);
5200
+ runAgentRouting(sql2, { insert: insertMsg }, agents, hub, channel_id, content);
5073
5201
  }
5202
+ break;
5074
5203
  }
5075
- break;
5076
- }
5077
- case "typing": {
5078
- if (channel_id) {
5204
+ case "typing": {
5205
+ if (channel_id) {
5206
+ hub.join(`messager:${channel_id}`, ws);
5207
+ }
5208
+ broadcastToChannel(hub, channel_id, {
5209
+ type: "typing",
5210
+ channel_id,
5211
+ user_id: userId,
5212
+ is_typing: is_typing ?? false
5213
+ });
5214
+ break;
5215
+ }
5216
+ case "read": {
5217
+ if (!channel_id || !last_message_id) return;
5079
5218
  hub.join(`messager:${channel_id}`, ws);
5219
+ await sql2`
5220
+ UPDATE "_channel_members"
5221
+ SET last_read_id = ${last_message_id}, last_read_at = NOW()
5222
+ WHERE channel_id = ${channel_id} AND member_id = ${userId} AND member_type = 'user'
5223
+ `;
5224
+ broadcastToChannel(hub, channel_id, {
5225
+ type: "read",
5226
+ channel_id,
5227
+ user_id: userId,
5228
+ last_message_id
5229
+ });
5230
+ break;
5080
5231
  }
5081
- broadcastToChannel(channel_id, {
5082
- type: "typing",
5083
- channel_id,
5084
- user_id: userId,
5085
- is_typing: is_typing ?? false
5086
- });
5087
- break;
5088
5232
  }
5089
- case "read": {
5090
- if (!channel_id || !last_message_id) return;
5091
- hub.join(`messager:${channel_id}`, ws);
5092
- await sql2`
5093
- UPDATE "_channel_members"
5094
- SET last_read_id = ${last_message_id}, last_read_at = NOW()
5095
- WHERE channel_id = ${channel_id} AND member_id = ${userId} AND member_type = 'user'
5096
- `;
5097
- broadcastToChannel(channel_id, {
5098
- type: "read",
5099
- channel_id,
5100
- user_id: userId,
5101
- last_message_id
5102
- });
5103
- break;
5104
- }
5105
- }
5106
- },
5107
- close(ws) {
5108
- hub?.leave(ws);
5109
- for (const [, conns] of userConnections) {
5110
- conns.delete(ws);
5233
+ },
5234
+ close(ws) {
5235
+ hub.leave(ws);
5236
+ untrackConnection(ws);
5237
+ },
5238
+ error(ws) {
5239
+ hub.leave(ws);
5240
+ untrackConnection(ws);
5111
5241
  }
5112
5242
  },
5113
- error(ws) {
5114
- hub?.leave(ws);
5115
- for (const [, conns] of userConnections) {
5116
- conns.delete(ws);
5117
- }
5118
- }
5243
+ hub
5119
5244
  };
5120
5245
  }
5121
5246
 
5122
5247
  // messager/rest.ts
5123
5248
  function buildRouter3(deps) {
5124
- const { sql: sql2, channels: channels2, members, messages: messages2, agents } = deps;
5249
+ const { sql: sql2, channels, members, messages: messages2, agents, hub } = deps;
5125
5250
  const r = new Router();
5126
5251
  r.post("/channels", async (req) => {
5127
5252
  const body = await req.json();
5128
5253
  if (!body.name) return Response.json({ error: "name is required" }, { status: 400 });
5129
- const channel = await channels2.insert({
5254
+ const channel = await channels.insert({
5130
5255
  name: body.name,
5131
5256
  type: body.type || "channel",
5132
5257
  created_by: body.created_by || 1
@@ -5169,14 +5294,14 @@ function buildRouter3(deps) {
5169
5294
  });
5170
5295
  r.get("/channels/:id", async (_req, ctx) => {
5171
5296
  const id2 = parseInt(ctx.params.id, 10);
5172
- const ch = await channels2.read(id2);
5297
+ const ch = await channels.read(id2);
5173
5298
  if (!ch) return Response.json({ error: "Channel not found" }, { status: 404 });
5174
5299
  const { data: memberRows } = await members.readMany({ channel_id: id2 });
5175
5300
  return Response.json({ channel: ch, members: memberRows });
5176
5301
  });
5177
5302
  r.delete("/channels/:id", async (_req, ctx) => {
5178
5303
  const id2 = parseInt(ctx.params.id, 10);
5179
- await channels2.delete(id2);
5304
+ await channels.delete(id2);
5180
5305
  return Response.json({ ok: true });
5181
5306
  });
5182
5307
  r.post("/channels/:id/members", async (req, ctx) => {
@@ -5228,31 +5353,8 @@ function buildRouter3(deps) {
5228
5353
  type: body.type || "text",
5229
5354
  content: body.content
5230
5355
  });
5231
- broadcastToChannel(channelId, { type: "message", data: msg });
5232
- if (agents) {
5233
- const agentMembers = await sql2`
5234
- SELECT member_id FROM "_channel_members"
5235
- WHERE channel_id = ${channelId} AND member_type = 'agent'
5236
- `;
5237
- for (const am of agentMembers) {
5238
- agents.run(am.member_id, { input: body.content, stream: false }).then((result) => {
5239
- if ("output" in result && result.output) {
5240
- messages2.insert({
5241
- channel_id: channelId,
5242
- sender_id: am.member_id,
5243
- sender_type: "agent",
5244
- content: result.output
5245
- }).then((r2) => {
5246
- broadcastToChannel(channelId, { type: "message", data: r2 });
5247
- }).catch((e) => {
5248
- console.error("[messager] agent reply insert failed:", e);
5249
- });
5250
- }
5251
- }).catch((e) => {
5252
- console.error("[messager] agent run failed:", e);
5253
- });
5254
- }
5255
- }
5356
+ broadcastToChannel(hub, channelId, { type: "message", data: msg });
5357
+ runAgentRouting(sql2, messages2, agents, hub, channelId, body.content);
5256
5358
  return Response.json(msg, { status: 201 });
5257
5359
  });
5258
5360
  r.post("/channels/:id/read", async (req, ctx) => {
@@ -5279,7 +5381,9 @@ function messager(options) {
5279
5381
  const agents = options.agents;
5280
5382
  const redis2 = options.redis;
5281
5383
  const base = new PgModule(pg);
5282
- const channels2 = pg.table("_channels", {
5384
+ const wsResult = createWSHandler({ sql: sql2, agents, redis: redis2 });
5385
+ const hub = wsResult.hub;
5386
+ const channels = pg.table("_channels", {
5283
5387
  id: serial("id").primaryKey(),
5284
5388
  tenant_id: text("tenant_id"),
5285
5389
  name: text("name").notNull().default(""),
@@ -5311,16 +5415,16 @@ function messager(options) {
5311
5415
  });
5312
5416
  return {
5313
5417
  migrate: async () => {
5314
- await channels2.create();
5315
- await channels2.createIndex("tenant_id");
5418
+ await channels.create();
5419
+ await channels.createIndex("tenant_id");
5316
5420
  await members.create();
5317
5421
  await members.createIndex("member_id");
5318
5422
  await members.createIndex(["channel_id", "member_id", "member_type"], { unique: true });
5319
5423
  await messages2.create();
5320
5424
  await messages2.createIndex(["channel_id", "created_at"], { desc: true });
5321
5425
  },
5322
- router: () => buildRouter3({ sql: sql2, channels: channels2, members, messages: messages2, agents }),
5323
- wsHandler: () => createWSHandler({ sql: sql2, agents, redis: redis2 }),
5426
+ router: () => buildRouter3({ sql: sql2, channels, members, messages: messages2, agents, hub }),
5427
+ wsHandler: () => wsResult.handler,
5324
5428
  async send(channelId, content, opts) {
5325
5429
  const msg = await messages2.insert({
5326
5430
  channel_id: channelId,
@@ -5329,7 +5433,7 @@ function messager(options) {
5329
5433
  type: opts?.type ?? "text",
5330
5434
  content
5331
5435
  });
5332
- broadcastToChannel(channelId, { type: "message", data: msg });
5436
+ broadcastToChannel(hub, channelId, { type: "message", data: msg });
5333
5437
  return msg;
5334
5438
  },
5335
5439
  close: () => base.close()
@@ -5429,6 +5533,56 @@ function createGateway(config, getPort) {
5429
5533
 
5430
5534
  // deploy/manager.ts
5431
5535
  import crypto5 from "node:crypto";
5536
+
5537
+ // deploy/process.ts
5538
+ import { fork } from "node:child_process";
5539
+ function forkApp(opts) {
5540
+ const child = fork(opts.entry, [], {
5541
+ cwd: opts.cwd,
5542
+ env: {
5543
+ ...process.env,
5544
+ ...opts.env,
5545
+ PORT: String(opts.port)
5546
+ },
5547
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
5548
+ });
5549
+ child.stdout?.on("data", (chunk) => {
5550
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
5551
+ opts.onLog?.(line);
5552
+ }
5553
+ });
5554
+ child.stderr?.on("data", (chunk) => {
5555
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
5556
+ opts.onLog?.(`[error] ${line}`);
5557
+ }
5558
+ });
5559
+ return { child, port: opts.port };
5560
+ }
5561
+ function stopProcess(mp, timeout = 1e4) {
5562
+ return new Promise((resolve11) => {
5563
+ const timer = setTimeout(() => {
5564
+ mp.child.kill("SIGKILL");
5565
+ resolve11();
5566
+ }, timeout);
5567
+ mp.child.on("exit", () => {
5568
+ clearTimeout(timer);
5569
+ resolve11();
5570
+ });
5571
+ mp.child.kill("SIGTERM");
5572
+ });
5573
+ }
5574
+ async function healthCheck(port, path2 = "/") {
5575
+ try {
5576
+ const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
5577
+ signal: AbortSignal.timeout(5e3)
5578
+ });
5579
+ return res.ok;
5580
+ } catch {
5581
+ return false;
5582
+ }
5583
+ }
5584
+
5585
+ // deploy/manager.ts
5432
5586
  function createManager(config, apps, manager) {
5433
5587
  const router = new Router();
5434
5588
  const auth2 = (req, ctx, next) => {
@@ -5477,7 +5631,7 @@ function createManager(config, apps, manager) {
5477
5631
  const app = apps.get(ctx.params.name);
5478
5632
  if (!app) return new Response("Not Found", { status: 404 });
5479
5633
  if (app.process) {
5480
- app.process.kill("SIGTERM");
5634
+ await stopProcess({ child: app.process, port: app.currentPort });
5481
5635
  app.process = null;
5482
5636
  }
5483
5637
  app.status = { ...app.status, status: "stopped", pid: void 0 };
@@ -5567,54 +5721,6 @@ function createManager(config, apps, manager) {
5567
5721
  return router;
5568
5722
  }
5569
5723
 
5570
- // deploy/process.ts
5571
- import { fork } from "node:child_process";
5572
- function forkApp(opts) {
5573
- const child = fork(opts.entry, [], {
5574
- cwd: opts.cwd,
5575
- env: {
5576
- ...process.env,
5577
- ...opts.env,
5578
- PORT: String(opts.port)
5579
- },
5580
- stdio: ["pipe", "pipe", "pipe", "ipc"]
5581
- });
5582
- child.stdout?.on("data", (chunk) => {
5583
- for (const line of chunk.toString().split("\n").filter(Boolean)) {
5584
- opts.onLog?.(line);
5585
- }
5586
- });
5587
- child.stderr?.on("data", (chunk) => {
5588
- for (const line of chunk.toString().split("\n").filter(Boolean)) {
5589
- opts.onLog?.(`[error] ${line}`);
5590
- }
5591
- });
5592
- return { child, port: opts.port };
5593
- }
5594
- function stopProcess(mp, timeout = 1e4) {
5595
- return new Promise((resolve11) => {
5596
- const timer = setTimeout(() => {
5597
- mp.child.kill("SIGKILL");
5598
- resolve11();
5599
- }, timeout);
5600
- mp.child.on("exit", () => {
5601
- clearTimeout(timer);
5602
- resolve11();
5603
- });
5604
- mp.child.kill("SIGTERM");
5605
- });
5606
- }
5607
- async function healthCheck(port, path2 = "/") {
5608
- try {
5609
- const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
5610
- signal: AbortSignal.timeout(5e3)
5611
- });
5612
- return res.ok;
5613
- } catch {
5614
- return false;
5615
- }
5616
- }
5617
-
5618
5724
  // deploy/config.ts
5619
5725
  function defineConfig(config) {
5620
5726
  if (!config.domain) throw new Error("deploy: domain is required");
@@ -6254,7 +6360,7 @@ function createEditTool(ctx) {
6254
6360
  // opencode/tools/grep.ts
6255
6361
  import { tool as tool7 } from "ai";
6256
6362
  import { z as z9 } from "zod";
6257
- import { execSync as execSync2 } from "node:child_process";
6363
+ import { execFileSync } from "node:child_process";
6258
6364
  import { resolve as resolve7 } from "node:path";
6259
6365
  import { existsSync as existsSync3 } from "node:fs";
6260
6366
  function createGrepTool(ctx) {
@@ -6268,15 +6374,21 @@ function createGrepTool(ctx) {
6268
6374
  }),
6269
6375
  execute: async ({ pattern, include, path: path2, context }) => {
6270
6376
  const searchDir = path2 ? resolve7(ctx.workspace, path2) : ctx.workspace;
6271
- const contextArg = context > 0 ? `-C ${context}` : "";
6272
- let cmd;
6273
- if (existsSync3("/usr/bin/rg") || existsSync3("/usr/local/bin/rg")) {
6274
- cmd = `rg -n ${contextArg} ${include ? `-g '${include}'` : ""} '${pattern.replace(/'/g, "'\\''")}' '${searchDir}'`;
6275
- } else {
6276
- cmd = `grep -rn ${contextArg} ${include ? `--include='${include}'` : ""} '${pattern.replace(/'/g, "'\\''")}' '${searchDir}'`;
6277
- }
6278
6377
  try {
6279
- const stdout = execSync2(cmd, { timeout: 15e3, maxBuffer: 1024 * 1024 }).toString();
6378
+ let stdout;
6379
+ if (existsSync3("/usr/bin/rg") || existsSync3("/usr/local/bin/rg")) {
6380
+ const args = ["-n"];
6381
+ if (context > 0) args.push("-C", String(context));
6382
+ if (include) args.push("-g", include);
6383
+ args.push(pattern, searchDir);
6384
+ stdout = execFileSync("rg", args, { timeout: 15e3, maxBuffer: 1024 * 1024 }).toString();
6385
+ } else {
6386
+ const args = ["-rn"];
6387
+ if (context > 0) args.push("-C", String(context));
6388
+ if (include) args.push("--include", include);
6389
+ args.push(pattern, searchDir);
6390
+ stdout = execFileSync("grep", args, { timeout: 15e3, maxBuffer: 1024 * 1024 }).toString();
6391
+ }
6280
6392
  const lines = stdout.split("\n").filter(Boolean);
6281
6393
  return { matches: lines.length, results: lines.slice(0, 200), truncated: lines.length > 200 };
6282
6394
  } catch (e) {
@@ -6292,7 +6404,7 @@ function createGrepTool(ctx) {
6292
6404
  // opencode/tools/glob.ts
6293
6405
  import { tool as tool8 } from "ai";
6294
6406
  import { z as z10 } from "zod";
6295
- import { execSync as execSync3 } from "node:child_process";
6407
+ import { execFileSync as execFileSync2 } from "node:child_process";
6296
6408
  import { resolve as resolve8 } from "node:path";
6297
6409
  function createGlobTool(ctx) {
6298
6410
  return tool8({
@@ -6304,12 +6416,19 @@ function createGlobTool(ctx) {
6304
6416
  execute: async ({ pattern, path: path2 }) => {
6305
6417
  const searchDir = path2 ? resolve8(ctx.workspace, path2) : ctx.workspace;
6306
6418
  try {
6307
- const stdout = execSync3(`find '${searchDir}' -name '${pattern.replace(/'/g, "'\\''")}' -not -path '*/node_modules/*' 2>/dev/null | head -200`, {
6419
+ const stdout = execFileSync2("find", [
6420
+ searchDir,
6421
+ "-name",
6422
+ pattern,
6423
+ "-not",
6424
+ "-path",
6425
+ "*/node_modules/*"
6426
+ ], {
6308
6427
  timeout: 1e4,
6309
6428
  maxBuffer: 1024 * 1024
6310
6429
  }).toString();
6311
- const files = stdout.split("\n").filter(Boolean);
6312
- return { files, total: files.length, truncated: files.length >= 200 };
6430
+ const lines = stdout.split("\n").filter(Boolean).slice(0, 200);
6431
+ return { files: lines, total: lines.length, truncated: stdout.split("\n").filter(Boolean).length > 200 };
6313
6432
  } catch {
6314
6433
  return { files: [], total: 0, truncated: false };
6315
6434
  }
@@ -6799,10 +6918,20 @@ function health(options) {
6799
6918
 
6800
6919
  // analytics.ts
6801
6920
  var DEFAULT_EXCLUDED = ["/__analytics", "/__wfw", "/static"];
6921
+ var MAX_MEM_ENTRIES = 1e4;
6802
6922
  var MemStore = class {
6803
6923
  days = /* @__PURE__ */ new Map();
6804
6924
  pages = /* @__PURE__ */ new Map();
6805
6925
  refs = /* @__PURE__ */ new Map();
6926
+ evict() {
6927
+ if (this.days.size <= MAX_MEM_ENTRIES) return;
6928
+ const sorted = [...this.days.keys()].sort();
6929
+ const toDelete = sorted.slice(0, this.days.size - MAX_MEM_ENTRIES);
6930
+ for (const d of toDelete) {
6931
+ this.days.delete(d);
6932
+ this.refs.delete(d);
6933
+ }
6934
+ }
6806
6935
  record(path2, date, refDomain, mobile) {
6807
6936
  let day = this.days.get(date);
6808
6937
  if (!day) {
@@ -6827,6 +6956,7 @@ var MemStore = class {
6827
6956
  }
6828
6957
  refs.set(refDomain, (refs.get(refDomain) || 0) + 1);
6829
6958
  }
6959
+ this.evict();
6830
6960
  }
6831
6961
  query(days) {
6832
6962
  const since = /* @__PURE__ */ new Date();
@@ -7062,8 +7192,12 @@ function preferences(options) {
7062
7192
  const localeOpts = { ...defaults.locale, ...options.locale };
7063
7193
  const themeOpts = { ...defaults.theme, ...options.theme };
7064
7194
  const cache = /* @__PURE__ */ new Map();
7195
+ function validLocale(locale) {
7196
+ return /^[\w-]+$/.test(locale) && !locale.includes("..");
7197
+ }
7065
7198
  async function load(locale) {
7066
7199
  if (!dir) return {};
7200
+ if (!validLocale(locale)) return {};
7067
7201
  const cached = cache.get(locale);
7068
7202
  if (cached) return cached;
7069
7203
  const filePath = join4(dir, `${locale}.json`);
@@ -7093,7 +7227,7 @@ function preferences(options) {
7093
7227
  if (dir) {
7094
7228
  const msgs = await load(locale);
7095
7229
  ctx.t = (key, params, fallback) => translate(msgs, key, params, fallback);
7096
- globalThis.__LOCALE_DATA__ = msgs;
7230
+ ctx.parsed = { ...ctx.parsed, __localeData: msgs };
7097
7231
  }
7098
7232
  ctx.setPref = (name, value) => {
7099
7233
  const cookieOpts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "SameSite=Lax"];
@@ -7311,7 +7445,7 @@ function mailer(options) {
7311
7445
  await transporter.sendMail({ ...opts, from: opts.from ?? from });
7312
7446
  }
7313
7447
  async function close() {
7314
- if (transporter) transporter.close();
7448
+ transporter?.close();
7315
7449
  }
7316
7450
  return { send, close };
7317
7451
  }
@@ -7345,11 +7479,11 @@ function csrf(options) {
7345
7479
  return res;
7346
7480
  }
7347
7481
  const cookieToken = getCookies(req)[cookieName];
7348
- let headerToken = req.headers.get(headerName);
7349
- if (!headerToken) {
7482
+ let headerToken = req.headers.get(headerName) ?? req.headers.get("x-xsrf-token") ?? "";
7483
+ if (!headerToken && (req.method === "POST" || req.method === "PUT" || req.method === "PATCH" || req.method === "DELETE")) {
7350
7484
  try {
7351
7485
  const body = await req.clone().json();
7352
- headerToken = body[bodyKey];
7486
+ headerToken = body[bodyKey] ?? "";
7353
7487
  } catch {
7354
7488
  }
7355
7489
  }
@@ -7361,6 +7495,15 @@ function csrf(options) {
7361
7495
  }
7362
7496
 
7363
7497
  // logdb/rest.ts
7498
+ function parseMetadata(row) {
7499
+ if (typeof row.metadata === "string") {
7500
+ try {
7501
+ row.metadata = JSON.parse(row.metadata);
7502
+ } catch {
7503
+ }
7504
+ }
7505
+ return row;
7506
+ }
7364
7507
  function createHandler(entries) {
7365
7508
  return async (req, ctx) => {
7366
7509
  const body = await req.json();
@@ -7377,13 +7520,7 @@ function createHandler(entries) {
7377
7520
  message: body.message,
7378
7521
  metadata
7379
7522
  });
7380
- if (typeof row.metadata === "string") {
7381
- try {
7382
- row.metadata = JSON.parse(row.metadata);
7383
- } catch {
7384
- }
7385
- }
7386
- return Response.json(row, { status: 201 });
7523
+ return Response.json(parseMetadata(row), { status: 201 });
7387
7524
  };
7388
7525
  }
7389
7526
  function listHandler(entries) {
@@ -7409,15 +7546,7 @@ function listHandler(entries) {
7409
7546
  conditions.length > 0 ? conditions : void 0,
7410
7547
  { orderBy: { created_at: "desc" }, limit, offset }
7411
7548
  );
7412
- for (const row of data) {
7413
- if (typeof row.metadata === "string") {
7414
- try {
7415
- row.metadata = JSON.parse(row.metadata);
7416
- } catch {
7417
- }
7418
- }
7419
- }
7420
- return Response.json({ entries: data, total: count });
7549
+ return Response.json({ entries: data.map(parseMetadata), total: count });
7421
7550
  };
7422
7551
  }
7423
7552
  function getHandler(entries) {
@@ -7426,13 +7555,7 @@ function getHandler(entries) {
7426
7555
  if (!id2) return Response.json({ error: "id is required" }, { status: 400 });
7427
7556
  const row = await entries.read(parseInt(id2));
7428
7557
  if (!row) return Response.json({ error: "not found" }, { status: 404 });
7429
- if (typeof row.metadata === "string") {
7430
- try {
7431
- row.metadata = JSON.parse(row.metadata);
7432
- } catch {
7433
- }
7434
- }
7435
- return Response.json(row);
7558
+ return Response.json(parseMetadata(row));
7436
7559
  };
7437
7560
  }
7438
7561
 
@@ -7522,7 +7645,8 @@ function logdb(options) {
7522
7645
  await ensurePartitions(sql2, tableName);
7523
7646
  },
7524
7647
  clean,
7525
- close: () => pg.close()
7648
+ close: async () => {
7649
+ }
7526
7650
  };
7527
7651
  }
7528
7652
 
@@ -7530,8 +7654,7 @@ function logdb(options) {
7530
7654
  import crypto6 from "node:crypto";
7531
7655
 
7532
7656
  // iii/stream.ts
7533
- var channels = /* @__PURE__ */ new Map();
7534
- function notify(stream, group, item, event, data) {
7657
+ function notify(channels, stream, group, item, event, data) {
7535
7658
  const keys = [
7536
7659
  `${stream}`,
7537
7660
  `${stream}:${group}`,
@@ -7583,7 +7706,7 @@ function applyOps(value, ops) {
7583
7706
  }
7584
7707
  return current;
7585
7708
  }
7586
- function createMemoryStore() {
7709
+ function createMemoryStore(channels) {
7587
7710
  const store = /* @__PURE__ */ new Map();
7588
7711
  function key(stream, group, item) {
7589
7712
  return `${stream}:${group}:${item}`;
@@ -7593,7 +7716,7 @@ function createMemoryStore() {
7593
7716
  const k = key(stream, group, item);
7594
7717
  const old = store.get(k) ?? null;
7595
7718
  store.set(k, deepClone(data));
7596
- notify(stream, group, item, "set", data);
7719
+ notify(channels, stream, group, item, "set", data);
7597
7720
  return { old_value: old, new_value: deepClone(data) };
7598
7721
  },
7599
7722
  async get(stream, group, item) {
@@ -7604,7 +7727,7 @@ function createMemoryStore() {
7604
7727
  const k = key(stream, group, item);
7605
7728
  const old = store.get(k) ?? null;
7606
7729
  store.delete(k);
7607
- notify(stream, group, item, "delete", null);
7730
+ notify(channels, stream, group, item, "delete", null);
7608
7731
  return { old_value: old };
7609
7732
  },
7610
7733
  async list(stream, group) {
@@ -7648,19 +7771,19 @@ function createMemoryStore() {
7648
7771
  return { streams, count: streams.length };
7649
7772
  },
7650
7773
  async send(stream, group, type, data, id2) {
7651
- notify(stream, group, id2 ?? "", "send", { type, data });
7774
+ notify(channels, stream, group, id2 ?? "", "send", { type, data });
7652
7775
  },
7653
7776
  async update(stream, group, item, ops) {
7654
7777
  const k = key(stream, group, item);
7655
7778
  const old = deepClone(store.get(k) ?? null);
7656
7779
  const newVal = applyOps(old, ops);
7657
7780
  store.set(k, deepClone(newVal));
7658
- notify(stream, group, item, "update", newVal);
7781
+ notify(channels, stream, group, item, "update", newVal);
7659
7782
  return { old_value: old, new_value: deepClone(newVal) };
7660
7783
  }
7661
7784
  };
7662
7785
  }
7663
- function createPgStore(pg) {
7786
+ function createPgStore(channels, pg) {
7664
7787
  const sql2 = pg.sql;
7665
7788
  return {
7666
7789
  async set(stream, group, item, data) {
@@ -7671,7 +7794,7 @@ function createPgStore(pg) {
7671
7794
  DO UPDATE SET data = ${data}, updated_at = NOW()
7672
7795
  RETURNING data
7673
7796
  `;
7674
- notify(stream, group, item, "set", data);
7797
+ notify(channels, stream, group, item, "set", data);
7675
7798
  return { old_value: null, new_value: data };
7676
7799
  },
7677
7800
  async get(stream, group, item) {
@@ -7691,7 +7814,7 @@ function createPgStore(pg) {
7691
7814
  RETURNING data
7692
7815
  `;
7693
7816
  const old = rows[0]?.data ?? null;
7694
- notify(stream, group, item, "delete", null);
7817
+ notify(channels, stream, group, item, "delete", null);
7695
7818
  return { old_value: old };
7696
7819
  },
7697
7820
  async list(stream, group) {
@@ -7729,7 +7852,7 @@ function createPgStore(pg) {
7729
7852
  return { streams, count: streams.length };
7730
7853
  },
7731
7854
  async send(stream, group, type, data, id2) {
7732
- notify(stream, group, id2 ?? "", "send", { type, data });
7855
+ notify(channels, stream, group, id2 ?? "", "send", { type, data });
7733
7856
  },
7734
7857
  async update(stream, group, item, ops) {
7735
7858
  const { value: oldVal } = await this.get(stream, group, item);
@@ -7740,12 +7863,12 @@ function createPgStore(pg) {
7740
7863
  ON CONFLICT (stream_name, group_id, item_id)
7741
7864
  DO UPDATE SET data = ${newVal}, updated_at = NOW()
7742
7865
  `;
7743
- notify(stream, group, item, "update", newVal);
7866
+ notify(channels, stream, group, item, "update", newVal);
7744
7867
  return { old_value: oldVal, new_value: deepClone(newVal) };
7745
7868
  }
7746
7869
  };
7747
7870
  }
7748
- function createRedisStore(redis2, ttl) {
7871
+ function createRedisStore(channels, redis2, ttl) {
7749
7872
  function hashKey(stream, group) {
7750
7873
  return `iii:stream:${stream}:${group}`;
7751
7874
  }
@@ -7760,7 +7883,7 @@ function createRedisStore(redis2, ttl) {
7760
7883
  await redis2.hset(hk, item, JSON.stringify(data));
7761
7884
  setTTL(hk);
7762
7885
  await redis2.publish(`iii:stream:${stream}`, JSON.stringify({ event: "set", group, item, data }));
7763
- notify(stream, group, item, "set", data);
7886
+ notify(channels, stream, group, item, "set", data);
7764
7887
  return { old_value: old, new_value: deepClone(data) };
7765
7888
  },
7766
7889
  async get(stream, group, item) {
@@ -7775,7 +7898,7 @@ function createRedisStore(redis2, ttl) {
7775
7898
  const remaining = await redis2.hlen(hk);
7776
7899
  if (remaining === 0) await redis2.del(hk);
7777
7900
  await redis2.publish(`iii:stream:${stream}`, JSON.stringify({ event: "delete", group, item }));
7778
- notify(stream, group, item, "delete", null);
7901
+ notify(channels, stream, group, item, "delete", null);
7779
7902
  return { old_value: old };
7780
7903
  },
7781
7904
  async list(stream, group) {
@@ -7826,7 +7949,7 @@ function createRedisStore(redis2, ttl) {
7826
7949
  return { streams, count: streams.length };
7827
7950
  },
7828
7951
  async send(stream, group, type, data, id2) {
7829
- notify(stream, group, id2 ?? "", "send", { type, data });
7952
+ notify(channels, stream, group, id2 ?? "", "send", { type, data });
7830
7953
  },
7831
7954
  async update(stream, group, item, ops) {
7832
7955
  const hk = hashKey(stream, group);
@@ -7836,13 +7959,14 @@ function createRedisStore(redis2, ttl) {
7836
7959
  await redis2.hset(hk, item, JSON.stringify(newVal));
7837
7960
  setTTL(hk);
7838
7961
  await redis2.publish(`iii:stream:${stream}`, JSON.stringify({ event: "update", group, item, data: newVal }));
7839
- notify(stream, group, item, "update", newVal);
7962
+ notify(channels, stream, group, item, "update", newVal);
7840
7963
  return { old_value: old, new_value: deepClone(newVal) };
7841
7964
  }
7842
7965
  };
7843
7966
  }
7844
7967
  function createStream(opts) {
7845
- const store = opts?.pg ? createPgStore(opts.pg) : opts?.redis ? createRedisStore(opts.redis, opts.streamTTL ?? 3600) : createMemoryStore();
7968
+ const channels = /* @__PURE__ */ new Map();
7969
+ const store = opts?.pg ? createPgStore(channels, opts.pg) : opts?.redis ? createRedisStore(channels, opts.redis, opts.streamTTL ?? 3600) : createMemoryStore(channels);
7846
7970
  let redisSub = null;
7847
7971
  if (opts?.redis) {
7848
7972
  redisSub = opts.redis.duplicate();
@@ -7852,9 +7976,9 @@ function createStream(opts) {
7852
7976
  try {
7853
7977
  const msg = JSON.parse(rawData);
7854
7978
  if (msg.event === "set" || msg.event === "update") {
7855
- notify(stream, msg.group, msg.item, msg.event, msg.data);
7979
+ notify(channels, stream, msg.group, msg.item, msg.event, msg.data);
7856
7980
  } else if (msg.event === "delete") {
7857
- notify(stream, msg.group, msg.item, "delete", null);
7981
+ notify(channels, stream, msg.group, msg.item, "delete", null);
7858
7982
  }
7859
7983
  } catch {
7860
7984
  }
@@ -7888,6 +8012,9 @@ function createStream(opts) {
7888
8012
  `;
7889
8013
  await sql2`CREATE INDEX IF NOT EXISTS idx_iii_stream_group ON "_iii_stream" (stream_name, group_id)`;
7890
8014
  }
8015
+ },
8016
+ async close() {
8017
+ if (redisSub) await redisSub.quit();
7891
8018
  }
7892
8019
  };
7893
8020
  }
@@ -8207,60 +8334,12 @@ function iii(opts = {}) {
8207
8334
  });
8208
8335
  }
8209
8336
  });
8210
- function doTrigger(request) {
8211
- const fn = functions.get(request.function_id);
8212
- if (!fn) throw new Error(`Function "${request.function_id}" not found`);
8213
- const ctx = { engine: module, functionId: request.function_id, workerName: fn.workerName };
8214
- if (request.action === "void") {
8215
- queueMicrotask(() => fn.handler(request.payload, ctx));
8216
- return Promise.resolve(void 0);
8217
- }
8218
- return Promise.resolve(fn.handler(request.payload, ctx));
8219
- }
8220
- const router = buildRouter5({
8221
- listWorkers: () => Array.from(workers.values()).map((w) => ({
8222
- id: w.id,
8223
- name: w.name,
8224
- status: "connected",
8225
- connectedAt: Date.now(),
8226
- functionCount: w.functions.length,
8227
- triggerCount: w.triggers.length
8228
- })),
8229
- listFunctions: () => Array.from(functions.values()).map((f) => ({
8230
- id: f.id,
8231
- workerId: f.workerId,
8232
- workerName: f.workerName,
8233
- triggers: f.triggers
8234
- })),
8235
- listTriggers: () => Array.from(triggers.values()).map((t) => ({
8236
- id: t.id,
8237
- type: t.type,
8238
- function_id: t.function_id,
8239
- config: t.config,
8240
- workerId: t.workerId
8241
- })),
8242
- trigger: doTrigger,
8243
- addWorker: addLocalWorker,
8244
- removeWorker: (worker) => {
8245
- for (const [wid, reg] of workers) {
8246
- if (reg.name === worker.name) {
8247
- removeWorker(wid);
8248
- return;
8249
- }
8250
- }
8251
- },
8252
- shutdown: async () => {
8253
- for (const [, reg] of workers) reg.ws?.close();
8254
- workers.clear();
8255
- functions.clear();
8256
- triggers.clear();
8257
- },
8258
- migrate: async () => {
8259
- await stream.migrate();
8260
- }
8261
- }, wsHandler);
8262
8337
  const module = {
8263
- router: () => router,
8338
+ router: () => {
8339
+ const r = buildRouter5(module, wsHandler);
8340
+ module.router = () => r;
8341
+ return r;
8342
+ },
8264
8343
  wsHandler: () => wsHandler,
8265
8344
  addWorker: addLocalWorker,
8266
8345
  removeWorker: (worker) => {
@@ -8271,7 +8350,16 @@ function iii(opts = {}) {
8271
8350
  }
8272
8351
  }
8273
8352
  },
8274
- trigger: doTrigger,
8353
+ trigger(request) {
8354
+ const fn = functions.get(request.function_id);
8355
+ if (!fn) throw new Error(`Function "${request.function_id}" not found`);
8356
+ const ctx = { engine: module, functionId: request.function_id, workerName: fn.workerName };
8357
+ if (request.action === "void") {
8358
+ queueMicrotask(() => fn.handler(request.payload, ctx));
8359
+ return Promise.resolve(void 0);
8360
+ }
8361
+ return Promise.resolve(fn.handler(request.payload, ctx));
8362
+ },
8275
8363
  listWorkers: () => Array.from(workers.values()).map((w) => ({
8276
8364
  id: w.id,
8277
8365
  name: w.name,
@@ -8297,10 +8385,16 @@ function iii(opts = {}) {
8297
8385
  await stream.migrate();
8298
8386
  },
8299
8387
  shutdown: async () => {
8388
+ for (const [, p] of pending) {
8389
+ clearTimeout(p.timer);
8390
+ p.reject(new Error("Engine shutting down"));
8391
+ }
8392
+ pending.clear();
8300
8393
  for (const [, reg] of workers) reg.ws?.close();
8301
8394
  workers.clear();
8302
8395
  functions.clear();
8303
8396
  triggers.clear();
8397
+ await stream.close();
8304
8398
  }
8305
8399
  };
8306
8400
  return module;
@@ -8467,7 +8561,16 @@ function registerWorker(url) {
8467
8561
  send({ type: "register_trigger", function_id: input.function_id, trigger_type: input.type, config: input.config });
8468
8562
  },
8469
8563
  unregisterTrigger(functionId) {
8470
- registeredTriggers.clear();
8564
+ for (const key of registeredTriggers) {
8565
+ try {
8566
+ const parsed = JSON.parse(key);
8567
+ if (parsed.function_id === functionId) {
8568
+ registeredTriggers.delete(key);
8569
+ break;
8570
+ }
8571
+ } catch {
8572
+ }
8573
+ }
8471
8574
  send({ type: "unregister_trigger", function_id: functionId });
8472
8575
  },
8473
8576
  trigger(request) {
@@ -8531,6 +8634,7 @@ export {
8531
8634
  generateObject,
8532
8635
  generateText2 as generateText,
8533
8636
  getCookies,
8637
+ getCtx,
8534
8638
  graphql,
8535
8639
  health,
8536
8640
  helmet,