hedgedoc/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts
Erik Michelson 68780f54e1
wip: range authorships backend storage
Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2024-04-28 16:48:54 +02:00

158 lines
4.8 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
Message,
MessageTransporter,
MessageType,
RealtimeUser,
} from '@hedgedoc/commons';
import { Listener } from 'eventemitter2';
import { Username } from '../../utils/username';
export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[];
/**
* Saves the current realtime status of a specific client and sends updates of changes to other clients.
*/
export class RealtimeUserStatusAdapter {
private readonly realtimeUser: RealtimeUser;
constructor(
private readonly username: Username | null,
private readonly displayName: string,
private collectOtherAdapters: OtherAdapterCollector,
private messageTransporter: MessageTransporter,
private acceptCursorUpdateProvider: () => boolean,
) {
this.realtimeUser = this.createInitialRealtimeUserState();
this.bindRealtimeUserStateEvents();
}
private createInitialRealtimeUserState(): RealtimeUser {
return {
username: this.username,
displayName: this.displayName,
active: true,
styleIndex: this.findLeastUsedStyleIndex(
this.createStyleIndexToCountMap(),
),
cursor: !this.acceptCursorUpdateProvider()
? null
: {
from: 0,
to: 0,
},
};
}
private bindRealtimeUserStateEvents(): void {
const transporterMessagesListener = this.messageTransporter.on(
MessageType.REALTIME_USER_SINGLE_UPDATE,
(message: Message<MessageType.REALTIME_USER_SINGLE_UPDATE>) => {
this.realtimeUser.cursor = this.acceptCursorUpdateProvider()
? message.payload
: null;
this.collectOtherAdapters()
.filter((adapter) => adapter !== this)
.forEach((adapter) => adapter.sendCompleteStateToClient());
},
{ objectify: true },
) as Listener;
const transporterRequestMessageListener = this.messageTransporter.on(
MessageType.REALTIME_USER_STATE_REQUEST,
() => {
this.sendCompleteStateToClient();
},
{ objectify: true },
) as Listener;
const clientRemoveListener = this.messageTransporter.on(
'disconnected',
() => {
this.collectOtherAdapters()
.filter((adapter) => adapter !== this)
.forEach((adapter) => adapter.sendCompleteStateToClient());
},
{
objectify: true,
},
) as Listener;
const realtimeUserSetActivityListener = this.messageTransporter.on(
MessageType.REALTIME_USER_SET_ACTIVITY,
(message: Message<MessageType.REALTIME_USER_SET_ACTIVITY>) => {
if (this.realtimeUser.active === message.payload.active) {
return;
}
this.realtimeUser.active = message.payload.active;
this.collectOtherAdapters()
.filter((adapter) => adapter !== this)
.forEach((adapter) => adapter.sendCompleteStateToClient());
},
{ objectify: true },
) as Listener;
this.messageTransporter.on('disconnected', () => {
transporterMessagesListener?.off();
transporterRequestMessageListener.off();
clientRemoveListener.off();
realtimeUserSetActivityListener.off();
});
}
private getSendableState(): RealtimeUser | undefined {
return this.messageTransporter.isReady() ? this.realtimeUser : undefined;
}
public sendCompleteStateToClient(): void {
if (!this.messageTransporter.isReady()) {
return;
}
const realtimeUsers = this.collectOtherAdapters()
.filter((adapter) => adapter !== this)
.map((adapter) => adapter.getSendableState())
.filter((value) => value !== undefined) as RealtimeUser[];
this.messageTransporter.sendMessage({
type: MessageType.REALTIME_USER_STATE_SET,
payload: {
users: realtimeUsers,
ownUser: {
displayName: this.realtimeUser.displayName,
styleIndex: this.realtimeUser.styleIndex,
username: this.realtimeUser.username,
},
},
});
}
private findLeastUsedStyleIndex(map: Map<number, number>): number {
let leastUsedStyleIndex = 0;
let leastUsedStyleIndexCount = map.get(0) ?? 0;
for (let styleIndex = 0; styleIndex < 8; styleIndex++) {
const count = map.get(styleIndex) ?? 0;
if (count < leastUsedStyleIndexCount) {
leastUsedStyleIndexCount = count;
leastUsedStyleIndex = styleIndex;
}
}
return leastUsedStyleIndex;
}
private createStyleIndexToCountMap(): Map<number, number> {
return this.collectOtherAdapters()
.map((adapter) => adapter.realtimeUser.styleIndex)
.reduce((map, styleIndex) => {
if (styleIndex !== undefined) {
const count = (map.get(styleIndex) ?? 0) + 1;
map.set(styleIndex, count);
}
return map;
}, new Map<number, number>());
}
}