UNPKG

sinch-rtc

Version:

RTC JavaScript/Web SDK

302 lines 10.8 kB
"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