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:
@@ -1,22 +1,22 @@
|
||||
import { trpc } from "@/lib/trpc/client";
|
||||
import { orpc } from "@/lib/orpc/client";
|
||||
import type { ChatbotHistoryRow, ChatbotResponse } from "@/lib/types";
|
||||
|
||||
export const chatbotApi = {
|
||||
send: (message: string, guildId?: string, userId?: string) =>
|
||||
trpc.chatbot.chat.mutate({
|
||||
orpc.chatbot.chat({
|
||||
message,
|
||||
context: guildId ? { guildId } : undefined,
|
||||
userId,
|
||||
}) as unknown as Promise<ChatbotResponse>,
|
||||
|
||||
getHistory: (userId?: string) =>
|
||||
trpc.chatbot.history.query({
|
||||
limit: 50,
|
||||
userId,
|
||||
}) as unknown as Promise<{ history: ChatbotHistoryRow[]; total: number }>,
|
||||
orpc.chatbot.history({ limit: 50, userId }) as unknown as Promise<{
|
||||
history: ChatbotHistoryRow[];
|
||||
total: number;
|
||||
}>,
|
||||
|
||||
clearHistory: (userId?: string) =>
|
||||
trpc.chatbot.clearHistory.mutate({
|
||||
userId,
|
||||
}) as unknown as Promise<{ ok: boolean }>,
|
||||
orpc.chatbot.clearHistory({ userId }) 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";
|
||||
|
||||
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 {
|
||||
DashboardActivity,
|
||||
DashboardChannelDetail,
|
||||
@@ -11,45 +11,40 @@ import type {
|
||||
} from "@/lib/types";
|
||||
|
||||
export const dashboardApi = {
|
||||
getStats: () =>
|
||||
trpc.dashboard.stats.query() as unknown as Promise<DashboardStats>,
|
||||
getStats: () => orpc.dashboard.stats() as unknown as Promise<DashboardStats>,
|
||||
|
||||
getActivity: (days = 14) =>
|
||||
trpc.dashboard.activity.query({
|
||||
days,
|
||||
}) as unknown as Promise<DashboardActivity>,
|
||||
orpc.dashboard.activity({ days }) as unknown as Promise<DashboardActivity>,
|
||||
|
||||
listUsers: (limit?: number, cursor?: string, search?: string) =>
|
||||
trpc.dashboard.users.query({
|
||||
orpc.dashboard.users({
|
||||
limit,
|
||||
cursor,
|
||||
search,
|
||||
}) as unknown as Promise<PaginatedUsers>,
|
||||
|
||||
getUserDetail: (userId: string) =>
|
||||
trpc.dashboard.userDetail.query({
|
||||
orpc.dashboard.userDetail({
|
||||
userId,
|
||||
}) as unknown as Promise<DashboardUserDetail>,
|
||||
|
||||
listChannels: (limit?: number, search?: string, guildId?: string) =>
|
||||
trpc.dashboard.channels.query({
|
||||
orpc.dashboard.channels({
|
||||
limit,
|
||||
search,
|
||||
guildId,
|
||||
}) as unknown as Promise<PaginatedChannels>,
|
||||
|
||||
getChannelDetail: (channelId: string) =>
|
||||
trpc.dashboard.channelDetail.query({
|
||||
orpc.dashboard.channelDetail({
|
||||
channelId,
|
||||
}) as unknown as Promise<DashboardChannelDetail>,
|
||||
|
||||
getTopReactions: (limit = 20) =>
|
||||
trpc.dashboard.reactions.query({ limit }) as unknown as Promise<
|
||||
orpc.dashboard.reactions({ limit }) as unknown as Promise<
|
||||
TopReactedMessage[]
|
||||
>,
|
||||
|
||||
getTopReactors: (limit = 20) =>
|
||||
trpc.dashboard.reactors.query({ limit }) as unknown as Promise<
|
||||
TopReactor[]
|
||||
>,
|
||||
orpc.dashboard.reactors({ limit }) as unknown as Promise<TopReactor[]>,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// tRPC client is browser-only (wsLink). Re-export it for convenience / for any
|
||||
// code that wants to call tRPC directly instead of going through the api/*
|
||||
// wrappers. Server-side RSC data lives in ./server (httpLink).
|
||||
export { trpc } from "../trpc/client";
|
||||
// oRPC client is browser-only (websocket RPCLink + partysocket). Re-export it
|
||||
// for convenience / for any code that wants to call oRPC directly instead of
|
||||
// going through the api/* wrappers. Server-side RSC data lives in ./server
|
||||
// (fetch RPCLink).
|
||||
export { orpc } from "../orpc/client";
|
||||
export { chatbotApi } from "./chatbot";
|
||||
export { configApi } from "./config";
|
||||
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";
|
||||
|
||||
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) =>
|
||||
trpc.media.queue.mutate({ source, mode }) as unknown as Promise<MediaState>,
|
||||
skip: () => trpc.media.skip.mutate() as unknown as Promise<MediaState>,
|
||||
stop: () => trpc.media.stop.mutate() as unknown as Promise<MediaState>,
|
||||
orpc.media.queue({ source, mode }) as unknown as Promise<MediaState>,
|
||||
skip: () => orpc.media.skip() as unknown as Promise<MediaState>,
|
||||
stop: () => orpc.media.stop() as unknown as Promise<MediaState>,
|
||||
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";
|
||||
|
||||
export const messagesApi = {
|
||||
@@ -8,7 +8,7 @@ export const messagesApi = {
|
||||
channelId?: string,
|
||||
cursor?: string,
|
||||
) =>
|
||||
trpc.messages.list.query({
|
||||
orpc.messages.list({
|
||||
guildId,
|
||||
limit,
|
||||
channelId,
|
||||
@@ -19,7 +19,7 @@ export const messagesApi = {
|
||||
}>,
|
||||
|
||||
getByChannel: (channelId: string, limit?: number, cursor?: string) =>
|
||||
trpc.messages.byChannel.query({
|
||||
orpc.messages.byChannel({
|
||||
channelId,
|
||||
query: { channelId, limit, cursor },
|
||||
}) as unknown as Promise<{
|
||||
@@ -28,10 +28,10 @@ export const messagesApi = {
|
||||
}>,
|
||||
|
||||
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) =>
|
||||
trpc.messages.images.query({ guildId, limit }) as unknown as Promise<{
|
||||
orpc.messages.images({ guildId, limit }) as unknown as Promise<{
|
||||
data: MessageRecord[];
|
||||
nextCursor: string | null;
|
||||
}>,
|
||||
@@ -42,7 +42,7 @@ export const messagesApi = {
|
||||
cursor?: string,
|
||||
messageId?: string,
|
||||
) =>
|
||||
trpc.messages.attachmentsByChannel.query({
|
||||
orpc.messages.attachmentsByChannel({
|
||||
channelId,
|
||||
query: { channelId, limit, cursor, messageId },
|
||||
}) as unknown as Promise<{
|
||||
@@ -51,15 +51,15 @@ export const messagesApi = {
|
||||
}>,
|
||||
|
||||
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[];
|
||||
limit: number;
|
||||
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) =>
|
||||
trpc.analysis.search.query({ q, limit }) as unknown as Promise<{
|
||||
orpc.analysis.search({ q, limit }) as unknown as Promise<{
|
||||
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";
|
||||
|
||||
export const moderationApi = {
|
||||
getStats: () =>
|
||||
trpc.moderation.stats.query() as unknown as Promise<ModerationStats>,
|
||||
orpc.moderation.stats() as unknown as Promise<ModerationStats>,
|
||||
|
||||
listActions: (
|
||||
limit?: number,
|
||||
@@ -11,7 +11,7 @@ export const moderationApi = {
|
||||
actionType?: string,
|
||||
cursor?: string,
|
||||
) =>
|
||||
trpc.moderation.actions.query({
|
||||
orpc.moderation.actions({
|
||||
limit,
|
||||
status,
|
||||
actionType,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { trpc } from "@/lib/trpc/client";
|
||||
import { orpc } from "@/lib/orpc/client";
|
||||
import type { PaginatedRecordings } from "@/lib/types";
|
||||
|
||||
export const recordingsApi = {
|
||||
@@ -8,7 +8,7 @@ export const recordingsApi = {
|
||||
userId?: string,
|
||||
cursor?: string,
|
||||
) =>
|
||||
trpc.recordings.list.query({
|
||||
orpc.recordings.list({
|
||||
limit,
|
||||
channelId,
|
||||
userId,
|
||||
@@ -16,7 +16,5 @@ export const recordingsApi = {
|
||||
}) as unknown as Promise<PaginatedRecordings>,
|
||||
|
||||
delete: (id: string) =>
|
||||
trpc.recordings.delete.mutate({ id }) as unknown as Promise<{
|
||||
ok: boolean;
|
||||
}>,
|
||||
orpc.recordings.delete({ id }) as unknown as Promise<{ ok: boolean }>,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
/**
|
||||
* Server-only data layer (React Server Components / route handlers).
|
||||
*
|
||||
* The dashboard is fully tRPC-native: this module talks to the backend's
|
||||
* tRPC HTTP endpoint (/trpc) via an httpLink client — the same appRouter the
|
||||
* browser reaches over WebSocket. No legacy REST `/api/*` is used.
|
||||
* The dashboard is fully oRPC-native: this module talks to the backend's
|
||||
* oRPC HTTP endpoint (/trpc) via a fetch RPCLink client — the same appRouter
|
||||
* 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.
|
||||
*
|
||||
* 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 {
|
||||
AppConfig,
|
||||
DashboardActivity,
|
||||
@@ -24,24 +25,22 @@ import type {
|
||||
PaginatedRecordings,
|
||||
VoiceStatus,
|
||||
} from "@/lib/types";
|
||||
import type { TRPCClient } from "../trpc/types";
|
||||
import type { ORPCClient } from "../orpc/types";
|
||||
|
||||
const BACKEND_URL =
|
||||
process.env.GMW_BACKEND_URL?.replace(/\/+$/, "") || "http://127.0.0.1:4001";
|
||||
|
||||
let _client: TRPCClient | null = null;
|
||||
function serverTrpc(): TRPCClient {
|
||||
let _client: ORPCClient | null = null;
|
||||
function serverOrpc(): ORPCClient {
|
||||
if (!_client) {
|
||||
_client = createTRPCClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${BACKEND_URL}/trpc`,
|
||||
fetch(url, init) {
|
||||
return fetch(url, { ...init, cache: "no-store" });
|
||||
},
|
||||
}),
|
||||
],
|
||||
}) as unknown as TRPCClient;
|
||||
const link = new RPCLink({
|
||||
url: `${BACKEND_URL}/trpc`,
|
||||
// Always bypass Next.js fetch cache for live dashboard data.
|
||||
fetch(url, init) {
|
||||
return fetch(url, { ...init, cache: "no-store" });
|
||||
},
|
||||
});
|
||||
_client = createORPCClient(link) as unknown as ORPCClient;
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
@@ -57,30 +56,30 @@ export class ApiServerError extends Error {
|
||||
|
||||
// ---- Dashboard ----
|
||||
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> {
|
||||
return serverTrpc().dashboard.activity.query({
|
||||
return serverOrpc().dashboard.activity({
|
||||
days,
|
||||
}) as unknown as Promise<DashboardActivity>;
|
||||
}
|
||||
|
||||
// ---- Media ----
|
||||
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 ----
|
||||
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 ----
|
||||
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) {
|
||||
const res = (await serverTrpc().moderation.actions.query({
|
||||
const res = (await serverOrpc().moderation.actions({
|
||||
limit,
|
||||
})) as unknown as PaginatedModerationActions;
|
||||
return res.data;
|
||||
@@ -88,15 +87,15 @@ export async function getModerationActions(limit = 100) {
|
||||
|
||||
// ---- Voice ----
|
||||
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> {
|
||||
return serverTrpc().voice.status.query() as unknown as Promise<VoiceStatus>;
|
||||
return serverOrpc().voice.status() as unknown as Promise<VoiceStatus>;
|
||||
}
|
||||
|
||||
// ---- Recordings ----
|
||||
export async function getRecordings(limit = 50): Promise<PaginatedRecordings> {
|
||||
return serverTrpc().recordings.list.query({
|
||||
return serverOrpc().recordings.list({
|
||||
limit,
|
||||
}) 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";
|
||||
|
||||
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) =>
|
||||
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";
|
||||
|
||||
export const voiceApi = {
|
||||
// Guilds
|
||||
getGuilds: () => trpc.voice.guilds.query() as unknown as Promise<Guild[]>,
|
||||
getGuilds: () => orpc.voice.guilds() as unknown as Promise<Guild[]>,
|
||||
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) =>
|
||||
trpc.voice.voiceChannels.query({
|
||||
guildId,
|
||||
}) as unknown as Promise<Channel[]>,
|
||||
orpc.voice.voiceChannels({ guildId }) as unknown as Promise<Channel[]>,
|
||||
|
||||
// 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) =>
|
||||
trpc.voice.connect.mutate({
|
||||
orpc.voice.connect({
|
||||
guildId,
|
||||
channelId,
|
||||
}) as unknown as Promise<VoiceStatus>,
|
||||
disconnect: () =>
|
||||
trpc.voice.disconnect.mutate() as unknown as Promise<VoiceStatus>,
|
||||
disconnect: () => orpc.voice.disconnect() as unknown as Promise<VoiceStatus>,
|
||||
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