kgni
kgni•2mo ago

tRPC context, NeonDB & WebSockets

Hi there! I'm currently working on a serverless app with the following tech stack: Cloudflare Workers, Hono, tRPC, DrizzleORM & NeonDB. I'm trying to create 2 Neon client instances (using DrizzleORM) on my tRPC server that is running on Cloudflare Workers. 1 for HTTP 1 for WebSockets (this is used for transactions) I'm a bit confused as to when the WebSocket connection will open. Currently I'm creating both db client instances in the tRPC context (using the Hono tRPC adapter) - so will a WebSocket connection open every single time a procedure runs, or only when I'm actually accessing and using the WebSocket db client in my procedure? I made this gist with the files I have currently
Gist
tRPC server (running on hono with adapter) - running on Cloudflare ...
tRPC server (running on hono with adapter) - running on Cloudflare Workers - websocket connection in trpc context - context.ts
9 Replies
BeBoRE
BeBoRE•2mo ago
You can do WebSocket connections with the hono adapter? Whether tRPC keeps a connection open depends on the lazy option. If you have lazy enabled there will only be a connection when needed, otherwise it stays open.
kgni
kgni•2mo ago
I'm using the neon serverless driver to create a drizzle client which connects over websockets, I just don't know if this connection will open everytime the context is created. I don't want that to happen when/if I'm only querying the database over HTTP. I hope I'm making sense. Essentially I have: 2 different db clients created in context. As far as I know context is created every time a procedure runs. So if I create a client with a pool like this:
export const createDBWebSocket = (DATABASE_URL: string) => {
const pool = new Pool({ connectionString: DATABASE_URL });
return drizzleServerless(pool, {
schema,
logger: true,
});
};
export const createDBWebSocket = (DATABASE_URL: string) => {
const pool = new Pool({ connectionString: DATABASE_URL });
return drizzleServerless(pool, {
schema,
logger: true,
});
};
Then the connection would technically open every time the context is created right? Or is it only when I'm actually using the client? I want the connection to only open when it is used and not when context is created. It might be a question more towards neondb and drizzle (or for pools in general), sorry if that is the case i'm still a newbie 😄
BeBoRE
BeBoRE•2mo ago
Ohhh, I get it. The context is created when a request is made. Why not have to database connection be created outside of the createContext?
kgni
kgni•2mo ago
Ah I see, so you could technically split it up as follows: 1. Create the Websocket database instance in a middleware and then pass it to the context - instead of creating it directly in the context (you could also close the pool in the middleware then). 2. Recreate all of your procedures, so that you have 2 different types - 1 that utilizes HTTP and 1 for websockets, for example: - publicProcedure, protectedProcedure (both just have the HTTP database instance) - publicProcedureWebSocketets, protectedProcedureWebSockets. Would that be a way of doing it to avoid opening the websocket connection every single time the context is created? Also, would there be a smarter way of doing this, than to recreate all of the procedures with websockets (seems a bit too repetitive and redundant)
BeBoRE
BeBoRE•2mo ago
No, open the connection when the server starts? Move it outside of the createContext. Like this:
const db = createDbHTTP(DATABASE_URL);
const dbWebSockets = createDBWebSocket(DATABASE_URL);

