sip.js
Version:
A SIP library for JavaScript
705 lines (704 loc) • 34.1 kB
JavaScript
import { Grammar } from "../grammar/grammar.js";
import { fromBodyLegacy, getBody } from "../core/messages/body.js";
import { SignalingState } from "../core/session/session.js";
import { Timers } from "../core/timers.js";
import { TransactionStateError } from "../core/exceptions/transaction-state-error.js";
import { getReasonPhrase } from "../core/messages/utils.js";
import { Cancel } from "./cancel.js";
import { ContentTypeUnsupportedError } from "./exceptions/content-type-unsupported.js";
import { SessionDescriptionHandlerError } from "./exceptions/session-description-handler.js";
import { SessionTerminatedError } from "./exceptions/session-terminated.js";
import { Session } from "./session.js";
import { SessionState } from "./session-state.js";
import { SIPExtension } from "./user-agent-options.js";
/**
* An invitation is an offer to establish a {@link Session} (incoming INVITE).
* @public
*/
export class Invitation extends Session {
/** @internal */
constructor(userAgent, incomingInviteRequest) {
super(userAgent);
this.incomingInviteRequest = incomingInviteRequest;
/** True if dispose() has been called. */
this.disposed = false;
/** INVITE will be rejected if not accepted within a certain period time. */
this.expiresTimer = undefined;
/** True if this Session has been Terminated due to a CANCEL request. */
this.isCanceled = false;
/** Are reliable provisional responses required or supported. */
this.rel100 = "none";
/** The current RSeq header value. */
this.rseq = Math.floor(Math.random() * 10000);
/** INVITE will be rejected if final response not sent in a certain period time. */
this.userNoAnswerTimer = undefined;
/** True if waiting for a PRACK before sending a 200 Ok. */
this.waitingForPrack = false;
this.logger = userAgent.getLogger("sip.Invitation");
const incomingRequestMessage = this.incomingInviteRequest.message;
// Set 100rel if necessary
const requireHeader = incomingRequestMessage.getHeader("require");
if (requireHeader && requireHeader.toLowerCase().includes("100rel")) {
this.rel100 = "required";
}
const supportedHeader = incomingRequestMessage.getHeader("supported");
if (supportedHeader && supportedHeader.toLowerCase().includes("100rel")) {
this.rel100 = "supported";
}
// FIXME: HACK: This is a hack to port an existing behavior.
// Set the toTag on the incoming request message to the toTag which
// will be used in the response to the incoming request!!!
// The behavior being ported appears to be a hack itself,
// so this is a hack to port a hack. At least one test spec
// relies on it (which is yet another hack).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
incomingRequestMessage.toTag = incomingInviteRequest.toTag;
if (typeof incomingRequestMessage.toTag !== "string") {
throw new TypeError("toTag should have been a string.");
}
// The following mapping values are RECOMMENDED:
// ...
// 19 no answer from the user 480 Temporarily unavailable
// https://tools.ietf.org/html/rfc3398#section-7.2.4.1
this.userNoAnswerTimer = setTimeout(() => {
incomingInviteRequest.reject({ statusCode: 480 });
this.stateTransition(SessionState.Terminated);
}, this.userAgent.configuration.noAnswerTimeout ? this.userAgent.configuration.noAnswerTimeout * 1000 : 60000);
// 1. If the request is an INVITE that contains an Expires header
// field, the UAS core sets a timer for the number of seconds
// indicated in the header field value. When the timer fires, the
// invitation is considered to be expired. If the invitation
// expires before the UAS has generated a final response, a 487
// (Request Terminated) response SHOULD be generated.
// https://tools.ietf.org/html/rfc3261#section-13.3.1
if (incomingRequestMessage.hasHeader("expires")) {
const expires = Number(incomingRequestMessage.getHeader("expires") || 0) * 1000;
this.expiresTimer = setTimeout(() => {
if (this.state === SessionState.Initial) {
incomingInviteRequest.reject({ statusCode: 487 });
this.stateTransition(SessionState.Terminated);
}
}, expires);
}
// Session parent properties
const assertedIdentity = this.request.getHeader("P-Asserted-Identity");
if (assertedIdentity) {
this._assertedIdentity = Grammar.nameAddrHeaderParse(assertedIdentity);
}
this._contact = this.userAgent.contact.toString();
const contentDisposition = incomingRequestMessage.parseHeader("Content-Disposition");
if (contentDisposition && contentDisposition.type === "render") {
this._renderbody = incomingRequestMessage.body;
this._rendertype = incomingRequestMessage.getHeader("Content-Type");
}
// Identifier
this._id = incomingRequestMessage.callId + incomingRequestMessage.fromTag;
// Add to the user agent's session collection.
this.userAgent._sessions[this._id] = this;
}
/**
* Destructor.
*/
dispose() {
// Only run through this once. It can and does get called multiple times
// depending on the what the sessions state is when first called.
// For example, if called when "establishing" it will be called again
// at least once when the session transitions to "terminated".
// Regardless, running through this more than once is pointless.
if (this.disposed) {
return Promise.resolve();
}
this.disposed = true;
// Clear timers
if (this.expiresTimer) {
clearTimeout(this.expiresTimer);
this.expiresTimer = undefined;
}
if (this.userNoAnswerTimer) {
clearTimeout(this.userNoAnswerTimer);
this.userNoAnswerTimer = undefined;
}
// If accept() is still waiting for a PRACK, make sure it rejects
this.prackNeverArrived();
// If the final response for the initial INVITE not yet been sent, reject it
switch (this.state) {
case SessionState.Initial:
return this.reject().then(() => super.dispose());
case SessionState.Establishing:
return this.reject().then(() => super.dispose());
case SessionState.Established:
return super.dispose();
case SessionState.Terminating:
return super.dispose();
case SessionState.Terminated:
return super.dispose();
default:
throw new Error("Unknown state.");
}
}
/**
* If true, a first provisional response after the 100 Trying
* will be sent automatically. This is false it the UAC required
* reliable provisional responses (100rel in Require header) or
* the user agent configuration has specified to not send an
* initial response, otherwise it is true. The provisional is sent by
* calling `progress()` without any options.
*/
get autoSendAnInitialProvisionalResponse() {
return this.rel100 !== "required" && this.userAgent.configuration.sendInitialProvisionalResponse;
}
/**
* Initial incoming INVITE request message body.
*/
get body() {
return this.incomingInviteRequest.message.body;
}
/**
* The identity of the local user.
*/
get localIdentity() {
return this.request.to;
}
/**
* The identity of the remote user.
*/
get remoteIdentity() {
return this.request.from;
}
/**
* Initial incoming INVITE request message.
*/
get request() {
return this.incomingInviteRequest.message;
}
/**
* Accept the invitation.
*
* @remarks
* Accept the incoming INVITE request to start a Session.
* Replies to the INVITE request with a 200 Ok response.
* Resolves once the response sent, otherwise rejects.
*
* This method may reject for a variety of reasons including
* the receipt of a CANCEL request before `accept` is able
* to construct a response.
* @param options - Options bucket.
*/
accept(options = {}) {
this.logger.log("Invitation.accept");
// validate state
if (this.state !== SessionState.Initial) {
const error = new Error(`Invalid session state ${this.state}`);
this.logger.error(error.message);
return Promise.reject(error);
}
// Modifiers and options for initial INVITE transaction
if (options.sessionDescriptionHandlerModifiers) {
this.sessionDescriptionHandlerModifiers = options.sessionDescriptionHandlerModifiers;
}
if (options.sessionDescriptionHandlerOptions) {
this.sessionDescriptionHandlerOptions = options.sessionDescriptionHandlerOptions;
}
// transition state
this.stateTransition(SessionState.Establishing);
return (this.sendAccept(options)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.then(({ message, session }) => {
session.delegate = {
onAck: (ackRequest) => this.onAckRequest(ackRequest),
onAckTimeout: () => this.onAckTimeout(),
onBye: (byeRequest) => this.onByeRequest(byeRequest),
onInfo: (infoRequest) => this.onInfoRequest(infoRequest),
onInvite: (inviteRequest) => this.onInviteRequest(inviteRequest),
onMessage: (messageRequest) => this.onMessageRequest(messageRequest),
onNotify: (notifyRequest) => this.onNotifyRequest(notifyRequest),
onPrack: (prackRequest) => this.onPrackRequest(prackRequest),
onRefer: (referRequest) => this.onReferRequest(referRequest)
};
this._dialog = session;
this.stateTransition(SessionState.Established);
// TODO: Reconsider this "automagic" send of a BYE to replacee behavior.
// This behavior has been ported forward from legacy versions.
if (this._replacee) {
this._replacee._bye();
}
})
.catch((error) => this.handleResponseError(error)));
}
/**
* Indicate progress processing the invitation.
*
* @remarks
* Report progress to the the caller.
* Replies to the INVITE request with a 1xx provisional response.
* Resolves once the response sent, otherwise rejects.
* @param options - Options bucket.
*/
progress(options = {}) {
this.logger.log("Invitation.progress");
// validate state
if (this.state !== SessionState.Initial) {
const error = new Error(`Invalid session state ${this.state}`);
this.logger.error(error.message);
return Promise.reject(error);
}
// Ported
const statusCode = options.statusCode || 180;
if (statusCode < 100 || statusCode > 199) {
throw new TypeError("Invalid statusCode: " + statusCode);
}
// Modifiers and options for initial INVITE transaction
if (options.sessionDescriptionHandlerModifiers) {
this.sessionDescriptionHandlerModifiers = options.sessionDescriptionHandlerModifiers;
}
if (options.sessionDescriptionHandlerOptions) {
this.sessionDescriptionHandlerOptions = options.sessionDescriptionHandlerOptions;
}
// After the first reliable provisional response for a request has been
// acknowledged, the UAS MAY send additional reliable provisional
// responses. The UAS MUST NOT send a second reliable provisional
// response until the first is acknowledged. After the first, it is
// RECOMMENDED that the UAS not send an additional reliable provisional
// response until the previous is acknowledged. The first reliable
// provisional response receives special treatment because it conveys
// the initial sequence number. If additional reliable provisional
// responses were sent before the first was acknowledged, the UAS could
// not be certain these were received in order.
// https://tools.ietf.org/html/rfc3262#section-3
if (this.waitingForPrack) {
this.logger.warn("Unexpected call for progress while waiting for prack, ignoring");
return Promise.resolve();
}
// Trying provisional response
if (options.statusCode === 100) {
return this.sendProgressTrying()
.then(() => {
return;
})
.catch((error) => this.handleResponseError(error));
}
// Standard provisional response
if (!(this.rel100 === "required") &&
!(this.rel100 === "supported" && options.rel100) &&
!(this.rel100 === "supported" && this.userAgent.configuration.sipExtension100rel === SIPExtension.Required)) {
return this.sendProgress(options)
.then(() => {
return;
})
.catch((error) => this.handleResponseError(error));
}
// Reliable provisional response
return this.sendProgressReliableWaitForPrack(options)
.then(() => {
return;
})
.catch((error) => this.handleResponseError(error));
}
/**
* Reject the invitation.
*
* @remarks
* Replies to the INVITE request with a 4xx, 5xx, or 6xx final response.
* Resolves once the response sent, otherwise rejects.
*
* The expectation is that this method is used to reject an INVITE request.
* That is indeed the case - a call to `progress` followed by `reject` is
* a typical way to "decline" an incoming INVITE request. However it may
* also be called after calling `accept` (but only before it completes)
* which will reject the call and cause `accept` to reject.
* @param options - Options bucket.
*/
reject(options = {}) {
this.logger.log("Invitation.reject");
// validate state
if (this.state !== SessionState.Initial && this.state !== SessionState.Establishing) {
const error = new Error(`Invalid session state ${this.state}`);
this.logger.error(error.message);
return Promise.reject(error);
}
const statusCode = options.statusCode || 480;
const reasonPhrase = options.reasonPhrase ? options.reasonPhrase : getReasonPhrase(statusCode);
const extraHeaders = options.extraHeaders || [];
if (statusCode < 300 || statusCode > 699) {
throw new TypeError("Invalid statusCode: " + statusCode);
}
const body = options.body ? fromBodyLegacy(options.body) : undefined;
// FIXME: Need to redirect to someplace
statusCode < 400
? this.incomingInviteRequest.redirect([], { statusCode, reasonPhrase, extraHeaders, body })
: this.incomingInviteRequest.reject({ statusCode, reasonPhrase, extraHeaders, body });
this.stateTransition(SessionState.Terminated);
return Promise.resolve();
}
/**
* Handle CANCEL request.
*
* @param message - CANCEL message.
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_onCancel(message) {
this.logger.log("Invitation._onCancel");
// validate state
if (this.state !== SessionState.Initial && this.state !== SessionState.Establishing) {
this.logger.error(`CANCEL received while in state ${this.state}, dropping request`);
return;
}
if (this.delegate && this.delegate.onCancel) {
const cancel = new Cancel(message);
this.delegate.onCancel(cancel);
}
// flag canceled
this.isCanceled = true;
// reject INVITE with 487 status code
this.incomingInviteRequest.reject({ statusCode: 487 });
this.stateTransition(SessionState.Terminated);
}
/**
* Helper function to handle offer/answer in a PRACK.
*/
handlePrackOfferAnswer(request) {
if (!this.dialog) {
throw new Error("Dialog undefined.");
}
// If the PRACK doesn't have an offer/answer, nothing to be done.
const body = getBody(request.message);
if (!body || body.contentDisposition !== "session") {
return Promise.resolve(undefined);
}
const options = {
sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions,
sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers
};
// If the UAC receives a reliable provisional response with an offer
// (this would occur if the UAC sent an INVITE without an offer, in
// which case the first reliable provisional response will contain the
// offer), it MUST generate an answer in the PRACK. If the UAC receives
// a reliable provisional response with an answer, it MAY generate an
// additional offer in the PRACK. If the UAS receives a PRACK with an
// offer, it MUST place the answer in the 2xx to the PRACK.
// https://tools.ietf.org/html/rfc3262#section-5
switch (this.dialog.signalingState) {
case SignalingState.Initial:
// State should never be reached as first reliable provisional response must have answer/offer.
throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);
case SignalingState.Stable:
// Receved answer.
return this.setAnswer(body, options).then(() => undefined);
case SignalingState.HaveLocalOffer:
// State should never be reached as local offer would be answered by this PRACK
throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);
case SignalingState.HaveRemoteOffer:
// Received offer, generate answer.
return this.setOfferAndGetAnswer(body, options);
case SignalingState.Closed:
throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);
default:
throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);
}
}
/**
* A handler for errors which occur while attempting to send 1xx and 2xx responses.
* In all cases, an attempt is made to reject the request if it is still outstanding.
* And while there are a variety of things which can go wrong and we log something here
* for all errors, there are a handful of common exceptions we pay some extra attention to.
* @param error - The error which occurred.
*/
handleResponseError(error) {
let statusCode = 480; // "Temporarily Unavailable"
// Log Error message
if (error instanceof Error) {
this.logger.error(error.message);
}
else {
// We don't actually know what a session description handler implementation might throw our way,
// and more generally as a last resort catch all, just assume we are getting an "unknown" and log it.
this.logger.error(error);
}
// Log Exception message
if (error instanceof ContentTypeUnsupportedError) {
this.logger.error("A session description handler occurred while sending response (content type unsupported");
statusCode = 415; // "Unsupported Media Type"
}
else if (error instanceof SessionDescriptionHandlerError) {
this.logger.error("A session description handler occurred while sending response");
}
else if (error instanceof SessionTerminatedError) {
this.logger.error("Session ended before response could be formulated and sent (while waiting for PRACK)");
}
else if (error instanceof TransactionStateError) {
this.logger.error("Session changed state before response could be formulated and sent");
}
// Reject if still in "initial" or "establishing" state.
if (this.state === SessionState.Initial || this.state === SessionState.Establishing) {
try {
this.incomingInviteRequest.reject({ statusCode });
this.stateTransition(SessionState.Terminated);
}
catch (e) {
this.logger.error("An error occurred attempting to reject the request while handling another error");
throw e; // This is not a good place to be...
}
}
// FIXME: TODO:
// Here we are squelching the throwing of errors due to an race condition.
// We have an internal race between calling `accept()` and handling an incoming
// CANCEL request. As there is no good way currently to delegate the handling of
// these race errors to the caller of `accept()`, we are squelching the throwing
// of ALL errors when/if they occur after receiving a CANCEL to catch the ONE we know
// is a "normal" exceptional condition. While this is a completely reasonable approach,
// the decision should be left up to the library user. Furthermore, as we are eating
// ALL errors in this case, we are potentially (likely) hiding "real" errors which occur.
//
// Only rethrow error if the session has not been canceled.
if (this.isCanceled) {
this.logger.warn("An error occurred while attempting to formulate and send a response to an incoming INVITE." +
" However a CANCEL was received and processed while doing so which can (and often does) result" +
" in errors occurring as the session terminates in the meantime. Said error is being ignored.");
return;
}
throw error;
}
/**
* Callback for when ACK for a 2xx response is never received.
* @param session - Session the ACK never arrived for.
*/
onAckTimeout() {
this.logger.log("Invitation.onAckTimeout");
if (!this.dialog) {
throw new Error("Dialog undefined.");
}
this.logger.log("No ACK received for an extended period of time, terminating session");
this.dialog.bye();
this.stateTransition(SessionState.Terminated);
}
/**
* A version of `accept` which resolves a session when the 200 Ok response is sent.
* @param options - Options bucket.
*/
sendAccept(options = {}) {
const responseOptions = {
sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions,
sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers
};
const extraHeaders = options.extraHeaders || [];
// The UAS MAY send a final response to the initial request before
// having received PRACKs for all unacknowledged reliable provisional
// responses, unless the final response is 2xx and any of the
// unacknowledged reliable provisional responses contained a session
// description. In that case, it MUST NOT send a final response until
// those provisional responses are acknowledged. If the UAS does send a
// final response when reliable responses are still unacknowledged, it
// SHOULD NOT continue to retransmit the unacknowledged reliable
// provisional responses, but it MUST be prepared to process PRACK
// requests for those outstanding responses. A UAS MUST NOT send new
// reliable provisional responses (as opposed to retransmissions of
// unacknowledged ones) after sending a final response to a request.
// https://tools.ietf.org/html/rfc3262#section-3
if (this.waitingForPrack) {
return this.waitForArrivalOfPrack()
.then(() => clearTimeout(this.userNoAnswerTimer)) // Ported
.then(() => this.generateResponseOfferAnswer(this.incomingInviteRequest, responseOptions))
.then((body) => this.incomingInviteRequest.accept({ statusCode: 200, body, extraHeaders }));
}
clearTimeout(this.userNoAnswerTimer); // Ported
return this.generateResponseOfferAnswer(this.incomingInviteRequest, responseOptions).then((body) => this.incomingInviteRequest.accept({ statusCode: 200, body, extraHeaders }));
}
/**
* A version of `progress` which resolves when the provisional response is sent.
* @param options - Options bucket.
*/
sendProgress(options = {}) {
const statusCode = options.statusCode || 180;
const reasonPhrase = options.reasonPhrase;
const extraHeaders = (options.extraHeaders || []).slice();
const body = options.body ? fromBodyLegacy(options.body) : undefined;
// The 183 (Session Progress) response is used to convey information
// about the progress of the call that is not otherwise classified. The
// Reason-Phrase, header fields, or message body MAY be used to convey
// more details about the call progress.
// https://tools.ietf.org/html/rfc3261#section-21.1.5
// It is the de facto industry standard to utilize 183 with SDP to provide "early media".
// While it is unlikely someone would want to send a 183 without SDP, so it should be an option.
if (statusCode === 183 && !body) {
return this.sendProgressWithSDP(options);
}
try {
const progressResponse = this.incomingInviteRequest.progress({ statusCode, reasonPhrase, extraHeaders, body });
this._dialog = progressResponse.session;
return Promise.resolve(progressResponse);
}
catch (error) {
return Promise.reject(error);
}
}
/**
* A version of `progress` which resolves when the provisional response with sdp is sent.
* @param options - Options bucket.
*/
sendProgressWithSDP(options = {}) {
const responseOptions = {
sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions,
sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers
};
const statusCode = options.statusCode || 183;
const reasonPhrase = options.reasonPhrase;
const extraHeaders = (options.extraHeaders || []).slice();
// Get an offer/answer and send a reply.
return this.generateResponseOfferAnswer(this.incomingInviteRequest, responseOptions)
.then((body) => this.incomingInviteRequest.progress({ statusCode, reasonPhrase, extraHeaders, body }))
.then((progressResponse) => {
this._dialog = progressResponse.session;
return progressResponse;
});
}
/**
* A version of `progress` which resolves when the reliable provisional response is sent.
* @param options - Options bucket.
*/
sendProgressReliable(options = {}) {
options.extraHeaders = (options.extraHeaders || []).slice();
options.extraHeaders.push("Require: 100rel");
options.extraHeaders.push("RSeq: " + Math.floor(Math.random() * 10000));
return this.sendProgressWithSDP(options);
}
/**
* A version of `progress` which resolves when the reliable provisional response is acknowledged.
* @param options - Options bucket.
*/
sendProgressReliableWaitForPrack(options = {}) {
const responseOptions = {
sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions,
sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers
};
const statusCode = options.statusCode || 183;
const reasonPhrase = options.reasonPhrase;
const extraHeaders = (options.extraHeaders || []).slice();
extraHeaders.push("Require: 100rel");
extraHeaders.push("RSeq: " + this.rseq++);
let body;
return new Promise((resolve, reject) => {
this.waitingForPrack = true;
this.generateResponseOfferAnswer(this.incomingInviteRequest, responseOptions)
.then((offerAnswer) => {
body = offerAnswer;
return this.incomingInviteRequest.progress({ statusCode, reasonPhrase, extraHeaders, body });
})
.then((progressResponse) => {
this._dialog = progressResponse.session;
let prackRequest;
let prackResponse;
progressResponse.session.delegate = {
onPrack: (request) => {
prackRequest = request;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
clearTimeout(prackWaitTimeoutTimer);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
clearTimeout(rel1xxRetransmissionTimer);
if (!this.waitingForPrack) {
return;
}
this.waitingForPrack = false;
this.handlePrackOfferAnswer(prackRequest)
.then((prackResponseBody) => {
try {
prackResponse = prackRequest.accept({ statusCode: 200, body: prackResponseBody });
this.prackArrived();
resolve({ prackRequest, prackResponse, progressResponse });
}
catch (error) {
reject(error);
}
})
.catch((error) => reject(error));
}
};
// https://tools.ietf.org/html/rfc3262#section-3
const prackWaitTimeout = () => {
if (!this.waitingForPrack) {
return;
}
this.waitingForPrack = false;
this.logger.warn("No PRACK received, rejecting INVITE.");
// eslint-disable-next-line @typescript-eslint/no-use-before-define
clearTimeout(rel1xxRetransmissionTimer);
this.reject({ statusCode: 504 })
.then(() => reject(new SessionTerminatedError()))
.catch((error) => reject(error));
};
const prackWaitTimeoutTimer = setTimeout(prackWaitTimeout, Timers.T1 * 64);
// https://tools.ietf.org/html/rfc3262#section-3
const rel1xxRetransmission = () => {
try {
this.incomingInviteRequest.progress({ statusCode, reasonPhrase, extraHeaders, body });
}
catch (error) {
this.waitingForPrack = false;
reject(error);
return;
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
rel1xxRetransmissionTimer = setTimeout(rel1xxRetransmission, (timeout *= 2));
};
let timeout = Timers.T1;
let rel1xxRetransmissionTimer = setTimeout(rel1xxRetransmission, timeout);
})
.catch((error) => {
this.waitingForPrack = false;
reject(error);
});
});
}
/**
* A version of `progress` which resolves when a 100 Trying provisional response is sent.
*/
sendProgressTrying() {
try {
const progressResponse = this.incomingInviteRequest.trying();
return Promise.resolve(progressResponse);
}
catch (error) {
return Promise.reject(error);
}
}
/**
* When attempting to accept the INVITE, an invitation waits
* for any outstanding PRACK to arrive before sending the 200 Ok.
* It will be waiting on this Promise to resolve which lets it know
* the PRACK has arrived and it may proceed to send the 200 Ok.
*/
waitForArrivalOfPrack() {
if (this.waitingForPrackPromise) {
throw new Error("Already waiting for PRACK");
}
this.waitingForPrackPromise = new Promise((resolve, reject) => {
this.waitingForPrackResolve = resolve;
this.waitingForPrackReject = reject;
});
return this.waitingForPrackPromise;
}
/**
* Here we are resolving the promise which in turn will cause
* the accept to proceed (it may still fail for other reasons, but...).
*/
prackArrived() {
if (this.waitingForPrackResolve) {
this.waitingForPrackResolve();
}
this.waitingForPrackPromise = undefined;
this.waitingForPrackResolve = undefined;
this.waitingForPrackReject = undefined;
}
/**
* Here we are rejecting the promise which in turn will cause
* the accept to fail and the session to transition to "terminated".
*/
prackNeverArrived() {
if (this.waitingForPrackReject) {
this.waitingForPrackReject(new SessionTerminatedError());
}
this.waitingForPrackPromise = undefined;
this.waitingForPrackResolve = undefined;
this.waitingForPrackReject = undefined;
}
}