sip.js
Version:
A SIP library for JavaScript
234 lines (233 loc) • 10.5 kB
JavaScript
import { Timers } from "../timers.js";
import { ClientTransaction } from "./client-transaction.js";
import { TransactionState } from "./transaction-state.js";
/**
* Non-INVITE Client Transaction.
* @remarks
* Non-INVITE transactions do not make use of ACK.
* They are simple request-response interactions.
* https://tools.ietf.org/html/rfc3261#section-17.1.2
* @public
*/
export class NonInviteClientTransaction extends ClientTransaction {
/**
* Constructor
* Upon construction, the outgoing request's Via header is updated by calling `setViaHeader`.
* Then `toString` is called on the outgoing request and the message is sent via the transport.
* After construction the transaction will be in the "calling" state and the transaction id
* will equal the branch parameter set in the Via header of the outgoing request.
* https://tools.ietf.org/html/rfc3261#section-17.1.2
* @param request - The outgoing Non-INVITE request.
* @param transport - The transport.
* @param user - The transaction user.
*/
constructor(request, transport, user) {
super(request, transport, user, TransactionState.Trying, "sip.transaction.nict");
// FIXME: Timer E for unreliable transports not implemented.
//
// The "Trying" state is entered when the TU initiates a new client
// transaction with a request. When entering this state, the client
// transaction SHOULD set timer F to fire in 64*T1 seconds. The request
// MUST be passed to the transport layer for transmission.
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
this.F = setTimeout(() => this.timerF(), Timers.TIMER_F);
this.send(request.toString()).catch((error) => {
this.logTransportError(error, "Failed to send initial outgoing request.");
});
}
/**
* Destructor.
*/
dispose() {
if (this.F) {
clearTimeout(this.F);
this.F = undefined;
}
if (this.K) {
clearTimeout(this.K);
this.K = undefined;
}
super.dispose();
}
/** Transaction kind. Deprecated. */
get kind() {
return "nict";
}
/**
* Handler for incoming responses from the transport which match this transaction.
* @param response - The incoming response.
*/
receiveResponse(response) {
const statusCode = response.statusCode;
if (!statusCode || statusCode < 100 || statusCode > 699) {
throw new Error(`Invalid status code ${statusCode}`);
}
switch (this.state) {
case TransactionState.Trying:
// If a provisional response is received while in the "Trying" state, the
// response MUST be passed to the TU, and then the client transaction
// SHOULD move to the "Proceeding" state.
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
if (statusCode >= 100 && statusCode <= 199) {
this.stateTransition(TransactionState.Proceeding);
if (this.user.receiveResponse) {
this.user.receiveResponse(response);
}
return;
}
// If a final response (status codes 200-699) is received while in the
// "Trying" state, the response MUST be passed to the TU, and the
// client transaction MUST transition to the "Completed" state.
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
if (statusCode >= 200 && statusCode <= 699) {
this.stateTransition(TransactionState.Completed);
if (statusCode === 408) {
this.onRequestTimeout();
return;
}
if (this.user.receiveResponse) {
this.user.receiveResponse(response);
}
return;
}
break;
case TransactionState.Proceeding:
// If a provisional response is received while in the "Proceeding" state,
// the response MUST be passed to the TU. (From Figure 6)
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
if (statusCode >= 100 && statusCode <= 199) {
if (this.user.receiveResponse) {
return this.user.receiveResponse(response);
}
}
// If a final response (status codes 200-699) is received while in the
// "Proceeding" state, the response MUST be passed to the TU, and the
// client transaction MUST transition to the "Completed" state.
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
if (statusCode >= 200 && statusCode <= 699) {
this.stateTransition(TransactionState.Completed);
if (statusCode === 408) {
this.onRequestTimeout();
return;
}
if (this.user.receiveResponse) {
this.user.receiveResponse(response);
}
return;
}
break;
case TransactionState.Completed:
// The "Completed" state exists to buffer any additional response
// retransmissions that may be received (which is why the client
// transaction remains there only for unreliable transports).
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
return;
case TransactionState.Terminated:
// For good measure just absorb additional response retransmissions.
return;
default:
throw new Error(`Invalid state ${this.state}`);
}
const message = `Non-INVITE client transaction received unexpected ${statusCode} response while in state ${this.state}.`;
this.logger.warn(message);
return;
}
/**
* The client transaction SHOULD inform the TU that a transport failure has occurred,
* and the client transaction SHOULD transition directly to the "Terminated" state.
* The TU will handle the fail over mechanisms described in [4].
* https://tools.ietf.org/html/rfc3261#section-17.1.4
* @param error - Transport error
*/
onTransportError(error) {
if (this.user.onTransportError) {
this.user.onTransportError(error);
}
this.stateTransition(TransactionState.Terminated, true);
}
/** For logging. */
typeToString() {
return "non-INVITE client transaction";
}
/**
* Execute a state transition.
* @param newState - New state.
*/
stateTransition(newState, dueToTransportError = false) {
// Assert valid state transitions.
const invalidStateTransition = () => {
throw new Error(`Invalid state transition from ${this.state} to ${newState}`);
};
switch (newState) {
case TransactionState.Trying:
invalidStateTransition();
break;
case TransactionState.Proceeding:
if (this.state !== TransactionState.Trying) {
invalidStateTransition();
}
break;
case TransactionState.Completed:
if (this.state !== TransactionState.Trying && this.state !== TransactionState.Proceeding) {
invalidStateTransition();
}
break;
case TransactionState.Terminated:
if (this.state !== TransactionState.Trying &&
this.state !== TransactionState.Proceeding &&
this.state !== TransactionState.Completed) {
if (!dueToTransportError) {
invalidStateTransition();
}
}
break;
default:
invalidStateTransition();
}
// Once the client transaction enters the "Completed" state, it MUST set
// Timer K to fire in T4 seconds for unreliable transports, and zero
// seconds for reliable transports The "Completed" state exists to
// buffer any additional response retransmissions that may be received
// (which is why the client transaction remains there only for unreliable transports).
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
if (newState === TransactionState.Completed) {
if (this.F) {
clearTimeout(this.F);
this.F = undefined;
}
this.K = setTimeout(() => this.timerK(), Timers.TIMER_K);
}
// Once the transaction is in the terminated state, it MUST be destroyed immediately.
// https://tools.ietf.org/html/rfc3261#section-17.1.2.2
if (newState === TransactionState.Terminated) {
this.dispose();
}
// Update state.
this.setState(newState);
}
/**
* If Timer F fires while the client transaction is still in the
* "Trying" state, the client transaction SHOULD inform the TU about the
* timeout, and then it SHOULD enter the "Terminated" state.
* If timer F fires while in the "Proceeding" state, the TU MUST be informed of
* a timeout, and the client transaction MUST transition to the terminated state.
* https://tools.ietf.org/html/rfc3261#section-17.1.2.2
*/
timerF() {
this.logger.debug(`Timer F expired for non-INVITE client transaction ${this.id}.`);
if (this.state === TransactionState.Trying || this.state === TransactionState.Proceeding) {
this.onRequestTimeout();
this.stateTransition(TransactionState.Terminated);
}
}
/**
* If Timer K fires while in this (COMPLETED) state, the client transaction
* MUST transition to the "Terminated" state.
* https://tools.ietf.org/html/rfc3261#section-17.1.2.2
*/
timerK() {
if (this.state === TransactionState.Completed) {
this.stateTransition(TransactionState.Terminated);
}
}
}