Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions __tests__/mongoClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @jest-environment node */
import { getClient, resetClient } from '@utils/mongodb/mongoClient.mjs';
import { MongoClient } from 'mongodb';

describe('getClient', () => {
beforeEach(async () => {
await resetClient();
jest.restoreAllMocks();
});

it('should throw an error if MONGODB_URI is missing', async () => {
const originalUri = process.env.MONGODB_URI;
delete process.env.MONGODB_URI;

await resetClient();

await expect(getClient()).rejects.toThrow(
'Missing MONGODB_URI environment variable.'
);

process.env.MONGODB_URI = originalUri;
});

it('should return the same instance on multiple calls', async () => {
const mockDb = { db: jest.fn() };
const spy = jest
.spyOn(MongoClient.prototype, 'connect')
.mockResolvedValue(mockDb as any);

Comment thread
michelleyeoh marked this conversation as resolved.
Comment thread
michelleyeoh marked this conversation as resolved.
const c1 = await getClient();
const c2 = await getClient();

expect(c1).toBe(c2);
expect(spy).toHaveBeenCalledTimes(1);
});
Comment thread
michelleyeoh marked this conversation as resolved.

it('should dedupe concurrent callers using cachedPromise', async () => {
const mockDb = { db: jest.fn() };
const spy = jest
.spyOn(MongoClient.prototype, 'connect')
.mockResolvedValue(mockDb as any);
const [c1, c2] = await Promise.all([getClient(), getClient()]);
expect(c1).toBe(c2);
expect(spy).toHaveBeenCalledTimes(1);
});

it('should retry after a failed connection', async () => {
const spy = jest
.spyOn(MongoClient.prototype, 'connect')
.mockRejectedValueOnce(new Error('Network Fail'))
.mockResolvedValueOnce({ db: jest.fn() } as any);

Comment thread
michelleyeoh marked this conversation as resolved.
await expect(getClient()).rejects.toThrow('Network Fail');

await expect(getClient()).resolves.toBeDefined();
expect(spy).toHaveBeenCalledTimes(2);
});
});
34 changes: 30 additions & 4 deletions app/(api)/_utils/mongodb/mongoClient.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
import { MongoClient } from 'mongodb';

const uri = process.env.MONGODB_URI;
let cachedClient = null;
let cachedPromise = null;

export async function getClient() {
const uri = process.env.MONGODB_URI;
if (!uri) {
throw new Error('Missing MONGODB_URI environment variable.');
}
Comment thread
michelleyeoh marked this conversation as resolved.

if (cachedClient) {
return cachedClient;
}
const client = new MongoClient(uri);
cachedClient = client;
return cachedClient;

if (!cachedPromise) {
const client = new MongoClient(uri);
cachedPromise = client
.connect()
.then((connectedClient) => {
cachedClient = connectedClient;
return connectedClient;
})
.catch((error) => {
client.close().catch(() => {});
Comment on lines +24 to +25
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.close() is kicked off but not awaited in the connect error handler. That means a retry can start while the previous client is still closing, which can leak sockets / create extra connections under failure conditions. Consider making this .catch handler async and awaiting client.close() (or await client?.close() if you refactor) before clearing the cached state and rethrowing.

Suggested change
.catch((error) => {
client.close().catch(() => {});
.catch(async (error) => {
try {
await client.close();
} catch {
// Ignore errors while closing the client
}

Copilot uses AI. Check for mistakes.
cachedPromise = null;
cachedClient = null;
throw error;
});
Comment thread
michelleyeoh marked this conversation as resolved.
}

return cachedPromise;
}

// Helper function for testing
export async function resetClient() {
cachedClient = null;
cachedPromise = null;
}

export async function getDatabase() {
Expand Down