sinch-rtc
Version:
RTC JavaScript/Web SDK
302 lines • 10.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Session = void 0;
const pubnub_1 = require("pubnub");
const models_1 = require("../mxp/models");
const fsm_1 = require("./fsm");
const Errors_1 = require("../utils/Errors");
const InvalidStateTransition_1 = require("./fsm/InvalidStateTransition");
const jsep_1 = require("./jsep");
const TerminationCause_1 = require("./TerminationCause");
const SessionState_1 = require("./SessionState");
const Event_1 = require("../utils/Event");
const MessageBuilder_1 = require("../mxp/MessageBuilder");
const utils_1 = require("../utils");
const TransitionSource_1 = require("./fsm/TransitionSource");
const TimerEventType_1 = require("./fsm/TimerEventType");
class Session {
constructor(mxpClient, sessionId, callDirection, scheduler) {
this.mxpClient = mxpClient;
this.sessionId = sessionId;
this.callDirection = callDirection;
this.scheduler = scheduler;
this.schedulerCancellations = new Map();
this.fsmState = new fsm_1.NullState();
this.transitions = new fsm_1.FsmTransitions();
this.transitionDepth = 0;
this.pendingJsepMessages = new Array();
this.mxpOutboundMessages = new Array();
this.pendingInboundMessages = new Array();
this.stateChangeEvents = new Array();
this.pendingStateChangeEvents = new Array();
this.jsepMessageReceivedEvent = new Event_1.Event();
this.symmetricSessionKey = null;
this.onSessionKey = new Event_1.Event();
this.stateChanged = new Event_1.Event();
this.outboundMessageEvent = new Event_1.Event();
this.addJsepMessage = (messages) => {
messages.forEach((m) => this.pendingJsepMessages.push(m));
};
if (sessionId.length <= 0)
throw new Errors_1.ArgumentError("Invalid session id", (0, utils_1.nameof)(sessionId));
}
get localPeer() {
return this.mxpClient;
}
get id() {
return this.sessionId;
}
get direction() {
return this.callDirection;
}
get state() {
return this.fsmState.sessionState;
}
get terminationCause() {
return this.fsmState.terminationCause;
}
get error() {
return this.sessionError;
}
set error(error) {
this.sessionError = error;
}
set onJsepMessageReceived(handler) {
this.jsepMessageReceivedEvent.add(handler);
}
get outboundMessages() {
return this.mxpOutboundMessages;
}
hasSessionKey() {
return this.symmetricSessionKey != null;
}
get sessionKey() {
return this.symmetricSessionKey;
}
set sessionKey(key) {
this.symmetricSessionKey = key;
if (key) {
this.onSessionKey.fire();
}
}
start() {
this.setStartedAt();
this.tryWithCurrentState((s) => s.transition(SessionState_1.SessionState.Initiating, TransitionSource_1.TransitionSource.LocalAction));
}
set onStateChanged(handler) {
this.stateChanged.add(handler);
}
set onOutboundMessage(handler) {
this.outboundMessageEvent.add(handler);
}
newMessage() {
return MessageBuilder_1.MessageBuilder.create()
.sessionId(this.sessionId)
.from(this.localPeer);
}
setInitialState(state) {
this.fsmState = state;
}
setStartedAt() {
if (this.startedAt)
throw new Errors_1.InvalidOperationError("Started timestamp already set");
this.startedAt = new Date();
}
timeoutRelativeToStart(ms) {
if (!this.startedAt)
throw new Errors_1.InvalidOperationError("StartedAt not set");
const now = Date.now();
if (this.startedAt.getTime() + ms < now)
return 0;
else
return ms - (now - this.startedAt.getTime());
}
terminate(error) {
if (error)
this.sessionError = error;
this.tryWithCurrentState((state) => {
state.terminate(error);
});
}
terminateWithCause(cause) {
this.tryWithCurrentState((state) => {
state.terminateWithCause(cause);
});
}
onInboundMessage(message) {
if (message.sessionId !== this.sessionId) {
throw new Errors_1.InvalidOperationError("Message not intended for this Session (SessionId mismatch)");
}
this.tryWithCurrentState((s) => {
s.onInboundMessage(message);
});
}
applyPendingInboundMessages() {
this.pendingInboundMessages.splice(0).forEach((m) => {
this.onInboundMessage(m);
});
}
onTimeout(t) {
this.tryWithCurrentState((s) => {
s.onTimeout(t);
});
}
onACKWindowTimeout(_, __) {
// Implemented by OutboundSession
}
tryWithCurrentState(action) {
const state = this.fsmState;
try {
action(state);
}
catch (error) {
state.onException(error);
}
}
sendClientEvent(event) {
this.sendOutboundMessage(this.newMessage()
.method(models_1.Method.PeerEvent)
.body(models_1.Body.clientEvent(event))
.build());
}
sendOutboundMessage(message) {
var _a;
this.mxpOutboundMessages.push(message);
(_a = this.outboundMessageEvent) === null || _a === void 0 ? void 0 : _a.fire(message);
}
hasSentMessage(predicate) {
return this.mxpOutboundMessages.find(predicate) != null;
}
transition(from, to, source) {
const previous = this.fsmState;
if (!this.transitions.tryGetNext(from, to, (nextState) => {
try {
this.transitionDepth++;
previous.exit(nextState, source);
this.fsmState = nextState;
nextState.enter(previous, source);
// Only push state change event onto stack at this
// point. Will be emitted later on when transition
// depth is at 0. This is required to handle
// re-entrant state transition, e.g. a state
// transition that is direct consequence of
// emitting a public state change event (think
// e.g. a event handler is immediately (in the
// event handler callback) terminating the session
// based on Established state).
this.pushStateChangeEvent(nextState);
this.applyPendingInboundMessages();
if (this.fsmState != nextState) {
// todo: log warning
}
// Emit state change events.
}
finally {
this.transitionDepth--;
}
if (this.transitionDepth == 0)
this.emitStateChangeEvents();
})) {
throw new InvalidStateTransition_1.InvalidStateTransition(from.sessionState, to);
}
}
emitStateChangeEvents() {
this.stateChangeEvents.splice(0).forEach((e) => {
this.tryEmitStateChangeEvent(e);
});
}
tryEmitStateChangeEvent(e) {
if (!this.emitStateChangeEvent(e))
this.pendingStateChangeEvents.push(e);
}
emitStateChangeEvent(e) {
this.applyPendingSessionStateChanges();
// Use local to be safe in case of re-entrancy
const handler = this.stateChanged;
if (handler == null)
return false;
handler.fire(e);
return true;
}
applyPendingSessionStateChanges() {
const changes = Object.assign([], this.pendingStateChangeEvents);
this.pendingStateChangeEvents.length = 0;
changes.forEach((e) => {
this.tryEmitStateChangeEvent(e);
});
}
pushStateChangeEvent(state) {
this.stateChangeEvents.push(this.createStateChangeEvent(state));
}
createStateChangeEvent(state) {
return Session.createStateChangeEvent(this.sessionId, state);
}
static createStateChangeEvent(id, state) {
if (state.terminationCause == TerminationCause_1.TerminationCause.None)
return { id, state: state.sessionState };
else
return {
id,
state: state.sessionState,
terminationCause: state.terminationCause,
};
}
emitPendingJsepEvents() {
this.emitJsepEvents(this.pendingJsepMessages.splice(0));
}
emitJsepEvents(messages) {
const handler = this.jsepMessageReceivedEvent.fire;
if (null == handler)
return;
if (messages.length > 0)
handler({ id: this.sessionId, messages });
}
emitJsepEvent(message) {
jsep_1.JsepMessage.tryParse(message, (jseps) => {
this.emitJsepEvents(jseps);
});
}
addPendingInboundMessage(m) {
this.pendingInboundMessages.push(m);
}
setLocalCandidate(candidates) {
this.tryWithCurrentState((s) => s.onCandidate(jsep_1.Source.Local, candidates));
}
setLocalSessionDescription(message) {
this.localDescription = new jsep_1.SessionDescription(message.isAnswer ? jsep_1.DescriptionType.Answer : jsep_1.DescriptionType.Offer, message.data);
this.tryWithCurrentState((s) => {
if (this.localDescription)
s.onSessionDescription(jsep_1.Source.Local, this.localDescription);
});
}
scheduleTimeout(source, timeoutMs, type) {
const evt = new fsm_1.TimerEvent((0, pubnub_1.generateUUID)().toLowerCase(), `${source.constructor.name} (timeout: ${timeoutMs}ms)`, type);
this.scheduler.scheduleDelayed(() => this.onScheduledTimeout(source, evt), timeoutMs, this.getCancellationToken(source));
}
getCancellationToken(source) {
let ct = this.schedulerCancellations.get(source);
if (!ct) {
ct = new fsm_1.CancellationTokenSource();
this.schedulerCancellations.set(source, ct);
}
return ct;
}
cancelScheduledTimeouts(source) {
var _a;
(_a = this.schedulerCancellations.get(source)) === null || _a === void 0 ? void 0 : _a.cancel();
}
onScheduledTimeout(source, evt) {
switch (evt.type) {
case TimerEventType_1.TimerEventType.Termination:
if (source == this.fsmState) {
source.onStateTimeout(evt);
}
break;
case TimerEventType_1.TimerEventType.ACKWindowEnd:
this.onACKWindowTimeout(source, evt);
break;
}
}
}
exports.Session = Session;
//# sourceMappingURL=Session.js.map