Commit b847744 for jssip.net

commit b847744deaa7e2b1148d99bdc5e04b8322407796
Author: José Luis Millán <jmillan@aliax.net>
Date:   Tue Oct 20 17:53:56 2026 +0200

    TS: Transactions (#988)

diff --git a/src/Dialog.js b/src/Dialog.js
index 70ec586..e2d2596 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 adeeb39..95ab5b1 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 97e1745..f6c8b8b 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 8cc73a2..b94d9ea 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<string, NonInviteClientTransaction>;
+	nist: Record<string, NonInviteServerTransaction>;
+	ict: Record<string, InviteClientTransaction>;
+	ist: Record<string, InviteServerTransaction>;
+}
+
+/**
+ * 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<typeof setTimeout>;
+	K?: ReturnType<typeof setTimeout>;
+
+	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<typeof setTimeout>;
+	D?: ReturnType<typeof setTimeout>;
+	M?: ReturnType<typeof setTimeout>;
+
+	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<typeof setTimeout>;
+
+	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<typeof setInterval> | 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<typeof setTimeout>;
+	I?: ReturnType<typeof setTimeout>;
+	L?: ReturnType<typeof setTimeout>;
+
+	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;
+}