fix(build): make oRPC/tRPC dist runnable under node ESM (deploy crashloop)
flake.nix only rewrote @/ aliases but left extensionless relative imports (./router) in compiled dist/. node dist/index.js (how prod runs) cannot resolve extensionless ESM specifiers -> ERR_MODULE_NOT_FOUND -> backend crashlooped (444 restarts, port 4001 dead). Extract the fixer into a shared scripts/fix-imports.mjs that appends .js to extensionless relative imports and rewrites @/ aliases, and wire it into backend + discord-gateway build phases. Verified: fresh tsc + fixer -> node dist/index.js boots; oRPC over /trpc serves both HTTP POST and WebSocket (config/dashboard/voice/moderation/ media/chatbot/analysis) end-to-end against Postgres + Redis. next build passes with the oRPC client + partysocket.
This commit is contained in:
@@ -101,31 +101,8 @@
|
|||||||
buildPhase = pnpmInstall + ''
|
buildPhase = pnpmInstall + ''
|
||||||
echo "=== Compiling TypeScript ==="
|
echo "=== Compiling TypeScript ==="
|
||||||
npx tsc 2>&1
|
npx tsc 2>&1
|
||||||
echo "=== Fixing @/ path aliases to relative paths ==="
|
echo "=== Fixing @/ path aliases + extensionless relative imports for node ESM ==="
|
||||||
node -e "
|
node scripts/fix-imports.mjs
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
let count = 0;
|
|
||||||
function walk(dir) {
|
|
||||||
if (!fs.existsSync(dir)) return;
|
|
||||||
for (const e of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
||||||
const p = path.join(dir, e.name);
|
|
||||||
if (e.isDirectory()) walk(p);
|
|
||||||
else if (e.name.endsWith('.js')) {
|
|
||||||
const c = fs.readFileSync(p, 'utf8');
|
|
||||||
const pat = /from\s+['\"]@\/([^'\"]+)['\"]/g;
|
|
||||||
const n = c.replace(pat, (m, p1) => {
|
|
||||||
const target = path.join('dist', p1) + '.js';
|
|
||||||
const rel = path.relative(path.dirname(p), target);
|
|
||||||
return 'from \"' + (rel.startsWith('.') ? rel : './' + rel) + '\"';
|
|
||||||
});
|
|
||||||
if (n !== c) { fs.writeFileSync(p, n); count++; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk('dist');
|
|
||||||
console.log('Fixed ' + count + ' files');
|
|
||||||
"
|
|
||||||
echo "=== Build complete ==="
|
echo "=== Build complete ==="
|
||||||
'' + pruneProd;
|
'' + pruneProd;
|
||||||
|
|
||||||
@@ -202,31 +179,8 @@ WRAPPER
|
|||||||
pnpm rebuild @discordjs/opus 2>&1 || true
|
pnpm rebuild @discordjs/opus 2>&1 || true
|
||||||
echo "=== Compiling TypeScript ===="
|
echo "=== Compiling TypeScript ===="
|
||||||
npx tsc 2>&1
|
npx tsc 2>&1
|
||||||
echo "=== Fixing @/ path aliases to relative paths ==="
|
echo "=== Fixing @/ path aliases + extensionless relative imports for node ESM ==="
|
||||||
node -e "
|
node scripts/fix-imports.mjs
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
let count = 0;
|
|
||||||
function walk(dir) {
|
|
||||||
if (!fs.existsSync(dir)) return;
|
|
||||||
for (const e of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
||||||
const p = path.join(dir, e.name);
|
|
||||||
if (e.isDirectory()) walk(p);
|
|
||||||
else if (e.name.endsWith('.js')) {
|
|
||||||
const c = fs.readFileSync(p, 'utf8');
|
|
||||||
const pat = /from\s+['\"]@\/([^'\"]+)['\"]/g;
|
|
||||||
const n = c.replace(pat, (m, p1) => {
|
|
||||||
const target = path.join('dist', p1) + '.js';
|
|
||||||
const rel = path.relative(path.dirname(p), target);
|
|
||||||
return 'from \"' + (rel.startsWith('.') ? rel : './' + rel) + '\"';
|
|
||||||
});
|
|
||||||
if (n !== c) { fs.writeFileSync(p, n); count++; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk('dist');
|
|
||||||
console.log('Fixed ' + count + ' files');
|
|
||||||
"
|
|
||||||
echo "=== Build complete ==="
|
echo "=== Build complete ==="
|
||||||
'' + pruneProd;
|
'' + pruneProd;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/voice": "^0.19.2",
|
"@discordjs/voice": "^0.19.2",
|
||||||
"@trpc/server": "^11.18.0",
|
"@orpc/server": "1.15.0",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
|||||||
Generated
+176
-13
@@ -11,9 +11,9 @@ importers:
|
|||||||
'@discordjs/voice':
|
'@discordjs/voice':
|
||||||
specifier: ^0.19.2
|
specifier: ^0.19.2
|
||||||
version: 0.19.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)
|
version: 0.19.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)
|
||||||
'@trpc/server':
|
'@orpc/server':
|
||||||
specifier: ^11.18.0
|
specifier: 1.15.0
|
||||||
version: 11.18.0(typescript@5.9.3)
|
version: 1.15.0(@opentelemetry/api@1.9.1)(ws@8.21.1)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.16.1
|
specifier: ^1.16.1
|
||||||
version: 1.19.0(debug@4.4.3)
|
version: 1.19.0(debug@4.4.3)
|
||||||
@@ -318,6 +318,57 @@ packages:
|
|||||||
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
|
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
'@orpc/client@1.15.0':
|
||||||
|
resolution: {integrity: sha512-Qt0FdPSGySdQwy83iUWOq+Iqhw2gM9qHtyxfBbcw1mwOz1s4O2BwUFA3ymVTLIRRNYRgPrQLaBZhp8CacqeePg==}
|
||||||
|
|
||||||
|
'@orpc/contract@1.15.0':
|
||||||
|
resolution: {integrity: sha512-zo8+x+5iqnIMrFdcj+ibVgn6dJneWzLNcD3kZQxrJFGFNAFoxvZbwryR5wOdcHkaSCeS3N7Ib2NydU6pihSYmg==}
|
||||||
|
|
||||||
|
'@orpc/interop@1.15.0':
|
||||||
|
resolution: {integrity: sha512-Ah48o/rc00rN1yP5HcaoVsDCjdUhoh3JP1VibBbvwijhGvdOwRwiw5RWiGu442GSKcrPt9etAGhWXH88q8C3ww==}
|
||||||
|
|
||||||
|
'@orpc/server@1.15.0':
|
||||||
|
resolution: {integrity: sha512-l9FnL1MjQ8g77mB10/8D61okuKHLJBWhuwmd6OF65POHzBF177kMCcbHvRFEqUuxSbzP2eNiZGf3kXPMHmIKiQ==}
|
||||||
|
peerDependencies:
|
||||||
|
crossws: '>=0.3.4'
|
||||||
|
ws: '>=8.18.1'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
crossws:
|
||||||
|
optional: true
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@orpc/shared@1.15.0':
|
||||||
|
resolution: {integrity: sha512-A3/JE7pQYSrrRm6/WYJxV3GBhpMdJRPM3h47slQtWBUZe9Sao5En5WBc4tISGQNP2emcez6qIvDziSgD2T+img==}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.9.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@opentelemetry/api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@orpc/standard-server-aws-lambda@1.15.0':
|
||||||
|
resolution: {integrity: sha512-laFW7C4t1gEV0NIt4EjCVCBU3kFPnj2FObJLQkTH+DkEOzxVy6zyJ1coIncDSeOPxzbf7rG8cs0B2vQQyuROag==}
|
||||||
|
|
||||||
|
'@orpc/standard-server-fastify@1.15.0':
|
||||||
|
resolution: {integrity: sha512-fcOl2004o3RnZxW7rhLuDCrCKSi/H2eWnyljeeoY+xykcygRf0ONCs2K2bSdxPm6cmvQGAXa9m8VBglG9oERVQ==}
|
||||||
|
peerDependencies:
|
||||||
|
fastify: '>=5.6.1'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
fastify:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@orpc/standard-server-fetch@1.15.0':
|
||||||
|
resolution: {integrity: sha512-XYVfgmIt71YrPJSI7RKRiNWjcyktPYHpevkjeohGNBP5aSMAUL95quZArAWr+XOPp4U56XRao54I6wFMZa4How==}
|
||||||
|
|
||||||
|
'@orpc/standard-server-node@1.15.0':
|
||||||
|
resolution: {integrity: sha512-3ye9SIIhYfJckQwyyGQQRHAEF5vW6QDbax7oi1mOQ9UBHaRDH5kcL28mT0JnETrUvgBoDRx+rfCsQaAOB4n3zw==}
|
||||||
|
|
||||||
|
'@orpc/standard-server-peer@1.15.0':
|
||||||
|
resolution: {integrity: sha512-fsTN+FrdPkseVx99yRenGJYSp0xOrEk9fODk8oh3zzricNYzlZ8GGgCYtDNgt9vFIR/J05dc+Nk76KhcPBQpLw==}
|
||||||
|
|
||||||
|
'@orpc/standard-server@1.15.0':
|
||||||
|
resolution: {integrity: sha512-bje/xn6thDqJY/JQ7xoOjmD1KWE4FsyZOPgnPxwqTZLx/r00stRhLuFvk1hDEGj9UlG7NLZEijDVx0wqvcyDzA==}
|
||||||
|
|
||||||
'@oxc-project/types@0.142.0':
|
'@oxc-project/types@0.142.0':
|
||||||
resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==}
|
resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==}
|
||||||
|
|
||||||
@@ -515,12 +566,6 @@ packages:
|
|||||||
'@standard-schema/spec@1.1.0':
|
'@standard-schema/spec@1.1.0':
|
||||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
'@trpc/server@11.18.0':
|
|
||||||
resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=5.7.2'
|
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.3':
|
'@tybys/wasm-util@0.10.3':
|
||||||
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
||||||
|
|
||||||
@@ -674,6 +719,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
cookie@1.1.1:
|
||||||
|
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -1083,6 +1132,9 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
openapi-types@12.1.3:
|
||||||
|
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||||
|
|
||||||
parseurl@1.3.3:
|
parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1203,6 +1255,10 @@ packages:
|
|||||||
quick-format-unescaped@4.0.4:
|
quick-format-unescaped@4.0.4:
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
|
radash@12.1.1:
|
||||||
|
resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==}
|
||||||
|
engines: {node: '>=14.18.0'}
|
||||||
|
|
||||||
range-parser@1.3.0:
|
range-parser@1.3.0:
|
||||||
resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
|
resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -1293,6 +1349,10 @@ packages:
|
|||||||
std-env@4.2.0:
|
std-env@4.2.0:
|
||||||
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
|
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
|
||||||
|
|
||||||
|
tagged-tag@1.0.0:
|
||||||
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
tdigest@0.1.2:
|
tdigest@0.1.2:
|
||||||
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
||||||
|
|
||||||
@@ -1326,6 +1386,10 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
type-fest@5.8.0:
|
||||||
|
resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
type-is@2.1.0:
|
type-is@2.1.0:
|
||||||
resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
|
resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@@ -1619,6 +1683,97 @@ snapshots:
|
|||||||
|
|
||||||
'@opentelemetry/api@1.9.1': {}
|
'@opentelemetry/api@1.9.1': {}
|
||||||
|
|
||||||
|
'@orpc/client@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-fetch': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-peer': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/contract@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/client': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
openapi-types: 12.1.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/interop@1.15.0': {}
|
||||||
|
|
||||||
|
'@orpc/server@1.15.0(@opentelemetry/api@1.9.1)(ws@8.21.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/client': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/contract': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/interop': 1.15.0
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-aws-lambda': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-fastify': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-fetch': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-node': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-peer': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
cookie: 1.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
ws: 8.21.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
- fastify
|
||||||
|
|
||||||
|
'@orpc/shared@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
radash: 12.1.1
|
||||||
|
type-fest: 5.8.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
|
||||||
|
'@orpc/standard-server-aws-lambda@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-fetch': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-node': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/standard-server-fastify@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-node': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/standard-server-fetch@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/standard-server-node@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server-fetch': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/standard-server-peer@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@orpc/standard-server': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/standard-server@1.15.0(@opentelemetry/api@1.9.1)':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0(@opentelemetry/api@1.9.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
'@oxc-project/types@0.142.0': {}
|
'@oxc-project/types@0.142.0': {}
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@pinojs/redact@0.4.0': {}
|
||||||
@@ -1743,10 +1898,6 @@ snapshots:
|
|||||||
|
|
||||||
'@standard-schema/spec@1.1.0': {}
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@trpc/server@11.18.0(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
typescript: 5.9.3
|
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.3':
|
'@tybys/wasm-util@0.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -1928,6 +2079,8 @@ snapshots:
|
|||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
cookie@1.1.1: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -2245,6 +2398,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
openapi-types@12.1.3: {}
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
path-to-regexp@8.4.2: {}
|
path-to-regexp@8.4.2: {}
|
||||||
@@ -2349,6 +2504,8 @@ snapshots:
|
|||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
|
radash@12.1.1: {}
|
||||||
|
|
||||||
range-parser@1.3.0: {}
|
range-parser@1.3.0: {}
|
||||||
|
|
||||||
raw-body@3.0.2:
|
raw-body@3.0.2:
|
||||||
@@ -2474,6 +2631,8 @@ snapshots:
|
|||||||
|
|
||||||
std-env@4.2.0: {}
|
std-env@4.2.0: {}
|
||||||
|
|
||||||
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
tdigest@0.1.2:
|
tdigest@0.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bintrees: 1.0.2
|
bintrees: 1.0.2
|
||||||
@@ -2503,6 +2662,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
type-fest@5.8.0:
|
||||||
|
dependencies:
|
||||||
|
tagged-tag: 1.0.0
|
||||||
|
|
||||||
type-is@2.1.0:
|
type-is@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 2.0.0
|
content-type: 2.0.0
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Rewrite import specifiers in the compiled dist/ so the output runs under
|
||||||
|
// plain `node dist/index.js` (native ESM, no bundler / no tsx).
|
||||||
|
//
|
||||||
|
// Background: tsconfig uses moduleResolution:"bundler", so `tsc` emits BARE
|
||||||
|
// relative specifiers WITHOUT extensions (e.g. `import "./router"`) and leaves
|
||||||
|
// the `@/*` path-alias imports untouched. Node's native ESM resolver rejects
|
||||||
|
// extensionless relative specifiers and knows nothing about the `@/` alias, so
|
||||||
|
// the emitted dist/ crashes at startup (`ERR_MODULE_NOT_FOUND`). This script
|
||||||
|
// fixes both:
|
||||||
|
// 1. `@/foo` -> relative path to dist/foo.js
|
||||||
|
// 2. `./foo` / `../foo` -> `./foo.js` / `../foo.js` (append .js)
|
||||||
|
// Already-extensioned relative imports (.js/.json/.node/.mjs/.cjs) and bare
|
||||||
|
// package specifiers are left untouched (idempotent).
|
||||||
|
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
||||||
|
import { join, relative, dirname } from "node:path";
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
function walk(dir) {
|
||||||
|
if (!existsSync(dir)) return;
|
||||||
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const p = join(dir, e.name);
|
||||||
|
if (e.isDirectory()) walk(p);
|
||||||
|
else if (e.name.endsWith(".js")) {
|
||||||
|
const c = readFileSync(p, "utf8");
|
||||||
|
const pat = /from\s+['"]([^'"]+)['"]/g;
|
||||||
|
const n = c.replace(pat, (m, spec) => {
|
||||||
|
if (spec.startsWith("@/")) {
|
||||||
|
const target = join("dist", spec.slice(2)) + ".js";
|
||||||
|
let rel = relative(dirname(p), target);
|
||||||
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
||||||
|
return `from "${rel}"`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(spec.startsWith("./") || spec.startsWith("../")) &&
|
||||||
|
!/\.(js|json|node|mjs|cjs)$/.test(spec)
|
||||||
|
) {
|
||||||
|
return `from "${spec}.js"`;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
if (n !== c) {
|
||||||
|
writeFileSync(p, n);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk("dist");
|
||||||
|
console.log(`Fixed ${count} import specifiers in dist/`);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { nodeHTTPRequestHandler } from "@trpc/server/adapters/node-http";
|
import { RPCHandler } from "@orpc/server/node";
|
||||||
|
import { onError } from "@orpc/server";
|
||||||
import express, {
|
import express, {
|
||||||
type Express,
|
type Express,
|
||||||
type NextFunction,
|
type NextFunction,
|
||||||
@@ -9,13 +10,13 @@ import helmet from "helmet";
|
|||||||
import { createChildLogger } from "@/shared/logger/index";
|
import { createChildLogger } from "@/shared/logger/index";
|
||||||
import { createHealthRouter } from "../modules/health/index.js";
|
import { createHealthRouter } from "../modules/health/index.js";
|
||||||
import { errorHandler } from "../shared/middlewares/index.js";
|
import { errorHandler } from "../shared/middlewares/index.js";
|
||||||
import { appRouter } from "../trpc/routers";
|
import { appRouter } from "../orpc/router";
|
||||||
|
|
||||||
// Auth removed — dashboard is public.
|
// Auth removed — dashboard is public.
|
||||||
// All data APIs (dashboard, messages, moderation, media, voice, recordings,
|
// All data APIs (dashboard, messages, moderation, media, voice, recordings,
|
||||||
// analysis, chatbot, config, ui-state) now flow over tRPC, served on TWO
|
// analysis, chatbot, config, ui-state) now flow over oRPC, served on TWO
|
||||||
// transports sharing the /trpc path:
|
// transports sharing the /trpc path:
|
||||||
// - WebSocket (browser live RPCs) — see trpc/ws.ts
|
// - WebSocket (browser live RPCs) — see orpc/ws.ts
|
||||||
// - HTTP POST (server-side / RSC fetch) — handled below
|
// - HTTP POST (server-side / RSC fetch) — handled below
|
||||||
// Only infra endpoints (health, prometheus metrics) remain plain HTTP.
|
// Only infra endpoints (health, prometheus metrics) remain plain HTTP.
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ export function createHttpApp(): Express {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Body parsing (still needed for any JSON POST; tRPC is WS-based)
|
// Body parsing (still needed for any JSON POST; oRPC is WS/HTTP-based)
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
@@ -60,29 +61,27 @@ export function createHttpApp(): Express {
|
|||||||
// Infra-only HTTP endpoints
|
// Infra-only HTTP endpoints
|
||||||
app.use("/api", createHealthRouter());
|
app.use("/api", createHealthRouter());
|
||||||
|
|
||||||
// tRPC over HTTP (server-side / RSC fetch). The context has no WebSocket
|
// oRPC over HTTP (server-side / RSC fetch). The same appRouter the browser
|
||||||
// here (that's the WS transport's job); procedures don't read ctx.conn, so
|
// reaches over the /trpc WebSocket. oRPC's node RPCHandler writes the full
|
||||||
// a null conn is safe.
|
// response itself; if no procedure matched we fall through to the 404 below.
|
||||||
// NOTE: Express 5 (path-to-regexp v8) rejects the `"/trpc/*"` wildcard route,
|
const orpcHandler = new RPCHandler(appRouter, {
|
||||||
// and `nodeHTTPRequestHandler` uses `opts.path` as the literal procedure
|
interceptors: [onError((error) => logger.error({ error }, "oRPC error"))],
|
||||||
// path (it does NOT derive it from `req.url`). So we mount a plain
|
});
|
||||||
// middleware and compute the procedure path from the URL ourselves.
|
|
||||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
if (!req.path.startsWith("/trpc")) {
|
if (!req.path.startsWith("/trpc")) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const procPath = req.url.replace(/^\/trpc\/?/, "").split("?")[0] || "/";
|
orpcHandler
|
||||||
nodeHTTPRequestHandler({
|
.handle(req, res, { prefix: "/trpc", context: {} })
|
||||||
router: appRouter,
|
.then(({ matched }) => {
|
||||||
createContext: () => ({ conn: null }),
|
if (!matched) next();
|
||||||
req,
|
})
|
||||||
res,
|
.catch((err: unknown) => {
|
||||||
path: procPath,
|
logger.error({ err }, "oRPC HTTP handler failed");
|
||||||
}).catch((err: unknown) => {
|
if (!res.headersSent) res.status(500).json({ error: "INTERNAL" });
|
||||||
logger.error({ err }, "tRPC HTTP handler failed");
|
});
|
||||||
if (!res.headersSent) res.status(500).json({ error: "INTERNAL" });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 404 handler
|
// 404 handler
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createServer, type Server } from "node:http";
|
|||||||
import { createChildLogger } from "@/shared/logger/index";
|
import { createChildLogger } from "@/shared/logger/index";
|
||||||
import { config } from "../shared/config/index.js";
|
import { config } from "../shared/config/index.js";
|
||||||
import { initializeDatabase } from "../shared/database/index.js";
|
import { initializeDatabase } from "../shared/database/index.js";
|
||||||
import { createTRPCWebSocketServer } from "../trpc/ws.js";
|
import { createORPCWebSocketServer } from "../orpc/ws.js";
|
||||||
import { startRedisBridge } from "../ws/redis-bridge.js";
|
import { startRedisBridge } from "../ws/redis-bridge.js";
|
||||||
import { createWebSocketServer } from "../ws/server.js";
|
import { createWebSocketServer } from "../ws/server.js";
|
||||||
import { createHttpApp } from "./app.js";
|
import { createHttpApp } from "./app.js";
|
||||||
@@ -19,7 +19,7 @@ export async function startHttpServer(): Promise<Server> {
|
|||||||
|
|
||||||
// Attach WebSocket servers to the same HTTP server
|
// Attach WebSocket servers to the same HTTP server
|
||||||
createWebSocketServer(server); // /ws — voice PCM + gateway events
|
createWebSocketServer(server); // /ws — voice PCM + gateway events
|
||||||
createTRPCWebSocketServer(server); // /trpc — structured data RPCs
|
createORPCWebSocketServer(server); // /trpc — structured data RPCs
|
||||||
|
|
||||||
// Start Redis pub/sub bridge to forward discord-gateway events to WS clients
|
// Start Redis pub/sub bridge to forward discord-gateway events to WS clients
|
||||||
await startRedisBridge();
|
await startRedisBridge();
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
|
import { os } from "@orpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { analysisService } from "../modules/analysis/analysis.service";
|
import { analysisService } from "../modules/analysis/analysis.service";
|
||||||
import { chatRequestSchema } from "../modules/chatbot/chatbot.schema";
|
import { chatRequestSchema } from "../modules/chatbot/chatbot.schema";
|
||||||
import { chatbotService } from "../modules/chatbot/chatbot.service";
|
import { chatbotService } from "../modules/chatbot/chatbot.service";
|
||||||
// ── Service imports ──────────────────────────────────────────────
|
// ── Service imports ──────────────────────────────────────────────
|
||||||
import { dashboardService } from "../modules/dashboard/dashboard.service";
|
import { dashboardService } from "../modules/dashboard/dashboard.service";
|
||||||
import {
|
import { mediaLoopSchema, mediaQueueSchema } from "../modules/media/media.schema";
|
||||||
mediaLoopSchema,
|
import { getStatus, queue, setLoop, skip, stop } from "../modules/media/media.service";
|
||||||
mediaQueueSchema,
|
|
||||||
} from "../modules/media/media.schema";
|
|
||||||
import {
|
|
||||||
getStatus,
|
|
||||||
queue,
|
|
||||||
setLoop,
|
|
||||||
skip,
|
|
||||||
stop,
|
|
||||||
} from "../modules/media/media.service";
|
|
||||||
import { messageQuerySchema } from "../modules/messages/messages.schema";
|
import { messageQuerySchema } from "../modules/messages/messages.schema";
|
||||||
import { messagesService } from "../modules/messages/messages.service";
|
import { messagesService } from "../modules/messages/messages.service";
|
||||||
import { moderationService } from "../modules/moderation/moderation.service";
|
import { moderationService } from "../modules/moderation/moderation.service";
|
||||||
@@ -30,17 +22,14 @@ import {
|
|||||||
} from "../modules/voice/voice.service";
|
} from "../modules/voice/voice.service";
|
||||||
import { config } from "../shared/config/index";
|
import { config } from "../shared/config/index";
|
||||||
import { publishCommandNoReply } from "../shared/redis/index";
|
import { publishCommandNoReply } from "../shared/redis/index";
|
||||||
import { logger, publicProcedure, router } from "./trpc";
|
|
||||||
|
|
||||||
// ── Dashboard ────────────────────────────────────────────────────
|
// ── Dashboard ────────────────────────────────────────────────────
|
||||||
const dashboardRouter = router({
|
const dashboardRouter = {
|
||||||
stats: publicProcedure.query(() => dashboardService.getStats()),
|
stats: os.handler(() => dashboardService.getStats()),
|
||||||
activity: publicProcedure
|
activity: os
|
||||||
.input(
|
.input(z.object({ days: z.coerce.number().int().min(1).max(90).default(14) }))
|
||||||
z.object({ days: z.coerce.number().int().min(1).max(90).default(14) }),
|
.handler(({ input }) => dashboardService.getActivity(input.days)),
|
||||||
)
|
users: os
|
||||||
.query(({ input }) => dashboardService.getActivity(input.days)),
|
|
||||||
users: publicProcedure
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.coerce.number().int().positive().default(20),
|
limit: z.coerce.number().int().positive().default(20),
|
||||||
@@ -48,17 +37,17 @@ const dashboardRouter = router({
|
|||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
dashboardService.listUsers({
|
dashboardService.listUsers({
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
cursor: input.cursor,
|
cursor: input.cursor,
|
||||||
search: input.search,
|
search: input.search,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
userDetail: publicProcedure
|
userDetail: os
|
||||||
.input(z.object({ userId: z.string() }))
|
.input(z.object({ userId: z.string() }))
|
||||||
.query(({ input }) => dashboardService.getUserDetail(input.userId)),
|
.handler(({ input }) => dashboardService.getUserDetail(input.userId)),
|
||||||
channels: publicProcedure
|
channels: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.coerce.number().int().positive().default(20),
|
limit: z.coerce.number().int().positive().default(20),
|
||||||
@@ -66,82 +55,82 @@ const dashboardRouter = router({
|
|||||||
guildId: z.string().optional(),
|
guildId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
dashboardService.listChannels({
|
dashboardService.listChannels({
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
search: input.search,
|
search: input.search,
|
||||||
guildId: input.guildId,
|
guildId: input.guildId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
channelDetail: publicProcedure
|
channelDetail: os
|
||||||
.input(z.object({ channelId: z.string() }))
|
.input(z.object({ channelId: z.string() }))
|
||||||
.query(({ input }) => dashboardService.getChannelDetail(input.channelId)),
|
.handler(({ input }) => dashboardService.getChannelDetail(input.channelId)),
|
||||||
reactions: publicProcedure
|
reactions: os
|
||||||
.input(z.object({ limit: z.coerce.number().int().positive().default(20) }))
|
.input(z.object({ limit: z.coerce.number().int().positive().default(20) }))
|
||||||
.query(({ input }) => dashboardService.getTopReactions(input.limit)),
|
.handler(({ input }) => dashboardService.getTopReactions(input.limit)),
|
||||||
reactors: publicProcedure
|
reactors: os
|
||||||
.input(z.object({ limit: z.coerce.number().int().positive().default(20) }))
|
.input(z.object({ limit: z.coerce.number().int().positive().default(20) }))
|
||||||
.query(({ input }) => dashboardService.getTopReactors(input.limit)),
|
.handler(({ input }) => dashboardService.getTopReactors(input.limit)),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Messages ─────────────────────────────────────────────────────
|
// ── Messages ─────────────────────────────────────────────────────
|
||||||
const messagesRouter = router({
|
const messagesRouter = {
|
||||||
list: publicProcedure
|
list: os
|
||||||
.input(messageQuerySchema)
|
.input(messageQuerySchema)
|
||||||
.query(({ input }) => messagesService.listMessages(input)),
|
.handler(({ input }) => messagesService.listMessages(input)),
|
||||||
byChannel: publicProcedure
|
byChannel: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
channelId: z.string(),
|
channelId: z.string(),
|
||||||
query: messageQuerySchema,
|
query: messageQuerySchema,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
messagesService.getMessagesByChannel(input.channelId, input.query),
|
messagesService.getMessagesByChannel(input.channelId, input.query),
|
||||||
),
|
),
|
||||||
detail: publicProcedure
|
detail: os
|
||||||
.input(z.object({ id: z.string() }))
|
.input(z.object({ id: z.string() }))
|
||||||
.query(({ input }) => messagesService.getMessageById(input.id)),
|
.handler(({ input }) => messagesService.getMessageById(input.id)),
|
||||||
images: publicProcedure
|
images: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
guildId: z.string(),
|
guildId: z.string(),
|
||||||
limit: z.coerce.number().int().positive().default(50),
|
limit: z.coerce.number().int().positive().default(50),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
messagesService.getImageMessages(input.guildId, input.limit),
|
messagesService.getImageMessages(input.guildId, input.limit),
|
||||||
),
|
),
|
||||||
attachmentsByChannel: publicProcedure
|
attachmentsByChannel: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
channelId: z.string(),
|
channelId: z.string(),
|
||||||
query: messageQuerySchema,
|
query: messageQuerySchema,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
messagesService.getAttachmentsByChannel(input.channelId, input.query),
|
messagesService.getAttachmentsByChannel(input.channelId, input.query),
|
||||||
),
|
),
|
||||||
review: publicProcedure
|
review: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.coerce.number().int().positive().default(20),
|
limit: z.coerce.number().int().positive().default(20),
|
||||||
channelId: z.string().optional(),
|
channelId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
const rows = await messagesService.getReviewMessages(
|
const rows = await messagesService.getReviewMessages(
|
||||||
input.channelId,
|
input.channelId,
|
||||||
input.limit,
|
input.limit,
|
||||||
);
|
);
|
||||||
return { results: rows, limit: input.limit, cursor: null };
|
return { results: rows, limit: input.limit, cursor: null };
|
||||||
}),
|
}),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Moderation ───────────────────────────────────────────────────
|
// ── Moderation ───────────────────────────────────────────────────
|
||||||
const moderationRouter = router({
|
const moderationRouter = {
|
||||||
stats: publicProcedure.query(() => moderationService.getStats()),
|
stats: os.handler(() => moderationService.getStats()),
|
||||||
actions: publicProcedure
|
actions: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.coerce.number().int().positive().default(50),
|
limit: z.coerce.number().int().positive().default(50),
|
||||||
@@ -150,7 +139,7 @@ const moderationRouter = router({
|
|||||||
cursor: z.coerce.number().int().optional(),
|
cursor: z.coerce.number().int().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
moderationService.listActions({
|
moderationService.listActions({
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
status: input.status,
|
status: input.status,
|
||||||
@@ -158,60 +147,64 @@ const moderationRouter = router({
|
|||||||
cursor: input.cursor,
|
cursor: input.cursor,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Media ────────────────────────────────────────────────────────
|
// ── Media ────────────────────────────────────────────────────────
|
||||||
const mediaRouter = router({
|
const mediaRouter = {
|
||||||
status: publicProcedure.query(() => getStatus()),
|
status: os.handler(() => getStatus()),
|
||||||
queue: publicProcedure.input(mediaQueueSchema).mutation(async ({ input }) => {
|
queue: os
|
||||||
await queue(input.source, input.mode);
|
.input(mediaQueueSchema)
|
||||||
return getStatus();
|
.handler(async ({ input }) => {
|
||||||
}),
|
await queue(input.source, input.mode);
|
||||||
skip: publicProcedure.mutation(async () => {
|
return getStatus();
|
||||||
|
}),
|
||||||
|
skip: os.handler(async () => {
|
||||||
await skip();
|
await skip();
|
||||||
return getStatus();
|
return getStatus();
|
||||||
}),
|
}),
|
||||||
stop: publicProcedure.mutation(async () => {
|
stop: os.handler(async () => {
|
||||||
await stop();
|
await stop();
|
||||||
return getStatus();
|
return getStatus();
|
||||||
}),
|
}),
|
||||||
loop: publicProcedure.input(mediaLoopSchema).mutation(async ({ input }) => {
|
loop: os
|
||||||
await setLoop(input.loop);
|
.input(mediaLoopSchema)
|
||||||
return getStatus();
|
.handler(async ({ input }) => {
|
||||||
}),
|
await setLoop(input.loop);
|
||||||
});
|
return getStatus();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
// ── Voice ─────────────────────────────────────────────────────────
|
// ── Voice ─────────────────────────────────────────────────────────
|
||||||
const voiceRouter = router({
|
const voiceRouter = {
|
||||||
guilds: publicProcedure.query(() => getGuilds()),
|
guilds: os.handler(() => getGuilds()),
|
||||||
textChannels: publicProcedure
|
textChannels: os
|
||||||
.input(z.object({ guildId: z.string() }))
|
.input(z.object({ guildId: z.string() }))
|
||||||
.query(({ input }) => getTextChannels(input.guildId)),
|
.handler(({ input }) => getTextChannels(input.guildId)),
|
||||||
voiceChannels: publicProcedure
|
voiceChannels: os
|
||||||
.input(z.object({ guildId: z.string() }))
|
.input(z.object({ guildId: z.string() }))
|
||||||
.query(({ input }) => getVoiceChannels(input.guildId)),
|
.handler(({ input }) => getVoiceChannels(input.guildId)),
|
||||||
status: publicProcedure.query(() => getVoiceStatus()),
|
status: os.handler(() => getVoiceStatus()),
|
||||||
connect: publicProcedure
|
connect: os
|
||||||
.input(z.object({ guildId: z.string(), channelId: z.string() }))
|
.input(z.object({ guildId: z.string(), channelId: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
await connectVoice(input.guildId, input.channelId);
|
await connectVoice(input.guildId, input.channelId);
|
||||||
return getVoiceStatus();
|
return getVoiceStatus();
|
||||||
}),
|
}),
|
||||||
disconnect: publicProcedure.mutation(async () => {
|
disconnect: os.handler(async () => {
|
||||||
await disconnectVoice();
|
await disconnectVoice();
|
||||||
return getVoiceStatus();
|
return getVoiceStatus();
|
||||||
}),
|
}),
|
||||||
command: publicProcedure
|
command: os
|
||||||
.input(z.object({ command: z.string().min(1) }))
|
.input(z.object({ command: z.string().min(1) }))
|
||||||
.mutation(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
await publishCommandNoReply(input.command);
|
await publishCommandNoReply(input.command);
|
||||||
return { success: true, command: input.command };
|
return { success: true, command: input.command };
|
||||||
}),
|
}),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Recordings ───────────────────────────────────────────────────
|
// ── Recordings ───────────────────────────────────────────────────
|
||||||
const recordingsRouter = router({
|
const recordingsRouter = {
|
||||||
list: publicProcedure
|
list: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.coerce.number().int().positive().default(50),
|
limit: z.coerce.number().int().positive().default(50),
|
||||||
@@ -220,24 +213,24 @@ const recordingsRouter = router({
|
|||||||
cursor: z.string().optional(),
|
cursor: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
recordingsService.getRecent(input.limit, {
|
recordingsService.getRecent(input.limit, {
|
||||||
channelId: input.channelId,
|
channelId: input.channelId,
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
cursor: input.cursor,
|
cursor: input.cursor,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
delete: publicProcedure
|
delete: os
|
||||||
.input(z.object({ id: z.string() }))
|
.input(z.object({ id: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
await recordingsService.deleteById(input.id);
|
await recordingsService.deleteById(input.id);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}),
|
}),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Analysis (search) ──────────────────────────────────────────────
|
// ── Analysis (search) ──────────────────────────────────────────────
|
||||||
const analysisRouter = router({
|
const analysisRouter = {
|
||||||
search: publicProcedure
|
search: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
q: z.string().default(""),
|
q: z.string().default(""),
|
||||||
@@ -245,18 +238,18 @@ const analysisRouter = router({
|
|||||||
limit: z.coerce.number().int().positive().default(20),
|
limit: z.coerce.number().int().positive().default(20),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(({ input }) =>
|
.handler(({ input }) =>
|
||||||
analysisService.search({
|
analysisService.search({
|
||||||
q: input.q,
|
q: input.q,
|
||||||
channelId: input.channelId,
|
channelId: input.channelId,
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Chatbot ───────────────────────────────────────────────────────
|
// ── Chatbot ───────────────────────────────────────────────────────
|
||||||
const chatbotRouter = router({
|
const chatbotRouter = {
|
||||||
chat: publicProcedure
|
chat: os
|
||||||
.input(
|
.input(
|
||||||
chatRequestSchema.extend({
|
chatRequestSchema.extend({
|
||||||
// Per-device actor id; the old REST layer used an X-User-Id header.
|
// Per-device actor id; the old REST layer used an X-User-Id header.
|
||||||
@@ -264,7 +257,7 @@ const chatbotRouter = router({
|
|||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
const userId = input.userId ?? "anonymous";
|
const userId = input.userId ?? "anonymous";
|
||||||
const response = await chatbotService.processMessage(
|
const response = await chatbotService.processMessage(
|
||||||
input.message,
|
input.message,
|
||||||
@@ -280,30 +273,30 @@ const chatbotRouter = router({
|
|||||||
});
|
});
|
||||||
return { response, timestamp: new Date().toISOString() };
|
return { response, timestamp: new Date().toISOString() };
|
||||||
}),
|
}),
|
||||||
history: publicProcedure
|
history: os
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.coerce.number().int().positive().max(100).default(50),
|
limit: z.coerce.number().int().positive().max(100).default(50),
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
const userId = input.userId ?? "anonymous";
|
const userId = input.userId ?? "anonymous";
|
||||||
const history = await chatbotService.getChatHistory(userId, input.limit);
|
const history = await chatbotService.getChatHistory(userId, input.limit);
|
||||||
return { history, total: history.length };
|
return { history, total: history.length };
|
||||||
}),
|
}),
|
||||||
clearHistory: publicProcedure
|
clearHistory: os
|
||||||
.input(z.object({ userId: z.string().optional() }))
|
.input(z.object({ userId: z.string().optional() }))
|
||||||
.mutation(async ({ input }) => {
|
.handler(async ({ input }) => {
|
||||||
const userId = input.userId ?? "anonymous";
|
const userId = input.userId ?? "anonymous";
|
||||||
await chatbotService.clearChatHistory(userId);
|
await chatbotService.clearChatHistory(userId);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}),
|
}),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Config (public dashboard config snapshot) ──────────────────────
|
// ── Config (public dashboard config snapshot) ──────────────────────
|
||||||
const configRouter = router({
|
const configRouter = {
|
||||||
get: publicProcedure.query(() => ({
|
get: os.handler(() => ({
|
||||||
monitorGuildId: config.MONITOR_GUILD_ID || null,
|
monitorGuildId: config.MONITOR_GUILD_ID || null,
|
||||||
webserverPort: config.WEBSERVER_PORT,
|
webserverPort: config.WEBSERVER_PORT,
|
||||||
nodeEnv: config.NODE_ENV,
|
nodeEnv: config.NODE_ENV,
|
||||||
@@ -318,18 +311,18 @@ const configRouter = router({
|
|||||||
voiceChannelId: config.VOICE_CHANNEL_ID || null,
|
voiceChannelId: config.VOICE_CHANNEL_ID || null,
|
||||||
logLevel: config.LOG_LEVEL,
|
logLevel: config.LOG_LEVEL,
|
||||||
})),
|
})),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── UI State ──────────────────────────────────────────────────────
|
// ── UI State ──────────────────────────────────────────────────────
|
||||||
const uiStateRouter = router({
|
const uiStateRouter = {
|
||||||
get: publicProcedure.query(() => uiStateService.getState()),
|
get: os.handler(() => uiStateService.getState()),
|
||||||
update: publicProcedure
|
update: os
|
||||||
.input(z.record(z.string(), z.unknown()))
|
.input(z.record(z.string(), z.unknown()))
|
||||||
.mutation(({ input }) => uiStateService.updateState(input)),
|
.handler(({ input }) => uiStateService.updateState(input)),
|
||||||
});
|
};
|
||||||
|
|
||||||
// ── Root router ───────────────────────────────────────────────────
|
// ── Root router ───────────────────────────────────────────────────
|
||||||
export const appRouter = router({
|
export const appRouter = {
|
||||||
dashboard: dashboardRouter,
|
dashboard: dashboardRouter,
|
||||||
messages: messagesRouter,
|
messages: messagesRouter,
|
||||||
moderation: moderationRouter,
|
moderation: moderationRouter,
|
||||||
@@ -340,8 +333,6 @@ export const appRouter = router({
|
|||||||
chatbot: chatbotRouter,
|
chatbot: chatbotRouter,
|
||||||
config: configRouter,
|
config: configRouter,
|
||||||
uiState: uiStateRouter,
|
uiState: uiStateRouter,
|
||||||
});
|
};
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
||||||
logger.info("tRPC appRouter constructed");
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { IncomingMessage, Server } from "node:http";
|
import type { IncomingMessage, Server } from "node:http";
|
||||||
import type { Duplex } from "node:stream";
|
import type { Duplex } from "node:stream";
|
||||||
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
import { RPCHandler } from "@orpc/server/ws";
|
||||||
|
import { onError } from "@orpc/server";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { createChildLogger } from "@/shared/logger/index";
|
import { createChildLogger } from "@/shared/logger/index";
|
||||||
import { appRouter } from "./routers";
|
import { appRouter } from "./router";
|
||||||
|
|
||||||
const logger = createChildLogger("trpc.ws");
|
const logger = createChildLogger("orpc.ws");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach the tRPC WebSocket handler to the shared HTTP server, on a path
|
* Attach the oRPC WebSocket handler to the shared HTTP server, on a path
|
||||||
* SEPARATE from the voice/binary WebSocket (`/ws`). All structured data RPCs
|
* SEPARATE from the voice/binary WebSocket (`/ws`). All structured data RPCs
|
||||||
* (dashboard, messages, moderation, media, voice control, recordings,
|
* (dashboard, messages, moderation, media, voice control, recordings,
|
||||||
* analysis, chatbot, config, ui-state) flow over this `/trpc` socket; the
|
* analysis, chatbot, config, ui-state) flow over this `/trpc` socket; the
|
||||||
@@ -21,27 +22,20 @@ const logger = createChildLogger("trpc.ws");
|
|||||||
* other server's path. Routing the upgrade ourselves by URL keeps `/trpc`
|
* other server's path. Routing the upgrade ourselves by URL keeps `/trpc`
|
||||||
* and `/ws` fully isolated.
|
* and `/ws` fully isolated.
|
||||||
*/
|
*/
|
||||||
export function createTRPCWebSocketServer(server: Server): WebSocketServer {
|
export function createORPCWebSocketServer(server: Server): WebSocketServer {
|
||||||
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
const handler = new RPCHandler(appRouter, {
|
||||||
|
interceptors: [onError((error) => logger.error({ error }, "oRPC WS error"))],
|
||||||
applyWSSHandler({
|
|
||||||
wss,
|
|
||||||
prefix: "/trpc",
|
|
||||||
router: appRouter,
|
|
||||||
createContext: (opts) => ({ conn: opts.res }),
|
|
||||||
keepAlive: { enabled: true, pingMs: 30_000, pongWaitMs: 10_000 },
|
|
||||||
onError: (err) => {
|
|
||||||
logger.error({ err }, "tRPC WS error");
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
||||||
|
|
||||||
server.on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => {
|
server.on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => {
|
||||||
if (!req.url?.startsWith("/trpc")) return; // let the /ws server handle it
|
if (!req.url?.startsWith("/trpc")) return; // let the /ws server handle it
|
||||||
wss.handleUpgrade(req, socket, head, (ws) => {
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||||
wss.emit("connection", ws, req);
|
handler.upgrade(ws, { context: {} });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info({ path: "/trpc" }, "tRPC WebSocket server attached");
|
logger.info({ path: "/trpc" }, "oRPC WebSocket server attached");
|
||||||
return wss;
|
return wss;
|
||||||
}
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { initTRPC } from "@trpc/server";
|
|
||||||
import type { WebSocket } from "ws";
|
|
||||||
import { createChildLogger } from "@/shared/logger/index";
|
|
||||||
|
|
||||||
const logger = createChildLogger("trpc");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* tRPC context. The WebSocket transport enriches each request with the raw
|
|
||||||
* socket so procedures can, if needed, inspect connection metadata. The
|
|
||||||
* dashboard is public (no auth), mirroring the previous REST layer.
|
|
||||||
*/
|
|
||||||
export interface TRPCContext {
|
|
||||||
conn: WebSocket | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = initTRPC.context<TRPCContext>().create({
|
|
||||||
errorFormatter({ shape, error }) {
|
|
||||||
return {
|
|
||||||
...shape,
|
|
||||||
data: {
|
|
||||||
...shape.data,
|
|
||||||
// Surface a stable code + message for client-side handling.
|
|
||||||
code: error.code,
|
|
||||||
stack: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const router = t.router;
|
|
||||||
export const publicProcedure = t.procedure;
|
|
||||||
|
|
||||||
// Re-export so routers can import z from one place if desired.
|
|
||||||
export { z } from "zod";
|
|
||||||
export { logger };
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Rewrite import specifiers in the compiled dist/ so the output runs under
|
||||||
|
// plain `node dist/index.js` (native ESM, no bundler / no tsx).
|
||||||
|
//
|
||||||
|
// Background: tsconfig uses moduleResolution:"bundler", so `tsc` emits BARE
|
||||||
|
// relative specifiers WITHOUT extensions (e.g. `import "./router"`) and leaves
|
||||||
|
// the `@/*` path-alias imports untouched. Node's native ESM resolver rejects
|
||||||
|
// extensionless relative specifiers and knows nothing about the `@/` alias, so
|
||||||
|
// the emitted dist/ crashes at startup (`ERR_MODULE_NOT_FOUND`). This script
|
||||||
|
// fixes both:
|
||||||
|
// 1. `@/foo` -> relative path to dist/foo.js
|
||||||
|
// 2. `./foo` / `../foo` -> `./foo.js` / `../foo.js` (append .js)
|
||||||
|
// Already-extensioned relative imports (.js/.json/.node/.mjs/.cjs) and bare
|
||||||
|
// package specifiers are left untouched (idempotent).
|
||||||
|
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
||||||
|
import { join, relative, dirname } from "node:path";
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
function walk(dir) {
|
||||||
|
if (!existsSync(dir)) return;
|
||||||
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const p = join(dir, e.name);
|
||||||
|
if (e.isDirectory()) walk(p);
|
||||||
|
else if (e.name.endsWith(".js")) {
|
||||||
|
const c = readFileSync(p, "utf8");
|
||||||
|
const pat = /from\s+['"]([^'"]+)['"]/g;
|
||||||
|
const n = c.replace(pat, (m, spec) => {
|
||||||
|
if (spec.startsWith("@/")) {
|
||||||
|
const target = join("dist", spec.slice(2)) + ".js";
|
||||||
|
let rel = relative(dirname(p), target);
|
||||||
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
||||||
|
return `from "${rel}"`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(spec.startsWith("./") || spec.startsWith("../")) &&
|
||||||
|
!/\.(js|json|node|mjs|cjs)$/.test(spec)
|
||||||
|
) {
|
||||||
|
return `from "${spec}.js"`;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
if (n !== c) {
|
||||||
|
writeFileSync(p, n);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk("dist");
|
||||||
|
console.log(`Fixed ${count} import specifiers in dist/`);
|
||||||
@@ -10,12 +10,13 @@
|
|||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trpc/client": "^11.18.0",
|
"@orpc/client": "1.15.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.27.0",
|
"lucide-react": "^1.27.0",
|
||||||
"motion": "^12.0.0",
|
"motion": "^12.0.0",
|
||||||
"next": "16.2.12",
|
"next": "16.2.12",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"partysocket": "1.3.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"swr": "^2.4.2",
|
"swr": "^2.4.2",
|
||||||
|
|||||||
Generated
+99
-25
@@ -8,9 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trpc/client':
|
'@orpc/client':
|
||||||
specifier: ^11.18.0
|
specifier: 1.15.0
|
||||||
version: 11.18.0(@trpc/server@11.18.0(typescript@5.9.3))(typescript@5.9.3)
|
version: 1.15.0
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -26,6 +26,9 @@ importers:
|
|||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
partysocket:
|
||||||
|
specifier: 1.3.0
|
||||||
|
version: 1.3.0(react@19.2.4)
|
||||||
react:
|
react:
|
||||||
specifier: 19.2.4
|
specifier: 19.2.4
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
@@ -433,6 +436,26 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@orpc/client@1.15.0':
|
||||||
|
resolution: {integrity: sha512-Qt0FdPSGySdQwy83iUWOq+Iqhw2gM9qHtyxfBbcw1mwOz1s4O2BwUFA3ymVTLIRRNYRgPrQLaBZhp8CacqeePg==}
|
||||||
|
|
||||||
|
'@orpc/shared@1.15.0':
|
||||||
|
resolution: {integrity: sha512-A3/JE7pQYSrrRm6/WYJxV3GBhpMdJRPM3h47slQtWBUZe9Sao5En5WBc4tISGQNP2emcez6qIvDziSgD2T+img==}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.9.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@opentelemetry/api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@orpc/standard-server-fetch@1.15.0':
|
||||||
|
resolution: {integrity: sha512-XYVfgmIt71YrPJSI7RKRiNWjcyktPYHpevkjeohGNBP5aSMAUL95quZArAWr+XOPp4U56XRao54I6wFMZa4How==}
|
||||||
|
|
||||||
|
'@orpc/standard-server-peer@1.15.0':
|
||||||
|
resolution: {integrity: sha512-fsTN+FrdPkseVx99yRenGJYSp0xOrEk9fODk8oh3zzricNYzlZ8GGgCYtDNgt9vFIR/J05dc+Nk76KhcPBQpLw==}
|
||||||
|
|
||||||
|
'@orpc/standard-server@1.15.0':
|
||||||
|
resolution: {integrity: sha512-bje/xn6thDqJY/JQ7xoOjmD1KWE4FsyZOPgnPxwqTZLx/r00stRhLuFvk1hDEGj9UlG7NLZEijDVx0wqvcyDzA==}
|
||||||
|
|
||||||
'@puppeteer/browsers@3.2.0':
|
'@puppeteer/browsers@3.2.0':
|
||||||
resolution: {integrity: sha512-LlBrE8oqGfU7b1Nk2d5Q1SbuPhZxTj0cJEMDPEws28OjNMELlflekmPPuf4FnK03x0ZRjKaYwJElUcKK4kyqJA==}
|
resolution: {integrity: sha512-LlBrE8oqGfU7b1Nk2d5Q1SbuPhZxTj0cJEMDPEws28OjNMELlflekmPPuf4FnK03x0ZRjKaYwJElUcKK4kyqJA==}
|
||||||
engines: {node: '>=22.12.0'}
|
engines: {node: '>=22.12.0'}
|
||||||
@@ -541,19 +564,6 @@ packages:
|
|||||||
'@tailwindcss/postcss@4.3.3':
|
'@tailwindcss/postcss@4.3.3':
|
||||||
resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==}
|
resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==}
|
||||||
|
|
||||||
'@trpc/client@11.18.0':
|
|
||||||
resolution: {integrity: sha512-wOqeg3Fvl25V1ZisQhUD3K8G60ZJDlSGJNSyeXrLH24xAo5w6GSR2Kzb1cSNY9Y+IQ2YZvYGZstBU+V/ulo/ow==}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
'@trpc/server': 11.18.0
|
|
||||||
typescript: '>=5.7.2'
|
|
||||||
|
|
||||||
'@trpc/server@11.18.0':
|
|
||||||
resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=5.7.2'
|
|
||||||
|
|
||||||
'@tweenjs/tween.js@23.1.3':
|
'@tweenjs/tween.js@23.1.3':
|
||||||
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||||
|
|
||||||
@@ -661,6 +671,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
event-target-polyfill@0.0.4:
|
||||||
|
resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==}
|
||||||
|
|
||||||
fflate@0.8.3:
|
fflate@0.8.3:
|
||||||
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
|
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
|
||||||
|
|
||||||
@@ -864,6 +877,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
|
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
partysocket@1.3.0:
|
||||||
|
resolution: {integrity: sha512-1zToNyolZFK/7nuAw/K2bZrNzFqaZyRoCEkS+9vG6WSC5ikrN6qWRe96q6ImU51uptz2r+dAwSkwhJVdQi4LiA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -879,6 +900,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wgBBj7dU5ceGyoT2PCrJpkYOhxPY8mDOmcSKZP92Cj5GgqQ3kv/UxzawnOLxiYuupe4bDf/yiQm3bzs/1nI0rQ==}
|
resolution: {integrity: sha512-wgBBj7dU5ceGyoT2PCrJpkYOhxPY8mDOmcSKZP92Cj5GgqQ3kv/UxzawnOLxiYuupe4bDf/yiQm3bzs/1nI0rQ==}
|
||||||
engines: {node: '>=22.12.0'}
|
engines: {node: '>=22.12.0'}
|
||||||
|
|
||||||
|
radash@12.1.1:
|
||||||
|
resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==}
|
||||||
|
engines: {node: '>=14.18.0'}
|
||||||
|
|
||||||
react-dom@19.2.4:
|
react-dom@19.2.4:
|
||||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -938,6 +963,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
tagged-tag@1.0.0:
|
||||||
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
tailwind-merge@3.6.0:
|
tailwind-merge@3.6.0:
|
||||||
resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
|
resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
|
||||||
|
|
||||||
@@ -954,6 +983,10 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
type-fest@5.8.0:
|
||||||
|
resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
typed-query-selector@2.12.2:
|
typed-query-selector@2.12.2:
|
||||||
resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==}
|
resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==}
|
||||||
|
|
||||||
@@ -1314,6 +1347,40 @@ snapshots:
|
|||||||
'@next/swc-win32-x64-msvc@16.2.12':
|
'@next/swc-win32-x64-msvc@16.2.12':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@orpc/client@1.15.0':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0
|
||||||
|
'@orpc/standard-server': 1.15.0
|
||||||
|
'@orpc/standard-server-fetch': 1.15.0
|
||||||
|
'@orpc/standard-server-peer': 1.15.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/shared@1.15.0':
|
||||||
|
dependencies:
|
||||||
|
radash: 12.1.1
|
||||||
|
type-fest: 5.8.0
|
||||||
|
|
||||||
|
'@orpc/standard-server-fetch@1.15.0':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0
|
||||||
|
'@orpc/standard-server': 1.15.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/standard-server-peer@1.15.0':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0
|
||||||
|
'@orpc/standard-server': 1.15.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
'@orpc/standard-server@1.15.0':
|
||||||
|
dependencies:
|
||||||
|
'@orpc/shared': 1.15.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
'@puppeteer/browsers@3.2.0':
|
'@puppeteer/browsers@3.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
modern-tar: 0.8.4
|
modern-tar: 0.8.4
|
||||||
@@ -1392,15 +1459,6 @@ snapshots:
|
|||||||
postcss: 8.5.25
|
postcss: 8.5.25
|
||||||
tailwindcss: 4.3.3
|
tailwindcss: 4.3.3
|
||||||
|
|
||||||
'@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@5.9.3))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@trpc/server': 11.18.0(typescript@5.9.3)
|
|
||||||
typescript: 5.9.3
|
|
||||||
|
|
||||||
'@trpc/server@11.18.0(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
typescript: 5.9.3
|
|
||||||
|
|
||||||
'@tweenjs/tween.js@23.1.3': {}
|
'@tweenjs/tween.js@23.1.3': {}
|
||||||
|
|
||||||
'@types/node@20.19.43':
|
'@types/node@20.19.43':
|
||||||
@@ -1496,6 +1554,8 @@ snapshots:
|
|||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
|
event-target-polyfill@0.0.4: {}
|
||||||
|
|
||||||
fflate@0.8.3: {}
|
fflate@0.8.3: {}
|
||||||
|
|
||||||
framer-motion@12.43.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
framer-motion@12.43.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
@@ -1647,6 +1707,12 @@ snapshots:
|
|||||||
node-releases@2.0.51:
|
node-releases@2.0.51:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
partysocket@1.3.0(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
event-target-polyfill: 0.0.4
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
postcss@8.4.31:
|
postcss@8.4.31:
|
||||||
@@ -1675,6 +1741,8 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- yauzl
|
- yauzl
|
||||||
|
|
||||||
|
radash@12.1.1: {}
|
||||||
|
|
||||||
react-dom@19.2.4(react@19.2.4):
|
react-dom@19.2.4(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
@@ -1752,6 +1820,8 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
|
|
||||||
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
tailwind-merge@3.6.0: {}
|
tailwind-merge@3.6.0: {}
|
||||||
|
|
||||||
tailwindcss@4.3.3: {}
|
tailwindcss@4.3.3: {}
|
||||||
@@ -1762,6 +1832,10 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
type-fest@5.8.0:
|
||||||
|
dependencies:
|
||||||
|
tagged-tag: 1.0.0
|
||||||
|
|
||||||
typed-query-selector@2.12.2: {}
|
typed-query-selector@2.12.2: {}
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { ChatbotHistoryRow, ChatbotResponse } from "@/lib/types";
|
import type { ChatbotHistoryRow, ChatbotResponse } from "@/lib/types";
|
||||||
|
|
||||||
export const chatbotApi = {
|
export const chatbotApi = {
|
||||||
send: (message: string, guildId?: string, userId?: string) =>
|
send: (message: string, guildId?: string, userId?: string) =>
|
||||||
trpc.chatbot.chat.mutate({
|
orpc.chatbot.chat({
|
||||||
message,
|
message,
|
||||||
context: guildId ? { guildId } : undefined,
|
context: guildId ? { guildId } : undefined,
|
||||||
userId,
|
userId,
|
||||||
}) as unknown as Promise<ChatbotResponse>,
|
}) as unknown as Promise<ChatbotResponse>,
|
||||||
|
|
||||||
getHistory: (userId?: string) =>
|
getHistory: (userId?: string) =>
|
||||||
trpc.chatbot.history.query({
|
orpc.chatbot.history({ limit: 50, userId }) as unknown as Promise<{
|
||||||
limit: 50,
|
history: ChatbotHistoryRow[];
|
||||||
userId,
|
total: number;
|
||||||
}) as unknown as Promise<{ history: ChatbotHistoryRow[]; total: number }>,
|
}>,
|
||||||
|
|
||||||
clearHistory: (userId?: string) =>
|
clearHistory: (userId?: string) =>
|
||||||
trpc.chatbot.clearHistory.mutate({
|
orpc.chatbot.clearHistory({ userId }) as unknown as Promise<{
|
||||||
userId,
|
ok: boolean;
|
||||||
}) as unknown as Promise<{ ok: boolean }>,
|
}>,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { AppConfig } from "@/lib/types";
|
import type { AppConfig } from "@/lib/types";
|
||||||
|
|
||||||
export const configApi = {
|
export const configApi = {
|
||||||
get: () => trpc.config.get.query() as unknown as Promise<AppConfig>,
|
get: () => orpc.config.get() as unknown as Promise<AppConfig>,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type {
|
import type {
|
||||||
DashboardActivity,
|
DashboardActivity,
|
||||||
DashboardChannelDetail,
|
DashboardChannelDetail,
|
||||||
@@ -11,45 +11,40 @@ import type {
|
|||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
|
|
||||||
export const dashboardApi = {
|
export const dashboardApi = {
|
||||||
getStats: () =>
|
getStats: () => orpc.dashboard.stats() as unknown as Promise<DashboardStats>,
|
||||||
trpc.dashboard.stats.query() as unknown as Promise<DashboardStats>,
|
|
||||||
|
|
||||||
getActivity: (days = 14) =>
|
getActivity: (days = 14) =>
|
||||||
trpc.dashboard.activity.query({
|
orpc.dashboard.activity({ days }) as unknown as Promise<DashboardActivity>,
|
||||||
days,
|
|
||||||
}) as unknown as Promise<DashboardActivity>,
|
|
||||||
|
|
||||||
listUsers: (limit?: number, cursor?: string, search?: string) =>
|
listUsers: (limit?: number, cursor?: string, search?: string) =>
|
||||||
trpc.dashboard.users.query({
|
orpc.dashboard.users({
|
||||||
limit,
|
limit,
|
||||||
cursor,
|
cursor,
|
||||||
search,
|
search,
|
||||||
}) as unknown as Promise<PaginatedUsers>,
|
}) as unknown as Promise<PaginatedUsers>,
|
||||||
|
|
||||||
getUserDetail: (userId: string) =>
|
getUserDetail: (userId: string) =>
|
||||||
trpc.dashboard.userDetail.query({
|
orpc.dashboard.userDetail({
|
||||||
userId,
|
userId,
|
||||||
}) as unknown as Promise<DashboardUserDetail>,
|
}) as unknown as Promise<DashboardUserDetail>,
|
||||||
|
|
||||||
listChannels: (limit?: number, search?: string, guildId?: string) =>
|
listChannels: (limit?: number, search?: string, guildId?: string) =>
|
||||||
trpc.dashboard.channels.query({
|
orpc.dashboard.channels({
|
||||||
limit,
|
limit,
|
||||||
search,
|
search,
|
||||||
guildId,
|
guildId,
|
||||||
}) as unknown as Promise<PaginatedChannels>,
|
}) as unknown as Promise<PaginatedChannels>,
|
||||||
|
|
||||||
getChannelDetail: (channelId: string) =>
|
getChannelDetail: (channelId: string) =>
|
||||||
trpc.dashboard.channelDetail.query({
|
orpc.dashboard.channelDetail({
|
||||||
channelId,
|
channelId,
|
||||||
}) as unknown as Promise<DashboardChannelDetail>,
|
}) as unknown as Promise<DashboardChannelDetail>,
|
||||||
|
|
||||||
getTopReactions: (limit = 20) =>
|
getTopReactions: (limit = 20) =>
|
||||||
trpc.dashboard.reactions.query({ limit }) as unknown as Promise<
|
orpc.dashboard.reactions({ limit }) as unknown as Promise<
|
||||||
TopReactedMessage[]
|
TopReactedMessage[]
|
||||||
>,
|
>,
|
||||||
|
|
||||||
getTopReactors: (limit = 20) =>
|
getTopReactors: (limit = 20) =>
|
||||||
trpc.dashboard.reactors.query({ limit }) as unknown as Promise<
|
orpc.dashboard.reactors({ limit }) as unknown as Promise<TopReactor[]>,
|
||||||
TopReactor[]
|
|
||||||
>,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// tRPC client is browser-only (wsLink). Re-export it for convenience / for any
|
// oRPC client is browser-only (websocket RPCLink + partysocket). Re-export it
|
||||||
// code that wants to call tRPC directly instead of going through the api/*
|
// for convenience / for any code that wants to call oRPC directly instead of
|
||||||
// wrappers. Server-side RSC data lives in ./server (httpLink).
|
// going through the api/* wrappers. Server-side RSC data lives in ./server
|
||||||
export { trpc } from "../trpc/client";
|
// (fetch RPCLink).
|
||||||
|
export { orpc } from "../orpc/client";
|
||||||
export { chatbotApi } from "./chatbot";
|
export { chatbotApi } from "./chatbot";
|
||||||
export { configApi } from "./config";
|
export { configApi } from "./config";
|
||||||
export { dashboardApi } from "./dashboard";
|
export { dashboardApi } from "./dashboard";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { MediaState } from "@/lib/types";
|
import type { MediaState } from "@/lib/types";
|
||||||
|
|
||||||
export const mediaApi = {
|
export const mediaApi = {
|
||||||
getStatus: () => trpc.media.status.query() as unknown as Promise<MediaState>,
|
getStatus: () => orpc.media.status() as unknown as Promise<MediaState>,
|
||||||
queue: (source: string, mode: string) =>
|
queue: (source: string, mode: string) =>
|
||||||
trpc.media.queue.mutate({ source, mode }) as unknown as Promise<MediaState>,
|
orpc.media.queue({ source, mode }) as unknown as Promise<MediaState>,
|
||||||
skip: () => trpc.media.skip.mutate() as unknown as Promise<MediaState>,
|
skip: () => orpc.media.skip() as unknown as Promise<MediaState>,
|
||||||
stop: () => trpc.media.stop.mutate() as unknown as Promise<MediaState>,
|
stop: () => orpc.media.stop() as unknown as Promise<MediaState>,
|
||||||
loop: (loop: boolean) =>
|
loop: (loop: boolean) =>
|
||||||
trpc.media.loop.mutate({ loop }) as unknown as Promise<MediaState>,
|
orpc.media.loop({ loop }) as unknown as Promise<MediaState>,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
||||||
|
|
||||||
export const messagesApi = {
|
export const messagesApi = {
|
||||||
@@ -8,7 +8,7 @@ export const messagesApi = {
|
|||||||
channelId?: string,
|
channelId?: string,
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
) =>
|
) =>
|
||||||
trpc.messages.list.query({
|
orpc.messages.list({
|
||||||
guildId,
|
guildId,
|
||||||
limit,
|
limit,
|
||||||
channelId,
|
channelId,
|
||||||
@@ -19,7 +19,7 @@ export const messagesApi = {
|
|||||||
}>,
|
}>,
|
||||||
|
|
||||||
getByChannel: (channelId: string, limit?: number, cursor?: string) =>
|
getByChannel: (channelId: string, limit?: number, cursor?: string) =>
|
||||||
trpc.messages.byChannel.query({
|
orpc.messages.byChannel({
|
||||||
channelId,
|
channelId,
|
||||||
query: { channelId, limit, cursor },
|
query: { channelId, limit, cursor },
|
||||||
}) as unknown as Promise<{
|
}) as unknown as Promise<{
|
||||||
@@ -28,10 +28,10 @@ export const messagesApi = {
|
|||||||
}>,
|
}>,
|
||||||
|
|
||||||
getDetail: (id: string) =>
|
getDetail: (id: string) =>
|
||||||
trpc.messages.detail.query({ id }) as unknown as Promise<MessageRecord>,
|
orpc.messages.detail({ id }) as unknown as Promise<MessageRecord>,
|
||||||
|
|
||||||
getImages: (guildId: string, limit?: number) =>
|
getImages: (guildId: string, limit?: number) =>
|
||||||
trpc.messages.images.query({ guildId, limit }) as unknown as Promise<{
|
orpc.messages.images({ guildId, limit }) as unknown as Promise<{
|
||||||
data: MessageRecord[];
|
data: MessageRecord[];
|
||||||
nextCursor: string | null;
|
nextCursor: string | null;
|
||||||
}>,
|
}>,
|
||||||
@@ -42,7 +42,7 @@ export const messagesApi = {
|
|||||||
cursor?: string,
|
cursor?: string,
|
||||||
messageId?: string,
|
messageId?: string,
|
||||||
) =>
|
) =>
|
||||||
trpc.messages.attachmentsByChannel.query({
|
orpc.messages.attachmentsByChannel({
|
||||||
channelId,
|
channelId,
|
||||||
query: { channelId, limit, cursor, messageId },
|
query: { channelId, limit, cursor, messageId },
|
||||||
}) as unknown as Promise<{
|
}) as unknown as Promise<{
|
||||||
@@ -51,15 +51,15 @@ export const messagesApi = {
|
|||||||
}>,
|
}>,
|
||||||
|
|
||||||
getReview: (limit?: number, channelId?: string) =>
|
getReview: (limit?: number, channelId?: string) =>
|
||||||
trpc.messages.review.query({ limit, channelId }) as unknown as Promise<{
|
orpc.messages.review({ limit, channelId }) as unknown as Promise<{
|
||||||
results: MessageRecord[];
|
results: MessageRecord[];
|
||||||
limit: number;
|
limit: number;
|
||||||
cursor: null;
|
cursor: null;
|
||||||
}>,
|
}>,
|
||||||
|
|
||||||
// Analysis search (formerly /api/analysis/search → tRPC analysis.search)
|
// Analysis search (formerly /api/analysis/search → oRPC analysis.search)
|
||||||
search: (q: string, limit?: number) =>
|
search: (q: string, limit?: number) =>
|
||||||
trpc.analysis.search.query({ q, limit }) as unknown as Promise<{
|
orpc.analysis.search({ q, limit }) as unknown as Promise<{
|
||||||
results: MessageRecord[];
|
results: MessageRecord[];
|
||||||
}>,
|
}>,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { ModerationStats, PaginatedModerationActions } from "@/lib/types";
|
import type { ModerationStats, PaginatedModerationActions } from "@/lib/types";
|
||||||
|
|
||||||
export const moderationApi = {
|
export const moderationApi = {
|
||||||
getStats: () =>
|
getStats: () =>
|
||||||
trpc.moderation.stats.query() as unknown as Promise<ModerationStats>,
|
orpc.moderation.stats() as unknown as Promise<ModerationStats>,
|
||||||
|
|
||||||
listActions: (
|
listActions: (
|
||||||
limit?: number,
|
limit?: number,
|
||||||
@@ -11,7 +11,7 @@ export const moderationApi = {
|
|||||||
actionType?: string,
|
actionType?: string,
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
) =>
|
) =>
|
||||||
trpc.moderation.actions.query({
|
orpc.moderation.actions({
|
||||||
limit,
|
limit,
|
||||||
status,
|
status,
|
||||||
actionType,
|
actionType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { PaginatedRecordings } from "@/lib/types";
|
import type { PaginatedRecordings } from "@/lib/types";
|
||||||
|
|
||||||
export const recordingsApi = {
|
export const recordingsApi = {
|
||||||
@@ -8,7 +8,7 @@ export const recordingsApi = {
|
|||||||
userId?: string,
|
userId?: string,
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
) =>
|
) =>
|
||||||
trpc.recordings.list.query({
|
orpc.recordings.list({
|
||||||
limit,
|
limit,
|
||||||
channelId,
|
channelId,
|
||||||
userId,
|
userId,
|
||||||
@@ -16,7 +16,5 @@ export const recordingsApi = {
|
|||||||
}) as unknown as Promise<PaginatedRecordings>,
|
}) as unknown as Promise<PaginatedRecordings>,
|
||||||
|
|
||||||
delete: (id: string) =>
|
delete: (id: string) =>
|
||||||
trpc.recordings.delete.mutate({ id }) as unknown as Promise<{
|
orpc.recordings.delete({ id }) as unknown as Promise<{ ok: boolean }>,
|
||||||
ok: boolean;
|
|
||||||
}>,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* Server-only data layer (React Server Components / route handlers).
|
* Server-only data layer (React Server Components / route handlers).
|
||||||
*
|
*
|
||||||
* The dashboard is fully tRPC-native: this module talks to the backend's
|
* The dashboard is fully oRPC-native: this module talks to the backend's
|
||||||
* tRPC HTTP endpoint (/trpc) via an httpLink client — the same appRouter the
|
* oRPC HTTP endpoint (/trpc) via a fetch RPCLink client — the same appRouter
|
||||||
* browser reaches over WebSocket. No legacy REST `/api/*` is used.
|
* the browser reaches over WebSocket. No legacy REST `/api/*` is used.
|
||||||
*
|
*
|
||||||
* The client is loosely typed (see ./types → TRPCClient); results are asserted
|
* The client is loosely typed (see ./types → ORPCClient); results are asserted
|
||||||
* to the frontend's local types at each call site.
|
* to the frontend's local types at each call site.
|
||||||
*
|
*
|
||||||
* Never import this module from a client component. Browser code uses
|
* Never import this module from a client component. Browser code uses
|
||||||
* `@/lib/trpc/client` (wsLink) via the `@/lib/api/*` wrappers.
|
* `@/lib/orpc/client` (websocket RPCLink) via the `@/lib/api/*` wrappers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
import { createORPCClient } from "@orpc/client";
|
||||||
|
import { RPCLink } from "@orpc/client/fetch";
|
||||||
import type {
|
import type {
|
||||||
AppConfig,
|
AppConfig,
|
||||||
DashboardActivity,
|
DashboardActivity,
|
||||||
@@ -24,24 +25,22 @@ import type {
|
|||||||
PaginatedRecordings,
|
PaginatedRecordings,
|
||||||
VoiceStatus,
|
VoiceStatus,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import type { TRPCClient } from "../trpc/types";
|
import type { ORPCClient } from "../orpc/types";
|
||||||
|
|
||||||
const BACKEND_URL =
|
const BACKEND_URL =
|
||||||
process.env.GMW_BACKEND_URL?.replace(/\/+$/, "") || "http://127.0.0.1:4001";
|
process.env.GMW_BACKEND_URL?.replace(/\/+$/, "") || "http://127.0.0.1:4001";
|
||||||
|
|
||||||
let _client: TRPCClient | null = null;
|
let _client: ORPCClient | null = null;
|
||||||
function serverTrpc(): TRPCClient {
|
function serverOrpc(): ORPCClient {
|
||||||
if (!_client) {
|
if (!_client) {
|
||||||
_client = createTRPCClient({
|
const link = new RPCLink({
|
||||||
links: [
|
url: `${BACKEND_URL}/trpc`,
|
||||||
httpBatchLink({
|
// Always bypass Next.js fetch cache for live dashboard data.
|
||||||
url: `${BACKEND_URL}/trpc`,
|
fetch(url, init) {
|
||||||
fetch(url, init) {
|
return fetch(url, { ...init, cache: "no-store" });
|
||||||
return fetch(url, { ...init, cache: "no-store" });
|
},
|
||||||
},
|
});
|
||||||
}),
|
_client = createORPCClient(link) as unknown as ORPCClient;
|
||||||
],
|
|
||||||
}) as unknown as TRPCClient;
|
|
||||||
}
|
}
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
@@ -57,30 +56,30 @@ export class ApiServerError extends Error {
|
|||||||
|
|
||||||
// ---- Dashboard ----
|
// ---- Dashboard ----
|
||||||
export async function getDashboardStats(): Promise<DashboardStats> {
|
export async function getDashboardStats(): Promise<DashboardStats> {
|
||||||
return serverTrpc().dashboard.stats.query() as unknown as Promise<DashboardStats>;
|
return serverOrpc().dashboard.stats() as unknown as Promise<DashboardStats>;
|
||||||
}
|
}
|
||||||
export async function getActivity(days = 14): Promise<DashboardActivity> {
|
export async function getActivity(days = 14): Promise<DashboardActivity> {
|
||||||
return serverTrpc().dashboard.activity.query({
|
return serverOrpc().dashboard.activity({
|
||||||
days,
|
days,
|
||||||
}) as unknown as Promise<DashboardActivity>;
|
}) as unknown as Promise<DashboardActivity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Media ----
|
// ---- Media ----
|
||||||
export async function getMediaStatus(): Promise<MediaState> {
|
export async function getMediaStatus(): Promise<MediaState> {
|
||||||
return serverTrpc().media.status.query() as unknown as Promise<MediaState>;
|
return serverOrpc().media.status() as unknown as Promise<MediaState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Config ----
|
// ---- Config ----
|
||||||
export async function getConfig(): Promise<AppConfig> {
|
export async function getConfig(): Promise<AppConfig> {
|
||||||
return serverTrpc().config.get.query() as unknown as Promise<AppConfig>;
|
return serverOrpc().config.get() as unknown as Promise<AppConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Moderation ----
|
// ---- Moderation ----
|
||||||
export async function getModerationStats(): Promise<ModerationStats> {
|
export async function getModerationStats(): Promise<ModerationStats> {
|
||||||
return serverTrpc().moderation.stats.query() as unknown as Promise<ModerationStats>;
|
return serverOrpc().moderation.stats() as unknown as Promise<ModerationStats>;
|
||||||
}
|
}
|
||||||
export async function getModerationActions(limit = 100) {
|
export async function getModerationActions(limit = 100) {
|
||||||
const res = (await serverTrpc().moderation.actions.query({
|
const res = (await serverOrpc().moderation.actions({
|
||||||
limit,
|
limit,
|
||||||
})) as unknown as PaginatedModerationActions;
|
})) as unknown as PaginatedModerationActions;
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -88,15 +87,15 @@ export async function getModerationActions(limit = 100) {
|
|||||||
|
|
||||||
// ---- Voice ----
|
// ---- Voice ----
|
||||||
export async function getGuilds(): Promise<Guild[]> {
|
export async function getGuilds(): Promise<Guild[]> {
|
||||||
return serverTrpc().voice.guilds.query() as unknown as Promise<Guild[]>;
|
return serverOrpc().voice.guilds() as unknown as Promise<Guild[]>;
|
||||||
}
|
}
|
||||||
export async function getVoiceStatus(): Promise<VoiceStatus> {
|
export async function getVoiceStatus(): Promise<VoiceStatus> {
|
||||||
return serverTrpc().voice.status.query() as unknown as Promise<VoiceStatus>;
|
return serverOrpc().voice.status() as unknown as Promise<VoiceStatus>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Recordings ----
|
// ---- Recordings ----
|
||||||
export async function getRecordings(limit = 50): Promise<PaginatedRecordings> {
|
export async function getRecordings(limit = 50): Promise<PaginatedRecordings> {
|
||||||
return serverTrpc().recordings.list.query({
|
return serverOrpc().recordings.list({
|
||||||
limit,
|
limit,
|
||||||
}) as unknown as Promise<PaginatedRecordings>;
|
}) as unknown as Promise<PaginatedRecordings>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { UiState } from "@/lib/types";
|
import type { UiState } from "@/lib/types";
|
||||||
|
|
||||||
export const uiStateApi = {
|
export const uiStateApi = {
|
||||||
get: () => trpc.uiState.get.query() as unknown as Promise<UiState>,
|
get: () => orpc.uiState.get() as unknown as Promise<UiState>,
|
||||||
|
|
||||||
save: (state: UiState) =>
|
save: (state: UiState) =>
|
||||||
trpc.uiState.update.mutate(state) as unknown as Promise<{ ok: boolean }>,
|
orpc.uiState.update(state) as unknown as Promise<{ ok: boolean }>,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import { trpc } from "@/lib/trpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
import type { Channel, Guild, VoiceStatus } from "@/lib/types";
|
import type { Channel, Guild, VoiceStatus } from "@/lib/types";
|
||||||
|
|
||||||
export const voiceApi = {
|
export const voiceApi = {
|
||||||
// Guilds
|
// Guilds
|
||||||
getGuilds: () => trpc.voice.guilds.query() as unknown as Promise<Guild[]>,
|
getGuilds: () => orpc.voice.guilds() as unknown as Promise<Guild[]>,
|
||||||
getTextChannels: (guildId: string) =>
|
getTextChannels: (guildId: string) =>
|
||||||
trpc.voice.textChannels.query({ guildId }) as unknown as Promise<Channel[]>,
|
orpc.voice.textChannels({ guildId }) as unknown as Promise<Channel[]>,
|
||||||
getVoiceChannels: (guildId: string) =>
|
getVoiceChannels: (guildId: string) =>
|
||||||
trpc.voice.voiceChannels.query({
|
orpc.voice.voiceChannels({ guildId }) as unknown as Promise<Channel[]>,
|
||||||
guildId,
|
|
||||||
}) as unknown as Promise<Channel[]>,
|
|
||||||
|
|
||||||
// Voice connection
|
// Voice connection
|
||||||
getStatus: () => trpc.voice.status.query() as unknown as Promise<VoiceStatus>,
|
getStatus: () => orpc.voice.status() as unknown as Promise<VoiceStatus>,
|
||||||
connect: (guildId: string, channelId: string) =>
|
connect: (guildId: string, channelId: string) =>
|
||||||
trpc.voice.connect.mutate({
|
orpc.voice.connect({
|
||||||
guildId,
|
guildId,
|
||||||
channelId,
|
channelId,
|
||||||
}) as unknown as Promise<VoiceStatus>,
|
}) as unknown as Promise<VoiceStatus>,
|
||||||
disconnect: () =>
|
disconnect: () => orpc.voice.disconnect() as unknown as Promise<VoiceStatus>,
|
||||||
trpc.voice.disconnect.mutate() as unknown as Promise<VoiceStatus>,
|
|
||||||
sendCommand: (command: string) =>
|
sendCommand: (command: string) =>
|
||||||
trpc.voice.command.mutate({ command }) as unknown as Promise<unknown>,
|
orpc.voice.command({ command }) as unknown as Promise<unknown>,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createORPCClient } from "@orpc/client";
|
||||||
|
import { RPCLink } from "@orpc/client/websocket";
|
||||||
|
import PartySocket from "partysocket";
|
||||||
|
import type { ORPCClient } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* oRPC client over WebSocket (browser only).
|
||||||
|
*
|
||||||
|
* oRPC's own WebSocket RPCLink takes a caller-provided socket and does NOT
|
||||||
|
* auto-reconnect, so we back it with partysocket — a reconnecting WebSocket
|
||||||
|
* whose `host` + `basePath: "trpc"` resolves to exactly `/trpc`, matching the
|
||||||
|
* backend's oRPC WS mount. partysocket keeps a stable socket identity across
|
||||||
|
* reconnects, which is what oRPC's RPCLink expects.
|
||||||
|
*
|
||||||
|
* The client is loosely typed (see ./types): we do NOT import the backend
|
||||||
|
* router's type into the FE (fragile coupling). api/* wrappers assert leaf
|
||||||
|
* results to the frontend's local types.
|
||||||
|
*/
|
||||||
|
const orpc: ORPCClient = (() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
// SSR fallback — api/* callers are client components, never run server-side.
|
||||||
|
return undefined as unknown as ORPCClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = new PartySocket({
|
||||||
|
host: window.location.host,
|
||||||
|
basePath: "trpc",
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = new RPCLink({ websocket: socket });
|
||||||
|
return createORPCClient(link) as unknown as ORPCClient;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export { orpc };
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Loosely-typed oRPC client shape shared by the browser (websocket RPCLink) and
|
||||||
|
* server-side (fetch RPCLink) clients. The real router lives on the backend; we
|
||||||
|
* do NOT import its type here (coupling the FE typecheck to the BE source tree
|
||||||
|
* and its `@/` alias layout is fragile). Instead we describe the minimal shape
|
||||||
|
* the api/* wrappers rely on: any path segment yields an object, and leaves are
|
||||||
|
* callable functions returning a Promise. Leaf results are asserted to the
|
||||||
|
* frontend's local types inside the api/* wrappers.
|
||||||
|
*/
|
||||||
|
export type ORPCClient = {
|
||||||
|
[k: string]: ORPCClient;
|
||||||
|
} & ((input?: unknown) => Promise<unknown>);
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { createTRPCClient, createWSClient, wsLink } from "@trpc/client";
|
|
||||||
import type { TRPCClient } from "./types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WebSocket URL for the tRPC data RPC endpoint (/trpc), served by the backend
|
|
||||||
* on the same host as the page (nginx proxies it). Upgrades http→ws and
|
|
||||||
* derives wss:// when the page is served over https.
|
|
||||||
*/
|
|
||||||
function resolveWsUrl(): string {
|
|
||||||
if (typeof window === "undefined") return "ws://localhost/trpc";
|
|
||||||
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
|
||||||
return `${proto}://${window.location.host}/trpc`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* tRPC client over WebSocket (browser only). `createTRPCClient` (no router
|
|
||||||
* generic) is an untyped client; we assert it into the loose `TRPCClient`
|
|
||||||
* shape so `.dashboard.stats.query(...)` etc. typecheck. See ./types for why
|
|
||||||
* the client shape is intentionally untyped.
|
|
||||||
*/
|
|
||||||
const wsClient =
|
|
||||||
typeof window === "undefined"
|
|
||||||
? null
|
|
||||||
: createWSClient({ url: resolveWsUrl() });
|
|
||||||
|
|
||||||
export const trpc: TRPCClient = wsClient
|
|
||||||
? (createTRPCClient({
|
|
||||||
links: [wsLink({ client: wsClient })],
|
|
||||||
}) as unknown as TRPCClient)
|
|
||||||
: // SSR fallback — api/* callers are client components, never run server-side.
|
|
||||||
(undefined as unknown as TRPCClient);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Loosely-typed tRPC client shape shared by the browser (wsLink) and
|
|
||||||
* server-side (httpLink) clients. The real router lives on the backend; we do
|
|
||||||
* NOT import its type here (coupling the FE typecheck to the BE source tree
|
|
||||||
* and its `@/` alias layout is fragile). Instead we describe the minimal shape
|
|
||||||
* the api/* wrappers rely on: any path segment yields an object with `query`
|
|
||||||
* and `mutate`, and arbitrary further nesting is allowed. Leaf results are
|
|
||||||
* asserted to the frontend's local types inside the api/* wrappers.
|
|
||||||
*/
|
|
||||||
export type TRPCClient = {
|
|
||||||
[k: string]: TRPCClient;
|
|
||||||
} & {
|
|
||||||
query(input?: unknown): Promise<unknown>;
|
|
||||||
mutate(input?: unknown): Promise<unknown>;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user