refactor: Clean lint errors and organise files
This commit is contained in:
@@ -59,6 +59,9 @@ function buildServer(app: ReturnType<typeof createApp>, serverProtocol: ServerHt
|
||||
return createHttpServer(app);
|
||||
}
|
||||
|
||||
let listeningServer: ReturnType<typeof buildServer> | null = null;
|
||||
let staleJoinRequestInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
const variablesConfig = ensureVariablesConfig();
|
||||
const serverProtocol = getServerProtocol();
|
||||
@@ -86,10 +89,12 @@ async function bootstrap(): Promise<void> {
|
||||
const app = createApp();
|
||||
const server = buildServer(app, serverProtocol);
|
||||
|
||||
listeningServer = server;
|
||||
|
||||
setupWebSocket(server);
|
||||
|
||||
// Periodically clean up stale join requests (older than 24 h)
|
||||
setInterval(() => {
|
||||
staleJoinRequestInterval = setInterval(() => {
|
||||
deleteStaleJoinRequests(24 * 60 * 60 * 1000)
|
||||
.catch(err => console.error('Failed to clean up stale join requests:', err));
|
||||
}, 60 * 1000);
|
||||
@@ -127,8 +132,25 @@ async function gracefulShutdown(signal: string): Promise<void> {
|
||||
|
||||
shuttingDown = true;
|
||||
|
||||
if (staleJoinRequestInterval) {
|
||||
clearInterval(staleJoinRequestInterval);
|
||||
staleJoinRequestInterval = null;
|
||||
}
|
||||
|
||||
console.log(`\n[Shutdown] ${signal} received - closing database…`);
|
||||
|
||||
if (listeningServer?.listening) {
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
listeningServer?.close(() => resolve());
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Shutdown] Error closing server:', err);
|
||||
}
|
||||
}
|
||||
|
||||
listeningServer = null;
|
||||
|
||||
try {
|
||||
await destroyDatabase();
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable complexity */
|
||||
import { Router } from 'express';
|
||||
import { getKlipyApiKey, hasKlipyApiKey } from '../config/variables';
|
||||
|
||||
@@ -47,6 +46,11 @@ interface KlipyApiResponse {
|
||||
};
|
||||
}
|
||||
|
||||
interface ResolvedGifMedia {
|
||||
previewMeta: NormalizedMediaMeta | null;
|
||||
sourceMeta: NormalizedMediaMeta;
|
||||
}
|
||||
|
||||
function pickFirst<T>(...values: (T | null | undefined)[]): T | undefined {
|
||||
for (const value of values) {
|
||||
if (value != null)
|
||||
@@ -130,33 +134,49 @@ function extractKlipyResponseData(payload: unknown): { items: unknown[]; hasNext
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGifMedia(file?: KlipyGifVariants): ResolvedGifMedia | null {
|
||||
const previewVariant = pickFirst(file?.md, file?.sm, file?.xs, file?.hd);
|
||||
const sourceVariant = pickFirst(file?.hd, file?.md, file?.sm, file?.xs);
|
||||
const previewMeta = pickGifMeta(previewVariant);
|
||||
const sourceMeta = pickGifMeta(sourceVariant) ?? previewMeta;
|
||||
|
||||
if (!sourceMeta?.url)
|
||||
return null;
|
||||
|
||||
return {
|
||||
previewMeta,
|
||||
sourceMeta
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGifSlug(gifItem: KlipyGifItem): string | undefined {
|
||||
return sanitizeString(gifItem.slug) ?? sanitizeString(gifItem.id);
|
||||
}
|
||||
|
||||
function normalizeGifItem(item: unknown): NormalizedKlipyGif | null {
|
||||
if (!item || typeof item !== 'object')
|
||||
return null;
|
||||
|
||||
const gifItem = item as KlipyGifItem;
|
||||
const resolvedMedia = resolveGifMedia(gifItem.file);
|
||||
const slug = resolveGifSlug(gifItem);
|
||||
|
||||
if (gifItem.type === 'ad')
|
||||
return null;
|
||||
|
||||
const lowVariant = pickFirst(gifItem.file?.md, gifItem.file?.sm, gifItem.file?.xs, gifItem.file?.hd);
|
||||
const highVariant = pickFirst(gifItem.file?.hd, gifItem.file?.md, gifItem.file?.sm, gifItem.file?.xs);
|
||||
const lowMeta = pickGifMeta(lowVariant);
|
||||
const highMeta = pickGifMeta(highVariant);
|
||||
const selectedMeta = highMeta ?? lowMeta;
|
||||
const slug = sanitizeString(gifItem.slug) ?? sanitizeString(gifItem.id);
|
||||
|
||||
if (!slug || !selectedMeta?.url)
|
||||
if (!slug || !resolvedMedia)
|
||||
return null;
|
||||
|
||||
const { previewMeta, sourceMeta } = resolvedMedia;
|
||||
|
||||
return {
|
||||
id: slug,
|
||||
slug,
|
||||
title: sanitizeString(gifItem.title),
|
||||
url: selectedMeta.url,
|
||||
previewUrl: lowMeta?.url ?? selectedMeta.url,
|
||||
width: selectedMeta.width ?? lowMeta?.width ?? 0,
|
||||
height: selectedMeta.height ?? lowMeta?.height ?? 0
|
||||
url: sourceMeta.url,
|
||||
previewUrl: previewMeta?.url ?? sourceMeta.url,
|
||||
width: sourceMeta.width ?? previewMeta?.width ?? 0,
|
||||
height: sourceMeta.height ?? previewMeta?.height ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,19 @@ function createConnectedUser(
|
||||
return user;
|
||||
}
|
||||
|
||||
function getRequiredConnectedUser(connectionId: string): ConnectedUser {
|
||||
const connectedUser = connectedUsers.get(connectionId);
|
||||
|
||||
if (!connectedUser)
|
||||
throw new Error(`Expected connected user for ${connectionId}`);
|
||||
|
||||
return connectedUser;
|
||||
}
|
||||
|
||||
function getSentMessagesStore(user: ConnectedUser): { sentMessages: string[] } {
|
||||
return user.ws as unknown as { sentMessages: string[] };
|
||||
}
|
||||
|
||||
describe('server websocket handler - status_update', () => {
|
||||
beforeEach(() => {
|
||||
connectedUsers.clear();
|
||||
@@ -68,27 +81,24 @@ describe('server websocket handler - status_update', () => {
|
||||
|
||||
await handleWebSocketMessage('conn-1', { type: 'status_update', status: 'busy' });
|
||||
|
||||
const ws2 = user2.ws as unknown as { sentMessages: string[] };
|
||||
const messages = ws2.sentMessages.map((m: string) => JSON.parse(m));
|
||||
const statusMsg = messages.find((m: { type: string }) => m.type === 'status_update');
|
||||
const messages = getSentMessagesStore(user2).sentMessages.map((messageText: string) => JSON.parse(messageText));
|
||||
const statusMsg = messages.find((message: { type: string }) => message.type === 'status_update');
|
||||
|
||||
expect(statusMsg).toBeDefined();
|
||||
expect(statusMsg.oderId).toBe('user-1');
|
||||
expect(statusMsg.status).toBe('busy');
|
||||
expect(statusMsg?.oderId).toBe('user-1');
|
||||
expect(statusMsg?.status).toBe('busy');
|
||||
});
|
||||
|
||||
it('does not broadcast to users in different servers', async () => {
|
||||
createConnectedUser('conn-1', 'user-1');
|
||||
const user2 = createConnectedUser('conn-2', 'user-2');
|
||||
|
||||
connectedUsers.get('conn-1')!.serverIds.add('server-1');
|
||||
getRequiredConnectedUser('conn-1').serverIds.add('server-1');
|
||||
user2.serverIds.add('server-2');
|
||||
|
||||
await handleWebSocketMessage('conn-1', { type: 'status_update', status: 'away' });
|
||||
|
||||
const ws2 = user2.ws as unknown as { sentMessages: string[] };
|
||||
|
||||
expect(ws2.sentMessages.length).toBe(0);
|
||||
expect(getSentMessagesStore(user2).sentMessages.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ignores invalid status values', async () => {
|
||||
@@ -134,22 +144,21 @@ describe('server websocket handler - status_update', () => {
|
||||
await handleWebSocketMessage('conn-1', { type: 'status_update', status: 'away' });
|
||||
|
||||
// Clear sent messages
|
||||
(user2.ws as unknown as { sentMessages: string[] }).sentMessages.length = 0;
|
||||
getSentMessagesStore(user2).sentMessages.length = 0;
|
||||
|
||||
// Identify first (required for handler)
|
||||
await handleWebSocketMessage('conn-1', { type: 'identify', oderId: 'user-1', displayName: 'User 1' });
|
||||
|
||||
// user-2 joins server → should receive server_users with user-1's status
|
||||
(user2.ws as unknown as { sentMessages: string[] }).sentMessages.length = 0;
|
||||
getSentMessagesStore(user2).sentMessages.length = 0;
|
||||
await handleWebSocketMessage('conn-2', { type: 'join_server', serverId: 'server-1' });
|
||||
|
||||
const ws2 = user2.ws as unknown as { sentMessages: string[] };
|
||||
const messages = ws2.sentMessages.map((m: string) => JSON.parse(m));
|
||||
const serverUsersMsg = messages.find((m: { type: string }) => m.type === 'server_users');
|
||||
const messages = getSentMessagesStore(user2).sentMessages.map((messageText: string) => JSON.parse(messageText));
|
||||
const serverUsersMsg = messages.find((message: { type: string }) => message.type === 'server_users');
|
||||
|
||||
expect(serverUsersMsg).toBeDefined();
|
||||
|
||||
const user1InList = serverUsersMsg.users.find((u: { oderId: string }) => u.oderId === 'user-1');
|
||||
const user1InList = serverUsersMsg?.users?.find((userEntry: { oderId: string }) => userEntry.oderId === 'user-1');
|
||||
|
||||
expect(user1InList?.status).toBe('away');
|
||||
});
|
||||
@@ -168,19 +177,18 @@ describe('server websocket handler - user_joined includes status', () => {
|
||||
user2.serverIds.add('server-1');
|
||||
|
||||
// Set user-1's status to busy before joining
|
||||
connectedUsers.get('conn-1')!.status = 'busy';
|
||||
getRequiredConnectedUser('conn-1').status = 'busy';
|
||||
|
||||
// Identify user-1
|
||||
await handleWebSocketMessage('conn-1', { type: 'identify', oderId: 'user-1', displayName: 'User 1' });
|
||||
|
||||
(user2.ws as unknown as { sentMessages: string[] }).sentMessages.length = 0;
|
||||
getSentMessagesStore(user2).sentMessages.length = 0;
|
||||
|
||||
// user-1 joins server-1
|
||||
await handleWebSocketMessage('conn-1', { type: 'join_server', serverId: 'server-1' });
|
||||
|
||||
const ws2 = user2.ws as unknown as { sentMessages: string[] };
|
||||
const messages = ws2.sentMessages.map((m: string) => JSON.parse(m));
|
||||
const joinMsg = messages.find((m: { type: string }) => m.type === 'user_joined');
|
||||
const messages = getSentMessagesStore(user2).sentMessages.map((messageText: string) => JSON.parse(messageText));
|
||||
const joinMsg = messages.find((message: { type: string }) => message.type === 'user_joined');
|
||||
|
||||
// user_joined may or may not appear depending on whether it's a new identity membership
|
||||
// Since both are already in the server, it may not broadcast. Either way, verify no crash.
|
||||
|
||||
Reference in New Issue
Block a user