Compare commits

..

1 Commits

Author SHA1 Message Date
Timo K
c45f6ab23f fix multi subscription to display name ambiguity map.
This results in the map being created proportionally to the number of members in the call. Each change in memberships will call multiple resubscription to the map observable.

Signed-off-by: Timo K <toger5@hotmail.de>
2025-03-06 22:50:34 +01:00
13 changed files with 120 additions and 164 deletions

View File

@@ -70,8 +70,7 @@
"livekit_sfu": "LiveKit SFU: {{url}}",
"matrix_id": "Matrix ID: {{id}}",
"show_connection_stats": "Show connection statistics",
"show_non_member_tiles": "Show tiles for non-member media",
"use_new_membership_manager": "Use the new implementation of the call MembershipManager"
"show_non_member_tiles": "Show tiles for non-member media"
},
"disconnected_banner": "Connectivity to the server has been lost.",
"error": {

View File

@@ -91,7 +91,7 @@
"livekit-client": "^2.5.7",
"lodash-es": "^4.17.21",
"loglevel": "^1.9.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#0cdb07544d9926cf0855a76ca5cc7dab253bdb24",
"matrix-js-sdk": "^36.1.0",
"matrix-widget-api": "1.11.0",
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",

View File

@@ -16,15 +16,12 @@ import {
} from "react";
import { type MatrixClient } from "matrix-js-sdk/src/client";
import {
Room as LivekitRoom,
isE2EESupported as isE2EESupportedBrowser,
Room,
} from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger";
import {
MatrixRTCSessionEvent,
type MatrixRTCSession,
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import {
OfflineIcon,
WebBrowserIcon,
@@ -69,14 +66,8 @@ import {
ElementCallError,
ErrorCategory,
ErrorCode,
RTCSessionError,
} from "../utils/errors.ts";
import { ElementCallRichError } from "../RichError.tsx";
import {
useNewMembershipManagerSetting as useNewMembershipManagerSetting,
useSetting,
} from "../settings/settings";
import { useTypedEventEmitter } from "../useEvents";
declare global {
interface Window {
@@ -135,18 +126,6 @@ export const GroupCallView: FC<Props> = ({
};
}, [rtcSession]);
useTypedEventEmitter(
rtcSession,
MatrixRTCSessionEvent.MembershipManagerError,
(error) => {
setError(
new RTCSessionError(
ErrorCode.MEMBERSHIP_MANAGER_UNRECOVERABLE,
error.message ?? error,
),
);
},
);
useEffect(() => {
// Sanity check the room object
if (client.getRoom(rtcSession.room.roomId) !== rtcSession.room)
@@ -155,14 +134,11 @@ export const GroupCallView: FC<Props> = ({
);
}, [client, rtcSession.room]);
const room = rtcSession.room as Room;
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(room);
const roomAvatar = useRoomAvatar(room);
const roomName = useRoomName(rtcSession.room);
const roomAvatar = useRoomAvatar(rtcSession.room);
const { perParticipantE2EE, returnToLobby } = useUrlParams();
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
usePageTitle(roomName);
const matrixInfo = useMemo((): MatrixInfo => {
@@ -170,13 +146,21 @@ export const GroupCallView: FC<Props> = ({
userId: client.getUserId()!,
displayName: displayName!,
avatarUrl: avatarUrl!,
roomId: room.roomId,
roomId: rtcSession.room.roomId,
roomName,
roomAlias: room.getCanonicalAlias(),
roomAlias: rtcSession.room.getCanonicalAlias(),
roomAvatar,
e2eeSystem,
};
}, [client, displayName, avatarUrl, roomName, room, roomAvatar, e2eeSystem]);
}, [
client,
displayName,
avatarUrl,
rtcSession.room,
roomName,
roomAvatar,
e2eeSystem,
]);
// Count each member only once, regardless of how many devices they use
const participantCount = useMemo(
@@ -191,18 +175,13 @@ export const GroupCallView: FC<Props> = ({
const enterRTCSessionOrError = async (
rtcSession: MatrixRTCSession,
perParticipantE2EE: boolean,
newMembershipManager: boolean,
): Promise<void> => {
try {
await enterRTCSession(
rtcSession,
perParticipantE2EE,
newMembershipManager,
);
await enterRTCSession(rtcSession, perParticipantE2EE);
} catch (e) {
if (e instanceof ElementCallError) {
// e.code === ErrorCode.MISSING_LIVE_KIT_SERVICE_URL)
setError(e);
setEnterRTCError(e);
} else {
logger.error(`Unknown Error while entering RTC session`, e);
const error = new ElementCallError(
@@ -210,7 +189,7 @@ export const GroupCallView: FC<Props> = ({
ErrorCode.UNKNOWN_ERROR,
ErrorCategory.UNKNOWN,
);
setError(error);
setEnterRTCError(error);
}
}
};
@@ -224,7 +203,7 @@ export const GroupCallView: FC<Props> = ({
// permissions and give you device names unless you specify a kind, but
// here we want all kinds of devices. This needs a fix in livekit-client
// for the following name-matching logic to do anything useful.
const devices = await LivekitRoom.getLocalDevices(undefined, true);
const devices = await Room.getLocalDevices(undefined, true);
if (audioInput) {
const deviceId = findDeviceByName(audioInput, "audioinput", devices);
@@ -264,11 +243,7 @@ export const GroupCallView: FC<Props> = ({
await defaultDeviceSetup(
ev.detail.data as unknown as JoinCallData,
);
await enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
);
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
widget.api.transport.reply(ev.detail, {});
})().catch((e) => {
logger.error("Error joining RTC session", e);
@@ -281,21 +256,13 @@ export const GroupCallView: FC<Props> = ({
} else {
// No lobby and no preload: we enter the rtc session right away
(async (): Promise<void> => {
await enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
);
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
})().catch((e) => {
logger.error("Error joining RTC session", e);
});
}
} else {
void enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
);
void enterRTCSessionOrError(rtcSession, perParticipantE2EE);
}
}
}, [
@@ -306,11 +273,12 @@ export const GroupCallView: FC<Props> = ({
perParticipantE2EE,
latestDevices,
latestMuteStates,
useNewMembershipManager,
]);
const [left, setLeft] = useState(false);
const [error, setError] = useState<ElementCallError | null>(null);
const [enterRTCError, setEnterRTCError] = useState<ElementCallError | null>(
null,
);
const navigate = useNavigate();
const onLeave = useCallback(
@@ -324,7 +292,7 @@ export const GroupCallView: FC<Props> = ({
// Otherwise the iFrame gets killed before the callEnded event got tracked.
const posthogRequest = new Promise((resolve) => {
PosthogAnalytics.instance.eventCallEnded.track(
room.roomId,
rtcSession.room.roomId,
rtcSession.memberships.length,
sendInstantly,
rtcSession,
@@ -353,12 +321,11 @@ export const GroupCallView: FC<Props> = ({
});
},
[
leaveSoundContext,
widget,
rtcSession,
room.roomId,
isPasswordlessUser,
confineToRoom,
leaveSoundContext,
navigate,
],
);
@@ -384,7 +351,7 @@ export const GroupCallView: FC<Props> = ({
}
}, [widget, isJoined, rtcSession]);
const joinRule = useJoinRule(room);
const joinRule = useJoinRule(rtcSession.room);
const [shareModalOpen, setInviteModalOpen] = useState(false);
const onDismissInviteModal = useCallback(
@@ -412,12 +379,8 @@ export const GroupCallView: FC<Props> = ({
const onReconnect = useCallback(() => {
setLeft(false);
resetError();
enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
).catch((e) => {
logger.error("Error re-entering RTC session on reconnect", e);
enterRTCSessionOrError(rtcSession, perParticipantE2EE).catch((e) => {
logger.error("Error re-entering RTC session", e);
});
}, [resetError]);
@@ -439,7 +402,7 @@ export const GroupCallView: FC<Props> = ({
);
}
return GroupCallErrorPage;
}, [t, rtcSession, onLeave, perParticipantE2EE, useNewMembershipManager]);
}, [onLeave, rtcSession, perParticipantE2EE, t]);
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
// If we have a encryption system but the browser does not support it.
@@ -454,7 +417,7 @@ export const GroupCallView: FC<Props> = ({
const shareModal = (
<InviteModal
room={room}
room={rtcSession.room}
open={shareModalOpen}
onDismiss={onDismissInviteModal}
/>
@@ -467,11 +430,7 @@ export const GroupCallView: FC<Props> = ({
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() =>
void enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
)
void enterRTCSessionOrError(rtcSession, perParticipantE2EE)
}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
@@ -482,11 +441,11 @@ export const GroupCallView: FC<Props> = ({
);
let body: ReactNode;
if (error) {
if (enterRTCError) {
// If an ElementCallError was recorded, then create a component that will fail to render and throw
// an ElementCallRichError error. This will then be handled by the ErrorBoundary component.
const ErrorComponent = (): ReactNode => {
throw new ElementCallRichError(error);
throw new ElementCallRichError(enterRTCError);
};
body = <ErrorComponent />;
} else if (isJoined) {

View File

@@ -111,7 +111,6 @@ test("It joins the correct Session", async () => {
{
manageMediaKeys: false,
useLegacyMemberEvents: false,
useNewMembershipManager: true,
},
);
});

View File

@@ -97,7 +97,6 @@ async function makePreferredLivekitFoci(
export async function enterRTCSession(
rtcSession: MatrixRTCSession,
encryptMedia: boolean,
useNewMembershipManager = true,
): Promise<void> {
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
@@ -115,7 +114,6 @@ export async function enterRTCSession(
await makePreferredLivekitFoci(rtcSession, livekitAlias),
makeActiveFocus(),
{
useNewMembershipManager,
manageMediaKeys: encryptMedia,
...(useDeviceSessionMemberEvents !== undefined && {
useLegacyMemberEvents: !useDeviceSessionMemberEvents,

View File

@@ -15,7 +15,6 @@ import {
debugTileLayout as debugTileLayoutSetting,
showNonMemberTiles as showNonMemberTilesSetting,
showConnectionStats as showConnectionStatsSetting,
useNewMembershipManagerSetting,
} from "./settings";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room as LivekitRoom } from "livekit-client";
@@ -39,10 +38,6 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRoom }) => {
showConnectionStatsSetting,
);
const [useNewMembershipManager, setNewMembershipManager] = useSetting(
useNewMembershipManagerSetting,
);
const sfuUrl = useMemo((): URL | null => {
if (livekitRoom?.engine.client.ws?.url) {
// strip the URL params
@@ -139,20 +134,6 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRoom }) => {
)}
/>
</FieldRow>
<FieldRow>
<InputField
id="useNewMembershipManager"
type="checkbox"
label={t("developer_mode.use_new_membership_manager")}
checked={!!useNewMembershipManager}
onChange={useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
setNewMembershipManager(event.target.checked);
},
[setNewMembershipManager],
)}
/>
</FieldRow>
{livekitRoom ? (
<>
<p>

View File

@@ -113,8 +113,4 @@ export const soundEffectVolumeSetting = new Setting<number>(
0.5,
);
export const useNewMembershipManagerSetting = new Setting<boolean>(
"new-membership-manager",
true,
);
export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);

View File

@@ -9,10 +9,10 @@ import { type ComponentProps, useCallback, useEffect, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import {
ClientEvent,
type Crypto,
type MatrixClient,
type MatrixEvent,
} from "matrix-js-sdk/src/matrix";
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { getLogsForReport } from "./rageshake";
import { useClient } from "../ClientContext";
@@ -34,7 +34,7 @@ const gzip = async (text: string): Promise<Blob> => {
* Collects crypto related information.
*/
async function collectCryptoInfo(
cryptoApi: CryptoApi,
cryptoApi: Crypto.CryptoApi,
body: FormData,
): Promise<void> {
body.append("crypto_version", cryptoApi.getVersion());
@@ -82,7 +82,7 @@ async function collectCryptoInfo(
*/
async function collectRecoveryInfo(
client: MatrixClient,
cryptoApi: CryptoApi,
cryptoApi: Crypto.CryptoApi,
body: FormData,
): Promise<void> {
const secretStorage = client.secretStorage;

View File

@@ -69,7 +69,11 @@ import {
ScreenShareViewModel,
type UserMediaViewModel,
} from "./MediaViewModel";
import { accumulate, finalizeValue } from "../utils/observable";
import {
accumulate,
finalizeValue,
singleSubscriberSubject$,
} from "../utils/observable";
import { ObservableScope } from "./ObservableScope";
import {
duplicateTiles,
@@ -316,7 +320,7 @@ class UserMedia {
newParticipant: LocalParticipant | RemoteParticipant | undefined,
): void {
if (this.participant$.value !== newParticipant) {
// Update the BehaviourSubject in the UserMedia.
// Update the BehaviorSubject in the UserMedia.
this.participant$.next(newParticipant);
}
}
@@ -469,37 +473,40 @@ export class CallViewModel extends ViewModel {
* any displaynames that clashes with another member. Only members
* joined to the call are considered here.
*/
public readonly memberDisplaynames$ = merge(
// Handle call membership changes.
fromEvent(this.matrixRTCSession, MatrixRTCSessionEvent.MembershipsChanged),
// Handle room membership changes (and displayname updates)
fromEvent(this.matrixRTCSession.room, RoomStateEvent.Members),
).pipe(
startWith(null),
map(() => {
const displaynameMap = new Map<string, string>();
const { room, memberships } = this.matrixRTCSession;
// eslint-disable-next-line rxjs/no-exposed-subjects
public readonly memberDisplaynames$ = singleSubscriberSubject$(
merge(
fromEvent(this.matrixRTCSession.room, RoomStateEvent.Members),
fromEvent(
this.matrixRTCSession,
MatrixRTCSessionEvent.MembershipsChanged,
),
).pipe(
startWith(null),
map(() => {
const displaynameMap = new Map<string, string>();
const { room, memberships } = this.matrixRTCSession;
// We only consider RTC members for disambiguation as they are the only visible members.
for (const rtcMember of memberships) {
const matrixIdentifier = `${rtcMember.sender}:${rtcMember.deviceId}`;
const { member } = getRoomMemberFromRtcMember(rtcMember, room);
if (!member) {
logger.error("Could not find member for media id:", matrixIdentifier);
continue;
// We only consider RTC members for disambiguation as they are the only visible members.
for (const rtcMember of memberships) {
const matrixIdentifier = `${rtcMember.sender}:${rtcMember.deviceId}`;
const { member } = getRoomMemberFromRtcMember(rtcMember, room);
if (!member) {
logger.error(
"Could not find member for media id:",
matrixIdentifier,
);
continue;
}
const disambiguate = shouldDisambiguate(member, memberships, room);
displaynameMap.set(
matrixIdentifier,
calculateDisplayName(member, disambiguate),
);
}
const disambiguate = shouldDisambiguate(member, memberships, room);
displaynameMap.set(
matrixIdentifier,
calculateDisplayName(member, disambiguate),
);
}
return displaynameMap;
}),
// It turns out that doing the disambiguation above is rather expensive on Safari (10x slower
// than on Chrome/Firefox). This means it is important that we share() the result so that we
// don't do this work more times than we need to. This is achieve through the state() operator:
this.scope.state(),
return displaynameMap;
}),
),
);
/**
@@ -573,8 +580,8 @@ export class CallViewModel extends ViewModel {
yield [
indexedMediaId,
// We create UserMedia with or without a participant.
// This will be the initial value of a BehaviourSubject.
// Once a participant appears we will update the BehaviourSubject. (see above)
// This will be the initial value of a BehaviorSubject.
// Once a participant appears we will update the BehaviorSubject. (see above)
prevMedia ??
new UserMedia(
indexedMediaId,
@@ -583,7 +590,7 @@ export class CallViewModel extends ViewModel {
this.encryptionSystem,
this.livekitRoom,
this.memberDisplaynames$.pipe(
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
map((m) => m?.get(matrixIdentifier) ?? "[👻]"),
),
this.handsRaised$.pipe(
map((v) => v[matrixIdentifier]?.time ?? null),
@@ -606,7 +613,7 @@ export class CallViewModel extends ViewModel {
this.encryptionSystem,
this.livekitRoom,
this.memberDisplaynames$.pipe(
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
map((m) => m?.get(matrixIdentifier) ?? "[👻]"),
),
),
];
@@ -647,7 +654,9 @@ export class CallViewModel extends ViewModel {
this.encryptionSystem,
this.livekitRoom,
this.memberDisplaynames$.pipe(
map((m) => m.get(participant.identity) ?? "[👻]"),
map(
(m) => m?.get(participant.identity) ?? "[👻]",
),
),
of(null),
of(null),

View File

@@ -13,7 +13,6 @@ export enum ErrorCode {
*/
MISSING_MATRIX_RTC_FOCUS = "MISSING_MATRIX_RTC_FOCUS",
CONNECTION_LOST_ERROR = "CONNECTION_LOST_ERROR",
MEMBERSHIP_MANAGER_UNRECOVERABLE = "MEMBERSHIP_MANAGER_UNRECOVERABLE",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
}
@@ -21,7 +20,6 @@ export enum ErrorCategory {
/** Calling is not supported, server misconfigured (JWT service missing, no MSC support ...)*/
CONFIGURATION_ISSUE = "CONFIGURATION_ISSUE",
NETWORK_CONNECTIVITY = "NETWORK_CONNECTIVITY",
RTC_SESSION_FAILURE = "RTC_SESSION_FAILURE",
UNKNOWN = "UNKNOWN",
// SYSTEM_FAILURE / FEDERATION_FAILURE ..
}
@@ -74,9 +72,3 @@ export class ConnectionLostError extends ElementCallError {
);
}
}
export class RTCSessionError extends ElementCallError {
public constructor(code: ErrorCode, message: string) {
super("RTCSession Error", code, ErrorCategory.RTC_SESSION_FAILURE, message);
}
}

View File

@@ -69,7 +69,7 @@ async function waitForSync(client: MatrixClient): Promise<void> {
* otherwise rust crypto will throw since it is not ready to initialize a new session.
* If another client is running make sure `.logout()` is called before executing this function.
* @param clientOptions Object of options passed through to the client
* @param restore If the rust crypto should be reset before the client initialization or
* @param restore If the rust crypto should be reset before the cient initialization or
* if the initialization should try to restore the crypto state from the indexDB.
* @returns The MatrixClient instance
*/
@@ -160,6 +160,7 @@ export async function initClient(
);
}
client.setGlobalErrorOnUnknownDevices(false);
// Once startClient is called, syncs are run asynchronously.
// Also, sync completion is communicated only via events.
// So, apply the event listener *before* starting the client.

View File

@@ -5,7 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type Observable, defer, finalize, scan, startWith, tap } from "rxjs";
import {
BehaviorSubject,
type Observable,
defer,
finalize,
scan,
startWith,
tap,
} from "rxjs";
const nothing = Symbol("nothing");
@@ -38,3 +46,16 @@ export function accumulate<State, Event>(
return (events$: Observable<Event>): Observable<State> =>
events$.pipe(scan(update, initial), startWith(initial));
}
/**
* A constructor that takes a source Observable and returns a BehaviorSubject.
* This makes sure we only subscribe to the source once. This is useful when using fromEvent.
*/
export function singleSubscriberSubject$<T>(
observable$: Observable<T>,
initial?: T,
): BehaviorSubject<T | undefined> {
const subject$ = new BehaviorSubject(initial);
observable$.subscribe(subject$);
return subject$;
}

View File

@@ -1787,10 +1787,10 @@
dependencies:
"@bufbuild/protobuf" "^1.10.0"
"@matrix-org/matrix-sdk-crypto-wasm@^14.0.1":
version "14.0.1"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.0.1.tgz#e258ef84bcc7889f0e7eb3a7dbecf0830a6dd606"
integrity sha512-CgLpHs6nTw5pjSsMBi9xbQnBXf2l8YhImQP9cv8nbGSCYdYjFI0FilMXffzjWV5HThpNHri/3pF20ahZtuS3VA==
"@matrix-org/matrix-sdk-crypto-wasm@^12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.1.0.tgz#2aef64eab2d30c0a1ace9c0fe876f53aa2949f14"
integrity sha512-NhJFu/8FOGjnW7mDssRUzaMSwXrYOcCqgAjZyAw9KQ9unNADKEi7KoIKe7GtrG2PWtm36y2bUf+hB8vhSY6Wdw==
"@matrix-org/olm@3.2.15":
version "3.2.15"
@@ -6334,12 +6334,13 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#0cdb07544d9926cf0855a76ca5cc7dab253bdb24":
version "37.0.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0cdb07544d9926cf0855a76ca5cc7dab253bdb24"
matrix-js-sdk@^36.1.0:
version "36.1.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-36.1.0.tgz#3685a85c0c1adf4e2c3622bce76c11430963f23d"
integrity sha512-KNPswMSAGKDxBybJedxRpWadaRes9paxmjTCUsQT8t1Jg3ZENraAt6ynIaxh6PxazAH9D5ly6EYKHaLMLbZ1Dg==
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^14.0.1"
"@matrix-org/matrix-sdk-crypto-wasm" "^12.1.0"
"@matrix-org/olm" "3.2.15"
another-json "^0.2.0"
bs58 "^6.0.0"