feat: signal server tag
This commit is contained in:
@@ -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');
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user