livekit-client 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/livekit-client.esm.mjs +2240 -1067
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +32 -0
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +315 -75
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +9 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/ReconnectPolicy.d.ts +1 -0
- package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts +14 -0
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
- package/dist/src/room/Room.d.ts +6 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +2 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +15 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +3 -2
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts +2 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +3 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +4 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +4 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/index.d.ts +3 -1
- package/dist/ts4.2/src/options.d.ts +5 -0
- package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
- package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
- package/dist/ts4.2/src/room/RTCEngine.d.ts +9 -1
- package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
- package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
- package/dist/ts4.2/src/room/Room.d.ts +6 -1
- package/dist/ts4.2/src/room/errors.d.ts +2 -1
- package/dist/ts4.2/src/room/events.d.ts +15 -2
- package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +2 -1
- package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
- package/dist/ts4.2/src/room/types.d.ts +4 -0
- package/dist/ts4.2/src/room/utils.d.ts +4 -0
- package/package.json +19 -19
- package/src/api/SignalClient.ts +4 -4
- package/src/index.ts +3 -0
- package/src/options.ts +6 -0
- package/src/proto/google/protobuf/timestamp.ts +15 -6
- package/src/proto/livekit_models.ts +903 -222
- package/src/proto/livekit_rtc.ts +1053 -279
- package/src/room/RTCEngine.ts +168 -56
- package/src/room/ReconnectPolicy.ts +2 -0
- package/src/room/RegionUrlProvider.ts +73 -0
- package/src/room/Room.ts +212 -133
- package/src/room/defaults.ts +1 -0
- package/src/room/errors.ts +1 -0
- package/src/room/events.ts +15 -0
- package/src/room/track/LocalAudioTrack.ts +14 -6
- package/src/room/track/LocalTrack.ts +22 -8
- package/src/room/track/LocalVideoTrack.ts +12 -6
- package/src/room/track/RemoteTrackPublication.ts +10 -4
- package/src/room/track/RemoteVideoTrack.test.ts +2 -0
- package/src/room/track/RemoteVideoTrack.ts +53 -9
- package/src/room/track/Track.ts +46 -31
- package/src/room/track/utils.ts +3 -2
- package/src/room/types.ts +6 -0
- package/src/room/utils.ts +53 -0
package/src/room/RTCEngine.ts
CHANGED
@@ -40,6 +40,7 @@ import type { SimulcastTrackInfo } from './track/LocalVideoTrack';
|
|
40
40
|
import type { TrackPublishOptions, VideoCodec } from './track/options';
|
41
41
|
import { Track } from './track/Track';
|
42
42
|
import {
|
43
|
+
isCloud,
|
43
44
|
isWeb,
|
44
45
|
Mutex,
|
45
46
|
sleep,
|
@@ -47,6 +48,7 @@ import {
|
|
47
48
|
supportsSetCodecPreferences,
|
48
49
|
supportsTransceiver,
|
49
50
|
} from './utils';
|
51
|
+
import { RegionUrlProvider } from './RegionUrlProvider';
|
50
52
|
|
51
53
|
const lossyDataChannel = '_lossy';
|
52
54
|
const reliableDataChannel = '_reliable';
|
@@ -84,6 +86,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
84
86
|
|
85
87
|
private reliableDC?: RTCDataChannel;
|
86
88
|
|
89
|
+
private dcBufferStatus: Map<DataPacket_Kind, boolean>;
|
90
|
+
|
87
91
|
// @ts-ignore noUnusedLocals
|
88
92
|
private reliableDCSub?: RTCDataChannel;
|
89
93
|
|
@@ -134,8 +138,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
134
138
|
|
135
139
|
private closingLock: Mutex;
|
136
140
|
|
141
|
+
private dataProcessLock: Mutex;
|
142
|
+
|
137
143
|
private shouldFailNext: boolean = false;
|
138
144
|
|
145
|
+
private regionUrlProvider?: RegionUrlProvider;
|
146
|
+
|
139
147
|
constructor(private options: InternalRoomOptions) {
|
140
148
|
super();
|
141
149
|
this.client = new SignalClient();
|
@@ -143,6 +151,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
143
151
|
this.reconnectPolicy = this.options.reconnectPolicy;
|
144
152
|
this.registerOnLineListener();
|
145
153
|
this.closingLock = new Mutex();
|
154
|
+
this.dataProcessLock = new Mutex();
|
155
|
+
this.dcBufferStatus = new Map([
|
156
|
+
[DataPacket_Kind.LOSSY, true],
|
157
|
+
[DataPacket_Kind.RELIABLE, true],
|
158
|
+
]);
|
146
159
|
}
|
147
160
|
|
148
161
|
async join(
|
@@ -506,6 +519,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
506
519
|
// handle datachannel errors
|
507
520
|
this.lossyDC.onerror = this.handleDataError;
|
508
521
|
this.reliableDC.onerror = this.handleDataError;
|
522
|
+
|
523
|
+
// set up dc buffer threshold, set to 64kB (otherwise 0 by default)
|
524
|
+
this.lossyDC.bufferedAmountLowThreshold = 65535;
|
525
|
+
this.reliableDC.bufferedAmountLowThreshold = 65535;
|
526
|
+
|
527
|
+
// handle buffer amount low events
|
528
|
+
this.lossyDC.onbufferedamountlow = this.handleBufferedAmountLow;
|
529
|
+
this.reliableDC.onbufferedamountlow = this.handleBufferedAmountLow;
|
509
530
|
}
|
510
531
|
|
511
532
|
private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
|
@@ -524,22 +545,28 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
524
545
|
};
|
525
546
|
|
526
547
|
private handleDataMessage = async (message: MessageEvent) => {
|
527
|
-
//
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
548
|
+
// make sure to respect incoming data message order by processing message events one after the other
|
549
|
+
const unlock = await this.dataProcessLock.lock();
|
550
|
+
try {
|
551
|
+
// decode
|
552
|
+
let buffer: ArrayBuffer | undefined;
|
553
|
+
if (message.data instanceof ArrayBuffer) {
|
554
|
+
buffer = message.data;
|
555
|
+
} else if (message.data instanceof Blob) {
|
556
|
+
buffer = await message.data.arrayBuffer();
|
557
|
+
} else {
|
558
|
+
log.error('unsupported data type', message.data);
|
559
|
+
return;
|
560
|
+
}
|
561
|
+
const dp = DataPacket.decode(new Uint8Array(buffer));
|
562
|
+
if (dp.value?.$case === 'speaker') {
|
563
|
+
// dispatch speaker updates
|
564
|
+
this.emit(EngineEvent.ActiveSpeakersUpdate, dp.value.speaker.speakers);
|
565
|
+
} else if (dp.value?.$case === 'user') {
|
566
|
+
this.emit(EngineEvent.DataPacketReceived, dp.value.user, dp.kind);
|
567
|
+
}
|
568
|
+
} finally {
|
569
|
+
unlock();
|
543
570
|
}
|
544
571
|
};
|
545
572
|
|
@@ -555,6 +582,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
555
582
|
}
|
556
583
|
};
|
557
584
|
|
585
|
+
private handleBufferedAmountLow = (event: Event) => {
|
586
|
+
const channel = event.currentTarget as RTCDataChannel;
|
587
|
+
const channelKind =
|
588
|
+
channel.maxRetransmits === 0 ? DataPacket_Kind.LOSSY : DataPacket_Kind.RELIABLE;
|
589
|
+
|
590
|
+
this.updateAndEmitDCBufferStatus(channelKind);
|
591
|
+
};
|
592
|
+
|
558
593
|
private setPreferredCodec(
|
559
594
|
transceiver: RTCRtpTransceiver,
|
560
595
|
kind: Track.Kind,
|
@@ -730,6 +765,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
730
765
|
log.debug(`reconnecting in ${delay}ms`);
|
731
766
|
|
732
767
|
this.clearReconnectTimeout();
|
768
|
+
if (this.url && this.token && isCloud(new URL(this.url))) {
|
769
|
+
this.regionUrlProvider = new RegionUrlProvider(this.url, this.token);
|
770
|
+
}
|
733
771
|
this.reconnectTimeout = CriticalTimers.setTimeout(
|
734
772
|
() => this.attemptReconnect(disconnectReason),
|
735
773
|
delay,
|
@@ -801,46 +839,62 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
801
839
|
return null;
|
802
840
|
}
|
803
841
|
|
804
|
-
private async restartConnection() {
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
842
|
+
private async restartConnection(regionUrl?: string) {
|
843
|
+
try {
|
844
|
+
if (!this.url || !this.token) {
|
845
|
+
// permanent failure, don't attempt reconnection
|
846
|
+
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
847
|
+
}
|
809
848
|
|
810
|
-
|
811
|
-
|
849
|
+
log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
|
850
|
+
this.emit(EngineEvent.Restarting);
|
812
851
|
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
852
|
+
if (this.client.isConnected) {
|
853
|
+
await this.client.sendLeave();
|
854
|
+
}
|
855
|
+
await this.client.close();
|
856
|
+
this.primaryPC = undefined;
|
857
|
+
this.publisher?.close();
|
858
|
+
this.publisher = undefined;
|
859
|
+
this.subscriber?.close();
|
860
|
+
this.subscriber = undefined;
|
861
|
+
|
862
|
+
let joinResponse: JoinResponse;
|
863
|
+
try {
|
864
|
+
if (!this.signalOpts) {
|
865
|
+
log.warn('attempted connection restart, without signal options present');
|
866
|
+
throw new SignalReconnectError();
|
867
|
+
}
|
868
|
+
// in case a regionUrl is passed, the region URL takes precedence
|
869
|
+
joinResponse = await this.join(regionUrl ?? this.url, this.token, this.signalOpts);
|
870
|
+
} catch (e) {
|
871
|
+
if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
|
872
|
+
throw new UnexpectedConnectionState('could not reconnect, token might be expired');
|
873
|
+
}
|
827
874
|
throw new SignalReconnectError();
|
828
875
|
}
|
829
|
-
joinResponse = await this.join(this.url, this.token, this.signalOpts);
|
830
|
-
} catch (e) {
|
831
|
-
throw new SignalReconnectError();
|
832
|
-
}
|
833
876
|
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
await this.waitForPCConnected();
|
840
|
-
this.client.setReconnected();
|
877
|
+
if (this.shouldFailNext) {
|
878
|
+
this.shouldFailNext = false;
|
879
|
+
throw new Error('simulated failure');
|
880
|
+
}
|
841
881
|
|
842
|
-
|
843
|
-
|
882
|
+
await this.waitForPCReconnected();
|
883
|
+
this.client.setReconnected();
|
884
|
+
this.regionUrlProvider?.resetAttempts();
|
885
|
+
// reconnect success
|
886
|
+
this.emit(EngineEvent.Restarted, joinResponse);
|
887
|
+
} catch (error) {
|
888
|
+
const nextRegionUrl = await this.regionUrlProvider?.getNextBestRegionUrl();
|
889
|
+
if (nextRegionUrl) {
|
890
|
+
await this.restartConnection(nextRegionUrl);
|
891
|
+
return;
|
892
|
+
} else {
|
893
|
+
// no more regions to try (or we're not on cloud)
|
894
|
+
this.regionUrlProvider?.resetAttempts();
|
895
|
+
throw error;
|
896
|
+
}
|
897
|
+
}
|
844
898
|
}
|
845
899
|
|
846
900
|
private async resumeConnection(reason?: ReconnectReason): Promise<void> {
|
@@ -868,6 +922,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
868
922
|
if (e instanceof Error) {
|
869
923
|
message = e.message;
|
870
924
|
}
|
925
|
+
if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
|
926
|
+
throw new UnexpectedConnectionState('could not reconnect, token might be expired');
|
927
|
+
}
|
871
928
|
throw new SignalReconnectError(message);
|
872
929
|
}
|
873
930
|
this.emit(EngineEvent.SignalResumed);
|
@@ -884,7 +941,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
884
941
|
await this.publisher.createAndSendOffer({ iceRestart: true });
|
885
942
|
}
|
886
943
|
|
887
|
-
await this.
|
944
|
+
await this.waitForPCReconnected();
|
888
945
|
this.client.setReconnected();
|
889
946
|
|
890
947
|
// recreate publish datachannel if it's id is null
|
@@ -897,7 +954,45 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
897
954
|
this.emit(EngineEvent.Resumed);
|
898
955
|
}
|
899
956
|
|
900
|
-
async
|
957
|
+
async waitForPCInitialConnection(timeout?: number, abortController?: AbortController) {
|
958
|
+
if (this.pcState === PCState.Connected) {
|
959
|
+
return;
|
960
|
+
}
|
961
|
+
if (this.pcState !== PCState.New) {
|
962
|
+
throw new UnexpectedConnectionState(
|
963
|
+
'Expected peer connection to be new on initial connection',
|
964
|
+
);
|
965
|
+
}
|
966
|
+
return new Promise<void>((resolve, reject) => {
|
967
|
+
const abortHandler = () => {
|
968
|
+
log.warn('closing engine');
|
969
|
+
CriticalTimers.clearTimeout(connectTimeout);
|
970
|
+
|
971
|
+
reject(
|
972
|
+
new ConnectionError(
|
973
|
+
'room connection has been cancelled',
|
974
|
+
ConnectionErrorReason.Cancelled,
|
975
|
+
),
|
976
|
+
);
|
977
|
+
};
|
978
|
+
if (abortController?.signal.aborted) {
|
979
|
+
abortHandler();
|
980
|
+
}
|
981
|
+
abortController?.signal.addEventListener('abort', abortHandler);
|
982
|
+
const onConnected = () => {
|
983
|
+
CriticalTimers.clearTimeout(connectTimeout);
|
984
|
+
abortController?.signal.removeEventListener('abort', abortHandler);
|
985
|
+
resolve();
|
986
|
+
};
|
987
|
+
const connectTimeout = CriticalTimers.setTimeout(() => {
|
988
|
+
this.off(EngineEvent.Connected, onConnected);
|
989
|
+
reject(new ConnectionError('could not establish pc connection'));
|
990
|
+
}, timeout ?? this.peerConnectionTimeout);
|
991
|
+
this.once(EngineEvent.Connected, onConnected);
|
992
|
+
});
|
993
|
+
}
|
994
|
+
|
995
|
+
async waitForPCReconnected() {
|
901
996
|
const startTime = Date.now();
|
902
997
|
let now = startTime;
|
903
998
|
this.pcState = PCState.Reconnecting;
|
@@ -934,13 +1029,29 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
934
1029
|
// make sure we do have a data connection
|
935
1030
|
await this.ensurePublisherConnected(kind);
|
936
1031
|
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
this.reliableDC.send(msg);
|
1032
|
+
const dc = this.dataChannelForKind(kind);
|
1033
|
+
if (dc) {
|
1034
|
+
dc.send(msg);
|
941
1035
|
}
|
1036
|
+
|
1037
|
+
this.updateAndEmitDCBufferStatus(kind);
|
942
1038
|
}
|
943
1039
|
|
1040
|
+
private updateAndEmitDCBufferStatus = (kind: DataPacket_Kind) => {
|
1041
|
+
const status = this.isBufferStatusLow(kind);
|
1042
|
+
if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
|
1043
|
+
this.dcBufferStatus.set(kind, status);
|
1044
|
+
this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
|
1045
|
+
}
|
1046
|
+
};
|
1047
|
+
|
1048
|
+
private isBufferStatusLow = (kind: DataPacket_Kind): boolean | undefined => {
|
1049
|
+
const dc = this.dataChannelForKind(kind);
|
1050
|
+
if (dc) {
|
1051
|
+
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
1052
|
+
}
|
1053
|
+
};
|
1054
|
+
|
944
1055
|
/**
|
945
1056
|
* @internal
|
946
1057
|
*/
|
@@ -1146,4 +1257,5 @@ export type EngineEventCallbacks = {
|
|
1146
1257
|
activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
|
1147
1258
|
dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void;
|
1148
1259
|
transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
|
1260
|
+
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
1149
1261
|
};
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import type { RegionInfo, RegionSettings } from '../proto/livekit_rtc';
|
2
|
+
import { ConnectionError, ConnectionErrorReason } from './errors';
|
3
|
+
import log from '../logger';
|
4
|
+
import { isCloud } from './utils';
|
5
|
+
|
6
|
+
export class RegionUrlProvider {
|
7
|
+
private serverUrl: URL;
|
8
|
+
|
9
|
+
private token: string;
|
10
|
+
|
11
|
+
private regionSettings: RegionSettings | undefined;
|
12
|
+
|
13
|
+
private lastUpdateAt: number = 0;
|
14
|
+
|
15
|
+
private settingsCacheTime = 3_000;
|
16
|
+
|
17
|
+
private attemptedRegions: RegionInfo[] = [];
|
18
|
+
|
19
|
+
constructor(url: string, token: string) {
|
20
|
+
this.serverUrl = new URL(url);
|
21
|
+
this.token = token;
|
22
|
+
}
|
23
|
+
|
24
|
+
isCloud() {
|
25
|
+
return isCloud(this.serverUrl);
|
26
|
+
}
|
27
|
+
|
28
|
+
async getNextBestRegionUrl(abortSignal?: AbortSignal) {
|
29
|
+
if (!this.isCloud()) {
|
30
|
+
throw Error('region availability is only supported for LiveKit Cloud domains');
|
31
|
+
}
|
32
|
+
if (!this.regionSettings || Date.now() - this.lastUpdateAt > this.settingsCacheTime) {
|
33
|
+
this.regionSettings = await this.fetchRegionSettings(abortSignal);
|
34
|
+
}
|
35
|
+
const regionsLeft = this.regionSettings.regions.filter(
|
36
|
+
(region) => !this.attemptedRegions.find((attempted) => attempted.url === region.url),
|
37
|
+
);
|
38
|
+
if (regionsLeft.length > 0) {
|
39
|
+
const nextRegion = regionsLeft[0];
|
40
|
+
this.attemptedRegions.push(nextRegion);
|
41
|
+
log.debug(`next region: ${nextRegion.region}`);
|
42
|
+
return nextRegion.url;
|
43
|
+
} else {
|
44
|
+
return null;
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
resetAttempts() {
|
49
|
+
this.attemptedRegions = [];
|
50
|
+
}
|
51
|
+
|
52
|
+
private async fetchRegionSettings(signal?: AbortSignal) {
|
53
|
+
const regionSettingsResponse = await fetch(`${getCloudConfigUrl(this.serverUrl)}/regions`, {
|
54
|
+
headers: { authorization: `Bearer ${this.token}` },
|
55
|
+
signal,
|
56
|
+
});
|
57
|
+
if (regionSettingsResponse.ok) {
|
58
|
+
const regionSettings = (await regionSettingsResponse.json()) as RegionSettings;
|
59
|
+
this.lastUpdateAt = Date.now();
|
60
|
+
return regionSettings;
|
61
|
+
} else {
|
62
|
+
throw new ConnectionError(
|
63
|
+
`Could not fetch region settings: ${regionSettingsResponse.statusText}`,
|
64
|
+
regionSettingsResponse.status === 401 ? ConnectionErrorReason.NotAllowed : undefined,
|
65
|
+
regionSettingsResponse.status,
|
66
|
+
);
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
function getCloudConfigUrl(serverUrl: URL) {
|
72
|
+
return `${serverUrl.protocol.replace('ws', 'http')}//${serverUrl.host}/settings`;
|
73
|
+
}
|