feat: signal server tag

This commit is contained in:
2026-06-05 06:16:02 +02:00
parent 6865147e8f
commit bf4e6891d1
69 changed files with 2808 additions and 1269 deletions

View File

@@ -68,11 +68,19 @@ describe('server websocket handler - status_update', () => {
});
it('treats signaling keepalive messages as connection liveness', async () => {
createConnectedUser('conn-1', 'user-1', { lastPong: 1 });
const user = createConnectedUser('conn-1', 'user-1', { lastPong: 1 });
await handleWebSocketMessage('conn-1', { type: 'keepalive' });
expect(connectedUsers.get('conn-1')?.lastPong).toBeGreaterThan(1);
const sentMessages = (user.ws as WebSocket & { sentMessages: string[] }).sentMessages;
expect(sentMessages).toHaveLength(1);
const ack = JSON.parse(sentMessages[0]) as { type: string; serverTime: number };
expect(ack.type).toBe('keepalive_ack');
expect(ack.serverTime).toEqual(expect.any(Number));
});
it('updates user status on valid status_update message', async () => {
@@ -276,6 +284,34 @@ describe('server websocket handler - profile metadata in presence messages', ()
expect(aliceInList?.profileUpdatedAt).toBe(123);
});
it('includes homeSignalServerUrl in server_users responses', async () => {
const alice = createConnectedUser('conn-1', 'user-1');
const bob = createConnectedUser('conn-2', 'user-2');
alice.serverIds.add('server-1');
bob.serverIds.add('server-1');
await handleWebSocketMessage('conn-1', {
type: 'identify',
oderId: 'user-1',
displayName: 'Alice',
homeSignalServerUrl: 'http://signal.example.com:3001/'
});
getSentMessagesStore(bob).sentMessages.length = 0;
await handleWebSocketMessage('conn-2', {
type: 'view_server',
serverId: 'server-1'
});
const messages = getSentMessagesStore(bob).sentMessages.map((messageText: string) => JSON.parse(messageText));
const serverUsersMsg = messages.find((message: { type: string }) => message.type === 'server_users');
const aliceInList = serverUsersMsg?.users?.find((userEntry: { oderId: string }) => userEntry.oderId === 'user-1');
expect(aliceInList?.homeSignalServerUrl).toBe('http://signal.example.com:3001');
});
it('includes description and profileUpdatedAt in user_joined broadcasts', async () => {
const bob = createConnectedUser('conn-2', 'user-2');

View File

@@ -39,6 +39,34 @@ function normalizeProfileUpdatedAt(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
}
function normalizeHomeSignalServerUrl(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const normalized = value.trim().replace(/\/+$/, '');
return normalized || undefined;
}
function buildPresenceUserPayload(user: ConnectedUser): {
oderId: string;
displayName: string;
description?: string;
profileUpdatedAt?: number;
homeSignalServerUrl?: string;
status: 'online' | 'away' | 'busy' | 'offline';
} {
return {
oderId: user.oderId,
displayName: normalizeDisplayName(user.displayName),
description: user.description,
profileUpdatedAt: user.profileUpdatedAt,
homeSignalServerUrl: user.homeSignalServerUrl,
status: user.status ?? 'online'
};
}
function readMessageId(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
@@ -82,13 +110,7 @@ function sendPluginError(user: ConnectedUser, error: unknown, message: WsMessage
/** Sends the current user list for a given server to a single connected user. */
function sendServerUsers(user: ConnectedUser, serverId: string): void {
const users = getUniqueUsersInServer(serverId, user.oderId).map((cu) => ({
oderId: cu.oderId,
displayName: normalizeDisplayName(cu.displayName),
description: cu.description,
profileUpdatedAt: cu.profileUpdatedAt,
status: cu.status ?? 'online'
}));
const users = getUniqueUsersInServer(serverId, user.oderId).map((cu) => buildPresenceUserPayload(cu));
user.ws.send(JSON.stringify({ type: 'server_users', serverId, users }));
}
@@ -115,6 +137,7 @@ function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: s
const previousDisplayName = normalizeDisplayName(user.displayName);
const previousDescription = user.description;
const previousProfileUpdatedAt = user.profileUpdatedAt;
const previousHomeSignalServerUrl = user.homeSignalServerUrl;
user.oderId = newOderId;
user.displayName = normalizeDisplayName(message['displayName'], normalizeDisplayName(user.displayName));
@@ -127,11 +150,20 @@ function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: s
user.profileUpdatedAt = normalizeProfileUpdatedAt(message['profileUpdatedAt']);
}
if (Object.prototype.hasOwnProperty.call(message, 'homeSignalServerUrl')) {
user.homeSignalServerUrl = normalizeHomeSignalServerUrl(message['homeSignalServerUrl']);
}
user.connectionScope = newScope;
connectedUsers.set(connectionId, user);
console.log(`User identified: ${user.displayName} (${user.oderId})`);
if (user.displayName === previousDisplayName && user.description === previousDescription && user.profileUpdatedAt === previousProfileUpdatedAt) {
if (
user.displayName === previousDisplayName
&& user.description === previousDescription
&& user.profileUpdatedAt === previousProfileUpdatedAt
&& user.homeSignalServerUrl === previousHomeSignalServerUrl
) {
return;
}
@@ -140,11 +172,7 @@ function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: s
serverId,
{
type: 'user_joined',
oderId: user.oderId,
displayName: normalizeDisplayName(user.displayName),
description: user.description,
profileUpdatedAt: user.profileUpdatedAt,
status: user.status ?? 'online',
...buildPresenceUserPayload(user),
serverId
},
user.oderId
@@ -191,11 +219,7 @@ async function handleJoinServer(user: ConnectedUser, message: WsMessage, connect
sid,
{
type: 'user_joined',
oderId: user.oderId,
displayName: normalizeDisplayName(user.displayName),
description: user.description,
profileUpdatedAt: user.profileUpdatedAt,
status: user.status ?? 'online',
...buildPresenceUserPayload(user),
serverId: sid
},
user.oderId
@@ -460,6 +484,7 @@ export async function handleWebSocketMessage(connectionId: string, message: WsMe
switch (message.type) {
case 'keepalive':
user.ws.send(JSON.stringify({ type: 'keepalive_ack', serverTime: Date.now() }));
break;
case 'identify':

View File

@@ -15,6 +15,8 @@ export interface ConnectedUser {
* URLs routing to the same server coexist without an eviction loop.
*/
connectionScope?: string;
/** Public signal-server URL the user registered on. */
homeSignalServerUrl?: string;
/** User availability status (online, away, busy, offline). */
status?: 'online' | 'away' | 'busy' | 'offline';
/** Latest server icon timestamp this connection can provide over P2P. */