export const createContext = async (
c: HonoContext,
DATABASE_URL: string,
AWS_ACCESS_KEY: string,
AWS_SECRET_KEY: string,
AWS_SES_REGION: string,
GITHUB_CLIENT_ID: string,
GITHUB_CLIENT_SECRET: string,
GOOGLE_CLIENT_ID: string,
GOOGLE_CLIENT_SECRET: string,
GOOGLE_REDIRECT_URI: string,
): Promise<ApiContextProps> => {
const db = createDbHTTP(DATABASE_URL);
const dbWebSockets = createDBWebSocket(DATABASE_URL);

export const createContext = async (
c: HonoContext,
DATABASE_URL: string,
AWS_ACCESS_KEY: string,
AWS_SECRET_KEY: string,
AWS_SES_REGION: string,
GITHUB_CLIENT_ID: string,
GITHUB_CLIENT_SECRET: string,
GOOGLE_CLIENT_ID: string,
GOOGLE_CLIENT_SECRET: string,
GOOGLE_REDIRECT_URI: string,
): Promise<ApiContextProps> => {
Instead of this:
export const createContext = async (
c: HonoContext,
DATABASE_URL: string,
AWS_ACCESS_KEY: string,
AWS_SECRET_KEY: string,
AWS_SES_REGION: string,
GITHUB_CLIENT_ID: string,
GITHUB_CLIENT_SECRET: string,
GOOGLE_CLIENT_ID: string,
GOOGLE_CLIENT_SECRET: string,
GOOGLE_REDIRECT_URI: string,
): Promise<ApiContextProps> => {
const db = createDbHTTP(DATABASE_URL);
const dbWebSockets = createDBWebSocket(DATABASE_URL);
const auth = createAuth(db);
export const createContext = async (
c: HonoContext,
DATABASE_URL: string,
AWS_ACCESS_KEY: string,
AWS_SECRET_KEY: string,
AWS_SES_REGION: string,
GITHUB_CLIENT_ID: string,
GITHUB_CLIENT_SECRET: string,
GOOGLE_CLIENT_ID: string,
GOOGLE_CLIENT_SECRET: string,
GOOGLE_REDIRECT_URI: string,
): Promise<ApiContextProps> => {
const db = createDbHTTP(DATABASE_URL);
const dbWebSockets = createDBWebSocket(DATABASE_URL);
const auth = createAuth(db);
kgni
kgni•2mo ago
With this implementation, wouldn't it always be open, which I don't want.. sorry for not being clear enough per neondb documentation it is advised to close the db connection after every response. So I want to open it when I need it (only in specific procedures) and close it after it is done
BeBoRE
BeBoRE•2mo ago
If you have to create the connection on every request it's best to use a procedure, since there is no way to cleanup with createContext from what I am aware of. You can use middleware too.
kgni
kgni•2mo ago
Yeah I think I'm going to do it in a middleware so I can automatically close the connection Thanks for your inputs, much appreciated! Is this implementation stupid?
const WebSocketsDb = t.middleware(async ({ next, ctx }) => {
const { dbWebSockets, pool } = createDBWebSocket(ctx.c.env.DATABASE_URL);

const res = await next({
ctx: {
...ctx,
db: dbWebSockets,
},
});

// still not sure which of these waitUntil to use since it is running in a worker
ctx.c.executionCtx.waitUntil(pool.end());
ctx.c.event.waitUntil(pool.end());

return res;
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthenticated);
export const publicProcedureWebSockets = t.procedure.use(WebSocketsDb);
export const protectedProcedureWebSockets =
protectedProcedure.use(WebSocketsDb);
const WebSocketsDb = t.middleware(async ({ next, ctx }) => {
const { dbWebSockets, pool } = createDBWebSocket(ctx.c.env.DATABASE_URL);

const res = await next({
ctx: {
...ctx,
db: dbWebSockets,
},
});

// still not sure which of these waitUntil to use since it is running in a worker
ctx.c.executionCtx.waitUntil(pool.end());
ctx.c.event.waitUntil(pool.end());

return res;
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthenticated);
export const publicProcedureWebSockets = t.procedure.use(WebSocketsDb);
export const protectedProcedureWebSockets =
protectedProcedure.use(WebSocketsDb);
Well, I spoke with a guy that works at neon, and he made me aware that "The pool is initially created empty and will create new clients lazily as they are needed.", so all of this is not necessary 🙃 The only thing I need in my case is to have a middleware that closes/ends the pool after the response.
DiamondDragon
DiamondDragon•2mo ago
What did your code end looking like?