diff --git a/src/Dialog.js b/src/Dialog.js index 70ec58608..e2d259695 100644 --- a/src/Dialog.js +++ b/src/Dialog.js @@ -266,12 +266,10 @@ module.exports = class Dialog { const stateChanged = () => { if ( + request.server_transaction.state === Transactions.Status.ACCEPTED || request.server_transaction.state === - Transactions.C.STATUS_ACCEPTED || - request.server_transaction.state === - Transactions.C.STATUS_COMPLETED || - request.server_transaction.state === - Transactions.C.STATUS_TERMINATED + Transactions.Status.COMPLETED || + request.server_transaction.state === Transactions.Status.TERMINATED ) { request.server_transaction.removeListener( 'stateChanged', @@ -288,7 +286,7 @@ module.exports = class Dialog { if (request.hasHeader('contact')) { request.server_transaction.on('stateChanged', () => { if ( - request.server_transaction.state === Transactions.C.STATUS_ACCEPTED + request.server_transaction.state === Transactions.Status.ACCEPTED ) { this._remote_target = request.parseHeader('contact').uri; } @@ -299,7 +297,7 @@ module.exports = class Dialog { if (request.hasHeader('contact')) { request.server_transaction.on('stateChanged', () => { if ( - request.server_transaction.state === Transactions.C.STATUS_COMPLETED + request.server_transaction.state === Transactions.Status.COMPLETED ) { this._remote_target = request.parseHeader('contact').uri; } diff --git a/src/Dialog/RequestSender.js b/src/Dialog/RequestSender.js index adeeb393d..95ab5b10e 100644 --- a/src/Dialog/RequestSender.js +++ b/src/Dialog/RequestSender.js @@ -59,19 +59,18 @@ module.exports = class DialogRequestSender { if ( (this._request.method === JsSIP_C.INVITE || (this._request.method === JsSIP_C.UPDATE && this._request.body)) && - request_sender.clientTransaction.state !== - Transactions.C.STATUS_TERMINATED + request_sender.clientTransaction.state !== Transactions.Status.TERMINATED ) { this._dialog.uac_pending_reply = true; const stateChanged = () => { if ( request_sender.clientTransaction.state === - Transactions.C.STATUS_ACCEPTED || + Transactions.Status.ACCEPTED || request_sender.clientTransaction.state === - Transactions.C.STATUS_COMPLETED || + Transactions.Status.COMPLETED || request_sender.clientTransaction.state === - Transactions.C.STATUS_TERMINATED + Transactions.Status.TERMINATED ) { request_sender.clientTransaction.removeListener( 'stateChanged', diff --git a/src/RTCSession.js b/src/RTCSession.js index 97e17459c..f6c8b8b27 100644 --- a/src/RTCSession.js +++ b/src/RTCSession.js @@ -843,7 +843,7 @@ module.exports = class RTCSession extends EventEmitter { this._status === C.STATUS_WAITING_FOR_ACK && this._direction === 'incoming' && this._request.server_transaction.state !== - Transactions.C.STATUS_TERMINATED + Transactions.Status.TERMINATED ) { // Save the dialog for later restoration. const dialog = this._dialog; @@ -863,7 +863,7 @@ module.exports = class RTCSession extends EventEmitter { this._request.server_transaction.on('stateChanged', () => { if ( this._request.server_transaction.state === - Transactions.C.STATUS_TERMINATED + Transactions.Status.TERMINATED ) { this.sendRequest(JsSIP_C.BYE, { extraHeaders, diff --git a/src/Transactions.js b/src/Transactions.ts similarity index 50% rename from src/Transactions.js rename to src/Transactions.ts index 8cc73a281..b94d9ea57 100644 --- a/src/Transactions.js +++ b/src/Transactions.ts @@ -1,8 +1,10 @@ -const EventEmitter = require('events').EventEmitter; -const { Logger } = require('./Logger'); -const JsSIP_C = require('./Constants'); -const SIPMessage = require('./SIPMessage'); -const Timers = require('./Timers'); +import { EventEmitter } from 'events'; + +import * as JsSIP_C from './Constants'; +import { Logger } from './Logger'; +import * as SIPMessage from './SIPMessage'; +import * as Timers from './Timers'; +import type { Transport } from './Transport'; const loggernict = new Logger('NonInviteClientTransaction'); const loggerict = new Logger('InviteClientTransaction'); @@ -10,28 +12,101 @@ const loggeract = new Logger('AckClientTransaction'); const loggernist = new Logger('NonInviteServerTransaction'); const loggerist = new Logger('InviteServerTransaction'); -const C = { - // Transaction states. - STATUS_TRYING: 1, - STATUS_PROCEEDING: 2, - STATUS_CALLING: 3, - STATUS_ACCEPTED: 4, - STATUS_COMPLETED: 5, - STATUS_TERMINATED: 6, - STATUS_CONFIRMED: 7, - - // Transaction types. - NON_INVITE_CLIENT: 'nict', - NON_INVITE_SERVER: 'nist', - INVITE_CLIENT: 'ict', - INVITE_SERVER: 'ist', -}; - -class NonInviteClientTransaction extends EventEmitter { - constructor(ua, transport, request, eventHandlers) { +/** + * Transaction states. + */ +export enum Status { + TRYING = 1, + PROCEEDING = 2, + CALLING = 3, + ACCEPTED = 4, + COMPLETED = 5, + TERMINATED = 6, + CONFIRMED = 7, +} + +/** + * Transaction types, each also being the key of the corresponding registry in + * the UA. + */ +export enum Type { + NON_INVITE_CLIENT = 'nict', + NON_INVITE_SERVER = 'nist', + INVITE_CLIENT = 'ict', + INVITE_SERVER = 'ist', +} + +/** + * The callbacks a client transaction reports to whoever sent the request. + */ +export interface ClientTransactionEventHandlers { + onRequestTimeout(): void; + onTransportError(): void; + onReceiveResponse(response: SIPMessage.IncomingResponse): void; +} + +/** + * Any transaction held in the UA transaction tables. + */ +export type Transaction = + | NonInviteClientTransaction + | InviteClientTransaction + | NonInviteServerTransaction + | InviteServerTransaction; + +/** + * The UA transaction tables, indexed by transaction id (the Via branch). + */ +export interface TransactionRegistry { + nict: Record; + nist: Record; + ict: Record; + ist: Record; +} + +/** + * The `UA` surface transactions depend on. + * + * TODO: Replace with `import type { UA } from './UA'` once `UA` is ported to + * TypeScript. It is declared structurally (on top of the `UALike` that + * `SIPMessage` already needs) so that this module does not depend on the + * still JavaScript `UA` module. + */ +export interface TransactionUA extends SIPMessage.UALike { + readonly configuration: SIPMessage.UALike['configuration'] & { + via_host: string; + }; + + newTransaction(transaction: Transaction): void; + destroyTransaction(transaction: Transaction): void; +} + +export class NonInviteClientTransaction extends EventEmitter { + type: Type; + id: string; + ua: TransactionUA; + transport: Transport; + request: SIPMessage.OutgoingRequest; + eventHandlers: ClientTransactionEventHandlers; + + /* NOTE: `declare`d (and so not emitted) because the original implementation + * does not create these own properties in the constructor. `state` is + * created by `stateChanged()` and the timer handles when their timers are + * armed. + */ + state?: Status; + F?: ReturnType; + K?: ReturnType; + + constructor( + ua: TransactionUA, + transport: Transport, + request: SIPMessage.OutgoingRequest, + eventHandlers: ClientTransactionEventHandlers + ) { super(); - this.type = C.NON_INVITE_CLIENT; + this.type = Type.NON_INVITE_CLIENT; this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`; this.ua = ua; this.transport = transport; @@ -47,17 +122,13 @@ class NonInviteClientTransaction extends EventEmitter { this.ua.newTransaction(this); } - get C() { - return C; - } - - stateChanged(state) { + stateChanged(state: Status): void { this.state = state; this.emit('stateChanged'); } - send() { - this.stateChanged(C.STATUS_TRYING); + send(): void { + this.stateChanged(Status.TRYING); this.F = setTimeout(() => { this.timer_F(); }, Timers.TIMER_F); @@ -67,46 +138,46 @@ class NonInviteClientTransaction extends EventEmitter { } } - onTransportError() { + onTransportError(): void { loggernict.debug( `transport error occurred, deleting transaction ${this.id}` ); clearTimeout(this.F); clearTimeout(this.K); - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); this.eventHandlers.onTransportError(); } - timer_F() { + timer_F(): void { loggernict.debug(`Timer F expired for transaction ${this.id}`); - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); this.eventHandlers.onRequestTimeout(); } - timer_K() { - this.stateChanged(C.STATUS_TERMINATED); + timer_K(): void { + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } - receiveResponse(response) { + receiveResponse(response: SIPMessage.IncomingResponse): void { const status_code = response.status_code; if (status_code < 200) { switch (this.state) { - case C.STATUS_TRYING: - case C.STATUS_PROCEEDING: { - this.stateChanged(C.STATUS_PROCEEDING); + case Status.TRYING: + case Status.PROCEEDING: { + this.stateChanged(Status.PROCEEDING); this.eventHandlers.onReceiveResponse(response); break; } } } else { switch (this.state) { - case C.STATUS_TRYING: - case C.STATUS_PROCEEDING: { - this.stateChanged(C.STATUS_COMPLETED); + case Status.TRYING: + case Status.PROCEEDING: { + this.stateChanged(Status.COMPLETED); clearTimeout(this.F); if (status_code === 408) { @@ -120,7 +191,7 @@ class NonInviteClientTransaction extends EventEmitter { }, Timers.TIMER_K); break; } - case C.STATUS_COMPLETED: { + case Status.COMPLETED: { break; } } @@ -128,11 +199,33 @@ class NonInviteClientTransaction extends EventEmitter { } } -class InviteClientTransaction extends EventEmitter { - constructor(ua, transport, request, eventHandlers) { +export class InviteClientTransaction extends EventEmitter { + type: Type; + id: string; + ua: TransactionUA; + transport: Transport; + request: SIPMessage.OutgoingRequest; + eventHandlers: ClientTransactionEventHandlers; + + /* NOTE: `declare`d (and so not emitted) because the original implementation + * does not create these own properties in the constructor. `state` is + * created by `stateChanged()` and the timer handles when their timers are + * armed. + */ + state?: Status; + B?: ReturnType; + D?: ReturnType; + M?: ReturnType; + + constructor( + ua: TransactionUA, + transport: Transport, + request: SIPMessage.OutgoingRequest, + eventHandlers: ClientTransactionEventHandlers + ) { super(); - this.type = C.INVITE_CLIENT; + this.type = Type.INVITE_CLIENT; this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`; this.ua = ua; this.transport = transport; @@ -149,17 +242,13 @@ class InviteClientTransaction extends EventEmitter { this.ua.newTransaction(this); } - get C() { - return C; - } - - stateChanged(state) { + stateChanged(state: Status): void { this.state = state; this.emit('stateChanged'); } - send() { - this.stateChanged(C.STATUS_CALLING); + send(): void { + this.stateChanged(Status.CALLING); this.B = setTimeout(() => { this.timer_B(); }, Timers.TIMER_B); @@ -169,65 +258,69 @@ class InviteClientTransaction extends EventEmitter { } } - onTransportError() { + onTransportError(): void { clearTimeout(this.B); clearTimeout(this.D); clearTimeout(this.M); - if (this.state !== C.STATUS_ACCEPTED) { + if (this.state !== Status.ACCEPTED) { loggerict.debug( `transport error occurred, deleting transaction ${this.id}` ); this.eventHandlers.onTransportError(); } - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } // RFC 6026 7.2. - timer_M() { + timer_M(): void { loggerict.debug(`Timer M expired for transaction ${this.id}`); - if (this.state === C.STATUS_ACCEPTED) { + if (this.state === Status.ACCEPTED) { clearTimeout(this.B); - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } } // RFC 3261 17.1.1. - timer_B() { + timer_B(): void { loggerict.debug(`Timer B expired for transaction ${this.id}`); - if (this.state === C.STATUS_CALLING) { - this.stateChanged(C.STATUS_TERMINATED); + if (this.state === Status.CALLING) { + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); this.eventHandlers.onRequestTimeout(); } } - timer_D() { + timer_D(): void { loggerict.debug(`Timer D expired for transaction ${this.id}`); clearTimeout(this.B); - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } - sendACK(response) { + sendACK(response: SIPMessage.IncomingResponse): void { + /* NOTE: The non-null assertions below just reproduce the original + * behaviour: the header getters are typed as possibly `undefined` but + * their value is forwarded unconditionally. + */ const ack = new SIPMessage.OutgoingRequest( JsSIP_C.ACK, this.request.ruri, this.ua, { route_set: this.request.getHeaders('route'), - call_id: this.request.getHeader('call-id'), + call_id: this.request.getHeader('call-id') as string, cseq: this.request.cseq, } ); - ack.setHeader('from', this.request.getHeader('from')); - ack.setHeader('via', this.request.getHeader('via')); - ack.setHeader('to', response.getHeader('to')); + ack.setHeader('from', this.request.getHeader('from')!); + ack.setHeader('via', this.request.getHeader('via')!); + ack.setHeader('to', response.getHeader('to')!); this.D = setTimeout(() => { this.timer_D(); @@ -236,9 +329,9 @@ class InviteClientTransaction extends EventEmitter { this.transport.send(ack); } - cancel(reason) { + cancel(reason?: string): void { // Send only if a provisional response (>100) has been received. - if (this.state !== C.STATUS_PROCEEDING) { + if (this.state !== Status.PROCEEDING) { return; } @@ -248,14 +341,14 @@ class InviteClientTransaction extends EventEmitter { this.ua, { route_set: this.request.getHeaders('route'), - call_id: this.request.getHeader('call-id'), + call_id: this.request.getHeader('call-id') as string, cseq: this.request.cseq, } ); - cancel.setHeader('from', this.request.getHeader('from')); - cancel.setHeader('via', this.request.getHeader('via')); - cancel.setHeader('to', this.request.getHeader('to')); + cancel.setHeader('from', this.request.getHeader('from')!); + cancel.setHeader('via', this.request.getHeader('via')!); + cancel.setHeader('to', this.request.getHeader('to')!); if (reason) { cancel.setHeader('reason', reason); @@ -264,47 +357,47 @@ class InviteClientTransaction extends EventEmitter { this.transport.send(cancel); } - receiveResponse(response) { + receiveResponse(response: SIPMessage.IncomingResponse): void { const status_code = response.status_code; if (status_code >= 100 && status_code <= 199) { switch (this.state) { - case C.STATUS_CALLING: { - this.stateChanged(C.STATUS_PROCEEDING); + case Status.CALLING: { + this.stateChanged(Status.PROCEEDING); this.eventHandlers.onReceiveResponse(response); break; } - case C.STATUS_PROCEEDING: { + case Status.PROCEEDING: { this.eventHandlers.onReceiveResponse(response); break; } } } else if (status_code >= 200 && status_code <= 299) { switch (this.state) { - case C.STATUS_CALLING: - case C.STATUS_PROCEEDING: { - this.stateChanged(C.STATUS_ACCEPTED); + case Status.CALLING: + case Status.PROCEEDING: { + this.stateChanged(Status.ACCEPTED); this.M = setTimeout(() => { this.timer_M(); }, Timers.TIMER_M); this.eventHandlers.onReceiveResponse(response); break; } - case C.STATUS_ACCEPTED: { + case Status.ACCEPTED: { this.eventHandlers.onReceiveResponse(response); break; } } } else if (status_code >= 300 && status_code <= 699) { switch (this.state) { - case C.STATUS_CALLING: - case C.STATUS_PROCEEDING: { - this.stateChanged(C.STATUS_COMPLETED); + case Status.CALLING: + case Status.PROCEEDING: { + this.stateChanged(Status.COMPLETED); this.sendACK(response); this.eventHandlers.onReceiveResponse(response); break; } - case C.STATUS_COMPLETED: { + case Status.COMPLETED: { this.sendACK(response); break; } @@ -313,8 +406,18 @@ class InviteClientTransaction extends EventEmitter { } } -class AckClientTransaction extends EventEmitter { - constructor(ua, transport, request, eventHandlers) { +export class AckClientTransaction extends EventEmitter { + id: string; + transport: Transport; + request: SIPMessage.OutgoingRequest; + eventHandlers: ClientTransactionEventHandlers; + + constructor( + ua: TransactionUA, + transport: Transport, + request: SIPMessage.OutgoingRequest, + eventHandlers: ClientTransactionEventHandlers + ) { super(); this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`; @@ -329,55 +432,65 @@ class AckClientTransaction extends EventEmitter { this.request.setHeader('via', via); } - get C() { - return C; - } - - send() { + send(): void { if (!this.transport.send(this.request)) { this.onTransportError(); } } - onTransportError() { + onTransportError(): void { loggeract.debug(`transport error occurred for transaction ${this.id}`); this.eventHandlers.onTransportError(); } } -class NonInviteServerTransaction extends EventEmitter { - constructor(ua, transport, request) { +export class NonInviteServerTransaction extends EventEmitter { + type: Type; + id: string; + ua: TransactionUA; + transport: Transport; + request: SIPMessage.IncomingRequest; + last_response: string; + state: Status; + + /* NOTE: `declare`d (and so not emitted) because the original implementation + * does not create these own properties in the constructor. + */ + transportError?: boolean; + J?: ReturnType; + + constructor( + ua: TransactionUA, + transport: Transport, + request: SIPMessage.IncomingRequest + ) { super(); - this.type = C.NON_INVITE_SERVER; - this.id = request.via_branch; + this.type = Type.NON_INVITE_SERVER; + this.id = request.via_branch!; this.ua = ua; this.transport = transport; this.request = request; this.last_response = ''; request.server_transaction = this; - this.state = C.STATUS_TRYING; + this.state = Status.TRYING; ua.newTransaction(this); } - get C() { - return C; - } - - stateChanged(state) { + stateChanged(state: Status): void { this.state = state; this.emit('stateChanged'); } - timer_J() { + timer_J(): void { loggernist.debug(`Timer J expired for transaction ${this.id}`); - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } - onTransportError() { + onTransportError(): void { if (!this.transportError) { this.transportError = true; @@ -386,12 +499,17 @@ class NonInviteServerTransaction extends EventEmitter { ); clearTimeout(this.J); - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } } - receiveResponse(status_code, response, onSuccess, onFailure) { + receiveResponse( + status_code: number, + response: string, + onSuccess?: () => void, + onFailure?: () => void + ): void { if (status_code === 100) { /* RFC 4320 4.1 * 'A SIP element MUST NOT @@ -399,14 +517,14 @@ class NonInviteServerTransaction extends EventEmitter { * Status-Code other than 100 to a non-INVITE request.' */ switch (this.state) { - case C.STATUS_TRYING: { - this.stateChanged(C.STATUS_PROCEEDING); + case Status.TRYING: { + this.stateChanged(Status.PROCEEDING); if (!this.transport.send(response)) { this.onTransportError(); } break; } - case C.STATUS_PROCEEDING: { + case Status.PROCEEDING: { this.last_response = response; if (!this.transport.send(response)) { this.onTransportError(); @@ -421,9 +539,9 @@ class NonInviteServerTransaction extends EventEmitter { } } else if (status_code >= 200 && status_code <= 699) { switch (this.state) { - case C.STATUS_TRYING: - case C.STATUS_PROCEEDING: { - this.stateChanged(C.STATUS_COMPLETED); + case Status.TRYING: + case Status.PROCEEDING: { + this.stateChanged(Status.COMPLETED); this.last_response = response; this.J = setTimeout(() => { this.timer_J(); @@ -438,7 +556,7 @@ class NonInviteServerTransaction extends EventEmitter { } break; } - case C.STATUS_COMPLETED: { + case Status.COMPLETED: { break; } } @@ -446,19 +564,40 @@ class NonInviteServerTransaction extends EventEmitter { } } -class InviteServerTransaction extends EventEmitter { - constructor(ua, transport, request) { +export class InviteServerTransaction extends EventEmitter { + type: Type; + id: string; + ua: TransactionUA; + transport: Transport; + request: SIPMessage.IncomingRequest; + last_response: string; + state: Status; + resendProvisionalTimer: ReturnType | null; + + /* NOTE: `declare`d (and so not emitted) because the original implementation + * does not create these own properties in the constructor. + */ + transportError?: boolean; + H?: ReturnType; + I?: ReturnType; + L?: ReturnType; + + constructor( + ua: TransactionUA, + transport: Transport, + request: SIPMessage.IncomingRequest + ) { super(); - this.type = C.INVITE_SERVER; - this.id = request.via_branch; + this.type = Type.INVITE_SERVER; + this.id = request.via_branch!; this.ua = ua; this.transport = transport; this.request = request; this.last_response = ''; request.server_transaction = this; - this.state = C.STATUS_PROCEEDING; + this.state = Status.PROCEEDING; ua.newTransaction(this); @@ -467,42 +606,38 @@ class InviteServerTransaction extends EventEmitter { request.reply(100); } - get C() { - return C; - } - - stateChanged(state) { + stateChanged(state: Status): void { this.state = state; this.emit('stateChanged'); } - timer_H() { + timer_H(): void { loggerist.debug(`Timer H expired for transaction ${this.id}`); - if (this.state === C.STATUS_COMPLETED) { + if (this.state === Status.COMPLETED) { loggerist.debug('ACK not received, dialog will be terminated'); } - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } - timer_I() { - this.stateChanged(C.STATUS_TERMINATED); + timer_I(): void { + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } // RFC 6026 7.1. - timer_L() { + timer_L(): void { loggerist.debug(`Timer L expired for transaction ${this.id}`); - if (this.state === C.STATUS_ACCEPTED) { - this.stateChanged(C.STATUS_TERMINATED); + if (this.state === Status.ACCEPTED) { + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } } - onTransportError() { + onTransportError(): void { if (!this.transportError) { this.transportError = true; @@ -519,22 +654,27 @@ class InviteServerTransaction extends EventEmitter { clearTimeout(this.H); clearTimeout(this.I); - this.stateChanged(C.STATUS_TERMINATED); + this.stateChanged(Status.TERMINATED); this.ua.destroyTransaction(this); } } - resend_provisional() { + resend_provisional(): void { if (!this.transport.send(this.last_response)) { this.onTransportError(); } } // INVITE Server Transaction RFC 3261 17.2.1. - receiveResponse(status_code, response, onSuccess, onFailure) { + receiveResponse( + status_code: number, + response: string, + onSuccess?: () => void, + onFailure?: () => void + ): void { if (status_code >= 100 && status_code <= 199) { switch (this.state) { - case C.STATUS_PROCEEDING: { + case Status.PROCEEDING: { if (!this.transport.send(response)) { this.onTransportError(); } @@ -547,7 +687,7 @@ class InviteServerTransaction extends EventEmitter { if ( status_code > 100 && status_code <= 199 && - this.state === C.STATUS_PROCEEDING + this.state === Status.PROCEEDING ) { // Trigger the resendProvisionalTimer only for the first non 100 provisional response. if (this.resendProvisionalTimer === null) { @@ -556,21 +696,30 @@ class InviteServerTransaction extends EventEmitter { }, Timers.PROVISIONAL_RESPONSE_INTERVAL); } } else if (status_code >= 200 && status_code <= 299) { - switch (this.state) { - case C.STATUS_PROCEEDING: { - this.stateChanged(C.STATUS_ACCEPTED); - this.last_response = response; - this.L = setTimeout(() => { - this.timer_L(); - }, Timers.TIMER_L); + /* NOTE: The original implementation lets the `Status.PROCEEDING` + * case fall through into the `Status.ACCEPTED` one. TypeScript's + * `noFallthroughCasesInSwitch` forbids that, so the proceeding part + * is guarded by the state read before entering the switch, which is + * exactly when the original evaluates the switch discriminant. + */ + const was_proceeding = this.state === Status.PROCEEDING; - if (this.resendProvisionalTimer !== null) { - clearInterval(this.resendProvisionalTimer); - this.resendProvisionalTimer = null; + switch (this.state) { + case Status.PROCEEDING: + case Status.ACCEPTED: { + if (was_proceeding) { + this.stateChanged(Status.ACCEPTED); + this.last_response = response; + this.L = setTimeout(() => { + this.timer_L(); + }, Timers.TIMER_L); + + if (this.resendProvisionalTimer !== null) { + clearInterval(this.resendProvisionalTimer); + this.resendProvisionalTimer = null; + } } - } - // falls through - case C.STATUS_ACCEPTED: { + // Note that this point will be reached for proceeding this.state also. if (!this.transport.send(response)) { this.onTransportError(); @@ -585,7 +734,7 @@ class InviteServerTransaction extends EventEmitter { } } else if (status_code >= 300 && status_code <= 699) { switch (this.state) { - case C.STATUS_PROCEEDING: { + case Status.PROCEEDING: { if (this.resendProvisionalTimer !== null) { clearInterval(this.resendProvisionalTimer); this.resendProvisionalTimer = null; @@ -597,7 +746,7 @@ class InviteServerTransaction extends EventEmitter { onFailure(); } } else { - this.stateChanged(C.STATUS_COMPLETED); + this.stateChanged(Status.COMPLETED); this.H = setTimeout(() => { this.timer_H(); }, Timers.TIMER_H); @@ -630,22 +779,28 @@ class InviteServerTransaction extends EventEmitter { * _true_ retransmission * _false_ new request */ -function checkTransaction({ _transactions }, request) { - let tr; - +export function checkTransaction( + { _transactions }: { _transactions: TransactionRegistry }, + request: SIPMessage.IncomingRequest +): boolean | undefined { + /* NOTE: The original implementation uses a single `let tr` declared before + * the switch. It is declared per case here so that each one gets the + * matching transaction type; the variable was never read across cases. + */ switch (request.method) { case JsSIP_C.INVITE: { - tr = _transactions.ist[request.via_branch]; + const tr = _transactions.ist[request.via_branch!]; + if (tr) { switch (tr.state) { - case C.STATUS_PROCEEDING: { + case Status.PROCEEDING: { tr.transport.send(tr.last_response); break; } // RFC 6026 7.1 Invite retransmission. - // Received while in C.STATUS_ACCEPTED state. Absorb it. - case C.STATUS_ACCEPTED: { + // Received while in Status.ACCEPTED state. Absorb it. + case Status.ACCEPTED: { break; } } @@ -655,14 +810,14 @@ function checkTransaction({ _transactions }, request) { break; } case JsSIP_C.ACK: { - tr = _transactions.ist[request.via_branch]; + const tr = _transactions.ist[request.via_branch!]; // RFC 6026 7.1. if (tr) { - if (tr.state === C.STATUS_ACCEPTED) { + if (tr.state === Status.ACCEPTED) { return false; - } else if (tr.state === C.STATUS_COMPLETED) { - tr.state = C.STATUS_CONFIRMED; + } else if (tr.state === Status.COMPLETED) { + tr.state = Status.CONFIRMED; tr.I = setTimeout(() => { tr.timer_I(); }, Timers.TIMER_I); @@ -677,10 +832,11 @@ function checkTransaction({ _transactions }, request) { break; } case JsSIP_C.CANCEL: { - tr = _transactions.ist[request.via_branch]; + const tr = _transactions.ist[request.via_branch!]; + if (tr) { request.reply_sl(200); - if (tr.state === C.STATUS_PROCEEDING) { + if (tr.state === Status.PROCEEDING) { return false; } else { return true; @@ -693,14 +849,15 @@ function checkTransaction({ _transactions }, request) { } default: { // Non-INVITE Server Transaction RFC 3261 17.2.2. - tr = _transactions.nist[request.via_branch]; + const tr = _transactions.nist[request.via_branch!]; + if (tr) { switch (tr.state) { - case C.STATUS_TRYING: { + case Status.TRYING: { break; } - case C.STATUS_PROCEEDING: - case C.STATUS_COMPLETED: { + case Status.PROCEEDING: + case Status.COMPLETED: { tr.transport.send(tr.last_response); break; } @@ -711,14 +868,6 @@ function checkTransaction({ _transactions }, request) { break; } } -} -module.exports = { - C, - NonInviteClientTransaction, - InviteClientTransaction, - AckClientTransaction, - NonInviteServerTransaction, - InviteServerTransaction, - checkTransaction, -}; + return undefined; +}