Commit bdfafb4 for jssip.net

commit bdfafb47a9fa7c18f61a7d64e777ec166c13c345
Author: José Luis Millán <jmillan@aliax.net>
Date:   Mon Jan 26 16:13:57 2026 +0100

    Modernize (#955)

    - Moderlize eslint
    - Use prettier
    - Prepare environment for TS

diff --git a/.gitignore b/.gitignore
index b0a5c34..216bf0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
-/node_modules/
-/dist/
+/node_modules
+/lib
+/dist
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..a824e54
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,10 @@
+{
+	"useTabs": true,
+	"tabWidth": 2,
+	"arrowParens": "avoid",
+	"bracketSpacing": true,
+	"semi": true,
+	"singleQuote": true,
+	"trailingComma": "es5",
+	"endOfLine": "auto"
+}
diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
index aaf9247..0000000
--- a/eslint.config.js
+++ /dev/null
@@ -1,245 +0,0 @@
-const {
-    defineConfig,
-    globalIgnores,
-} = require("eslint/config");
-
-const globals = require("globals");
-const js = require("@eslint/js");
-
-const {
-    FlatCompat,
-} = require("@eslint/eslintrc");
-
-const compat = new FlatCompat({
-    baseDirectory: __dirname,
-    recommendedConfig: js.configs.recommended,
-    allConfig: js.configs.all
-});
-
-module.exports = defineConfig([
-    {
-        files: ['test/**/*.js'], // Apply these settings only to test files
-        languageOptions: {
-            globals: {
-                jest: true, // Enables Jest global variables like `test`, `describe`, and `expect`
-                describe: true, // TMP: explicitly enable globally.
-                test: true, // TMP: explicitly enable globally.
-                expect: true, // TMP: explicitly enable globally.
-            },
-        },
-        plugins: {
-            jest: require('eslint-plugin-jest'), // Load the Jest plugin
-        },
-        rules: {
-            ...require('eslint-plugin-jest').configs.recommended.rules, // Use Jest's recommended rules
-        },
-    },
-    {
-        languageOptions: {
-            globals: {
-            ...globals.browser,
-            ...globals.node,
-        },
-
-        ecmaVersion: 6,
-        sourceType: "module",
-
-        parserOptions: {
-            ecmaFeatures: {
-                impliedStrict: true,
-            },
-        },
-    },
-
-    plugins: {},
-    extends: compat.extends("eslint:recommended"),
-    settings: {},
-
-    rules: {
-        "array-bracket-spacing": [2, "always", {
-            objectsInArrays: true,
-            arraysInArrays: true,
-        }],
-
-        "arrow-parens": [2, "always"],
-        "arrow-spacing": 2,
-        "block-spacing": [2, "always"],
-
-        "brace-style": [2, "allman", {
-            allowSingleLine: true,
-        }],
-
-        "camelcase": 0,
-        "comma-dangle": 2,
-
-        "comma-spacing": [2, {
-            before: false,
-            after: true,
-        }],
-
-        "comma-style": 2,
-        "computed-property-spacing": 2,
-        "constructor-super": 2,
-        "func-call-spacing": 2,
-        "generator-star-spacing": 2,
-        "guard-for-in": 2,
-
-        "indent": [2, 2, {
-            "SwitchCase": 1,
-        }],
-
-        "key-spacing": [2, {
-            singleLine: {
-                beforeColon: false,
-                afterColon: true,
-            },
-
-            multiLine: {
-                beforeColon: true,
-                afterColon: true,
-                align: "colon",
-            },
-        }],
-
-        "keyword-spacing": 2,
-        "linebreak-style": [2, "unix"],
-
-        "lines-around-comment": [2, {
-            allowBlockStart: true,
-            allowObjectStart: true,
-            beforeBlockComment: true,
-            beforeLineComment: false,
-        }],
-
-        "max-len": [2, 90, {
-            tabWidth: 2,
-            comments: 110,
-            ignoreUrls: true,
-            ignoreStrings: true,
-            ignoreTemplateLiterals: true,
-            ignoreRegExpLiterals: true,
-        }],
-
-        "newline-after-var": 2,
-        "newline-before-return": 2,
-        "newline-per-chained-call": 2,
-        "no-alert": 2,
-        "no-caller": 2,
-        "no-case-declarations": 2,
-        "no-catch-shadow": 2,
-        "no-class-assign": 2,
-        "no-confusing-arrow": 2,
-
-        "no-console": [2, {
-            allow: ["warn"],
-        }],
-
-        "no-const-assign": 2,
-
-        "no-constant-condition": [2, {
-            "checkLoops": false,
-        }],
-
-        "no-debugger": 2,
-        "no-dupe-args": 2,
-        "no-dupe-keys": 2,
-        "no-duplicate-case": 2,
-        "no-div-regex": 2,
-
-        "no-empty": [2, {
-            allowEmptyCatch: true,
-        }],
-
-        "no-empty-pattern": 2,
-        "no-else-return": 0,
-        "no-eval": 2,
-        "no-extend-native": 2,
-        "no-ex-assign": 2,
-        "no-extra-bind": 2,
-        "no-extra-boolean-cast": 2,
-        "no-extra-label": 2,
-        "no-extra-semi": 2,
-        "no-fallthrough": 2,
-        "no-func-assign": 2,
-        "no-global-assign": 2,
-        "no-implicit-coercion": 2,
-        "no-implicit-globals": 2,
-        "no-inner-declarations": 2,
-        "no-invalid-regexp": 2,
-        "no-invalid-this": 0,
-        "no-irregular-whitespace": 2,
-        "no-lonely-if": 2,
-        "no-mixed-operators": 2,
-        "no-mixed-spaces-and-tabs": 2,
-        "no-multi-spaces": 2,
-        "no-multi-str": 2,
-        "no-multiple-empty-lines": 2,
-        "no-native-reassign": 2,
-        "no-negated-in-lhs": 2,
-        "no-new": 2,
-        "no-new-func": 2,
-        "no-new-wrappers": 2,
-        "no-obj-calls": 2,
-        "no-proto": 2,
-        "no-prototype-builtins": 0,
-        "no-redeclare": 2,
-        "no-regex-spaces": 2,
-        "no-restricted-imports": 2,
-        "no-return-assign": 2,
-        "no-self-assign": 2,
-        "no-self-compare": 2,
-        "no-sequences": 2,
-        "no-shadow": 2,
-        "no-shadow-restricted-names": 2,
-        "no-spaced-func": 2,
-        "no-sparse-arrays": 2,
-        "no-this-before-super": 2,
-        "no-throw-literal": 2,
-        "no-trailing-spaces": 2,
-        "no-undef": 2,
-        "no-unexpected-multiline": 2,
-        "no-unmodified-loop-condition": 2,
-        "no-unreachable": 2,
-
-        "no-unused-vars": [1, {
-            vars: "all",
-            args: "after-used",
-        }],
-
-        "no-use-before-define": [2, {
-            functions: false,
-        }],
-
-        "no-useless-call": 2,
-        "no-useless-computed-key": 2,
-        "no-useless-concat": 2,
-        "no-useless-rename": 2,
-        "no-var": 2,
-        "no-whitespace-before-property": 2,
-        "object-curly-newline": 0,
-        "object-curly-spacing": [2, "always"],
-
-        "object-property-newline": [2, {
-            allowMultiplePropertiesPerLine: true,
-        }],
-
-        "prefer-const": 2,
-        "prefer-rest-params": 2,
-        "prefer-spread": 2,
-        "prefer-template": 2,
-
-        "quotes": [2, "single", {
-            avoidEscape: true,
-        }],
-
-        "semi": [2, "always"],
-        "semi-spacing": 2,
-        "space-before-blocks": 2,
-        "space-before-function-paren": [2, "never"],
-        "space-in-parens": [2, "never"],
-        "spaced-comment": [2, "always"],
-        "strict": 2,
-        "valid-typeof": 2,
-        "yoda": 2,
-    },
-}, globalIgnores(["src/Grammar.js"])]);
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..e23db21
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,186 @@
+import eslint from '@eslint/js';
+import tsEslint from 'typescript-eslint';
+import jestEslint from 'eslint-plugin-jest';
+import prettierRecommendedEslint from 'eslint-plugin-prettier/recommended';
+import globals from 'globals';
+
+const config = tsEslint.config(
+	{
+		languageOptions: {
+			sourceType: 'module',
+			globals: { ...globals.node, ...globals.jest },
+		},
+		linterOptions: {
+			noInlineConfig: false,
+			reportUnusedDisableDirectives: 'error',
+		},
+	},
+	eslint.configs.recommended,
+	prettierRecommendedEslint,
+	{
+		rules: {
+			'constructor-super': 2,
+			curly: [2, 'all'],
+			// Unfortunatelly `curly` does not apply to blocks in `switch` cases so
+			// this is needed.
+			'no-restricted-syntax': [
+				2,
+				{
+					selector: 'SwitchCase > *.consequent[type!="BlockStatement"]',
+					message: 'Switch cases without blocks are disallowed',
+				},
+			],
+			'guard-for-in': 2,
+			'newline-after-var': 2,
+			'newline-before-return': 2,
+			'no-alert': 2,
+			'no-caller': 2,
+			'no-case-declarations': 2,
+			'no-catch-shadow': 2,
+			'no-class-assign': 2,
+			'no-console': 2,
+			'no-const-assign': 2,
+			'no-debugger': 2,
+			'no-dupe-args': 2,
+			'no-dupe-keys': 2,
+			'no-duplicate-case': 2,
+			'no-div-regex': 2,
+			'no-empty': [2, { allowEmptyCatch: true }],
+			'no-empty-pattern': 2,
+			'no-eval': 2,
+			'no-extend-native': 2,
+			'no-ex-assign': 2,
+			'no-extra-bind': 2,
+			'no-extra-boolean-cast': 2,
+			'no-extra-label': 2,
+			'no-fallthrough': 2,
+			'no-func-assign': 2,
+			'no-global-assign': 2,
+			'no-implicit-coercion': 2,
+			'no-implicit-globals': 2,
+			'no-inner-declarations': 2,
+			'no-invalid-regexp': 2,
+			'no-invalid-this': 2,
+			'no-irregular-whitespace': 2,
+			'no-lonely-if': 2,
+			'no-multi-str': 2,
+			'no-native-reassign': 2,
+			'no-negated-in-lhs': 2,
+			'no-new': 2,
+			'no-new-func': 2,
+			'no-new-wrappers': 2,
+			'no-obj-calls': 2,
+			'no-proto': 2,
+			'no-prototype-builtins': 0,
+			'no-redeclare': 2,
+			'no-regex-spaces': 2,
+			'no-restricted-imports': 2,
+			'no-return-assign': 2,
+			'no-self-assign': 2,
+			'no-self-compare': 2,
+			'no-sequences': 2,
+			'no-shadow': 2,
+			'no-shadow-restricted-names': 2,
+			'no-sparse-arrays': 2,
+			'no-this-before-super': 2,
+			'no-throw-literal': 2,
+			'no-undef': 2,
+			'no-unmodified-loop-condition': 2,
+			'no-unreachable': 2,
+			'no-unused-vars': [
+				2,
+				{ vars: 'all', args: 'after-used', caughtErrors: 'none' },
+			],
+			'no-use-before-define': 0,
+			'no-useless-call': 2,
+			'no-useless-computed-key': 2,
+			'no-useless-concat': 2,
+			'no-useless-rename': 2,
+			'no-var': 2,
+			'object-curly-newline': 0,
+			'prefer-const': 2,
+			'prefer-rest-params': 2,
+			'prefer-spread': 2,
+			'prefer-template': 2,
+			'spaced-comment': [2, 'always'],
+			strict: 2,
+			'valid-typeof': 2,
+			yoda: 2,
+		},
+	},
+	// NOTE: We need to apply this only to .ts source files (and not to .mjs
+	// files).
+	...tsEslint.configs.recommendedTypeChecked.map(item => ({
+		...item,
+		files: ['src/**/*.ts'],
+	})),
+	// NOTE: We need to apply this only to .ts source files (and not to .mjs
+	// files).
+	...tsEslint.configs.stylisticTypeChecked.map(item => ({
+		...item,
+		files: ['src/**/*.ts'],
+	})),
+	{
+		name: '.ts source files',
+		files: ['src/**/*.ts'],
+		languageOptions: {
+			parserOptions: {
+				project: 'tsconfig.json',
+			},
+		},
+		rules: {
+			'@typescript-eslint/consistent-generic-constructors': [
+				2,
+				'type-annotation',
+			],
+			'@typescript-eslint/prefer-function-type': 0,
+			'@typescript-eslint/dot-notation': 0,
+			'@typescript-eslint/no-unused-vars': [
+				2,
+				{
+					vars: 'all',
+					args: 'after-used',
+					caughtErrors: 'none',
+					ignoreRestSiblings: false,
+				},
+			],
+			// We want to use `type` instead of `interface`.
+			'@typescript-eslint/consistent-type-definitions': 0,
+			'@typescript-eslint/explicit-function-return-type': [
+				2,
+				{ allowExpressions: true },
+			],
+			'@typescript-eslint/no-inferrable-types': 0,
+			'@typescript-eslint/no-unsafe-member-access': 0,
+			'@typescript-eslint/no-unsafe-assignment': 0,
+			'@typescript-eslint/no-unsafe-call': 0,
+			'@typescript-eslint/no-unsafe-return': 0,
+			'@typescript-eslint/no-unsafe-argument': 0,
+			'@typescript-eslint/consistent-indexed-object-style': 0,
+			'@typescript-eslint/no-empty-function': 0,
+			'@typescript-eslint/prefer-nullish-coalescing': 0,
+			'@typescript-eslint/prefer-regexp-exec': 0,
+			'@typescript-eslint/require-await': 0,
+			'@typescript-eslint/restrict-template-expressions': 0,
+			'@typescript-eslint/unbound-method': 0,
+			'@typescript-eslint/no-redundant-type-constituents': 0,
+		},
+	},
+	{
+		name: '.ts test files',
+		...jestEslint.configs['flat/recommended'],
+		files: ['src/test/**/*.ts'],
+		rules: {
+			...jestEslint.configs['flat/recommended'].rules,
+			'jest/no-disabled-tests': 2,
+			'jest/prefer-expect-assertions': 0,
+			'@typescript-eslint/no-unnecessary-type-assertion': 0,
+		},
+	},
+	{
+		name: 'lib/ files',
+		ignores: ['lib/**'],
+	}
+);
+
+export default config;
diff --git a/jest.config.js b/jest.config.js
index 5e022fc..37e2081 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,3 +1,3 @@
 module.exports = {
-  testRegex : 'test/test-.*\\.js'
+	testRegex: 'test/test-.*\\.js',
 };
diff --git a/npm-scripts.js b/npm-scripts.js
deleted file mode 100644
index 0ff5de2..0000000
--- a/npm-scripts.js
+++ /dev/null
@@ -1,159 +0,0 @@
-const esbuild = require("esbuild");
-const fs = require('fs');
-const path = require('path');
-const process = require('process');
-const { execSync } = require('child_process');
-const pkg = require('./package.json');
-
-const task = process.argv.slice(2).join(' ');
-
-const ESLINT_PATHS = [ 'src', 'test' ].join(' ');
-
-// eslint-disable-next-line no-console
-console.log(`npm-scripts.js [INFO] running task "${task}"`);
-
-void run();
-
-async function run() {
-  switch (task)
-  {
-    case 'grammar': {
-      grammar();
-      break;
-    }
-
-    case 'lint': {
-      lint();
-      break;
-    }
-
-    case 'test': {
-      test();
-      break;
-    }
-
-    case 'build': {
-      await build(true /* minify */);
-      await build(false /* minify */);
-
-      break;
-    }
-
-    case 'release': {
-      lint();
-      test();
-      executeCmd(`git commit -am '${pkg.version}'`);
-      executeCmd(`git tag -a ${pkg.version} -m '${pkg.version}'`);
-      executeCmd('git push origin master && git push origin --tags');
-      executeCmd('npm publish');
-
-      // eslint-disable-next-line no-console
-      console.log('update tryit-jssip and JsSIP website');
-      break;
-    }
-
-    default: {
-      throw new TypeError(`unknown task "${task}"`);
-    }
-  }
-}
-
-function lint()
-{
-  logInfo('lint()');
-
-  executeCmd(`eslint -c eslint.config.js --max-warnings 0 ${ESLINT_PATHS}`);
-}
-
-function test()
-{
-  logInfo('test()');
-
-  executeCmd('jest');
-}
-
-function grammar()
-{
-  logInfo('grammar()');
-
-  const local_pegjs = path.resolve('./node_modules/.bin/pegjs');
-  const Grammar_pegjs = path.resolve('src/Grammar.pegjs');
-  const Grammar_js = path.resolve('src/Grammar.js');
-
-  logInfo('compiling Grammar.pegjs into Grammar.js...');
-
-  executeCmd(`${local_pegjs} ${Grammar_pegjs} ${Grammar_js}`);
-
-  logInfo('grammar compiled');
-
-  // Modify the generated Grammar.js file with custom changes.
-  logInfo('applying custom changes to Grammar.js...');
-
-  const current_grammar = fs.readFileSync('src/Grammar.js').toString();
-  let modified_grammar = current_grammar.replace(
-    /throw new this\.SyntaxError\(([\s\S]*?)\);([\s\S]*?)}([\s\S]*?)return result;/,
-    'new this.SyntaxError($1);\n        return -1;$2}$3return data;'
-  );
-
-  modified_grammar = modified_grammar.replace(/\s+$/gm, '');
-  fs.writeFileSync('src/Grammar.js', modified_grammar);
-
-  logInfo('grammar done');
-}
-
-// Build sources into a file for publishing.
-async function build(minify = true) {
-  const entry = path.resolve("src/JsSIP.js");
-  const outfile = path.resolve("./dist", `jssip${minify ? '.min' : ''}.js`);
-  const banner = `
- /*
-  * JsSIP ${pkg.version}
-  * ${pkg.description}
-  * Copyright: 2012-${new Date().getFullYear()} ${pkg.contributors.join(' ')}
-  * Homepage: ${pkg.homepage}
-  * License: ${pkg.license}
-  */`;
-
-  await esbuild.build({
-    entryPoints: [entry],
-    outfile,
-    bundle: true,
-    minify,
-    sourcemap: false,
-    // https://esbuild.github.io/api/#global-name.
-    format: "iife",
-    globalName: "JsSIP",
-    platform: "browser",
-    target: ["es2015"],
-    // Make the generated output a single line.
-    supported: {
-      "template-literal": false,
-    },
-    // Add banner.
-    banner: {
-      js: banner,
-    },
-  });
-}
-
-function executeCmd(command)
-{
-  // eslint-disable-next-line no-console
-  console.log(`npm-scripts.js [INFO] executing command: ${command}`);
-
-  try
-  {
-    execSync(command, { stdio: [ 'ignore', process.stdout, process.stderr ] });
-  }
-  // eslint-disable-next-line no-unused-vars
-  catch (error)
-  {
-    process.exit(1);
-  }
-}
-
-function logInfo(...args)
-{
-  // eslint-disable-next-line no-console
-  console.log(`npm-scripts.mjs \x1b[36m[INFO] [${task}]\x1b[0m`, ...args);
-}
diff --git a/npm-scripts.mjs b/npm-scripts.mjs
new file mode 100644
index 0000000..306e538
--- /dev/null
+++ b/npm-scripts.mjs
@@ -0,0 +1,201 @@
+import esbuild from 'esbuild';
+import fs from 'fs';
+import path from 'path';
+import process from 'process';
+import { execSync } from 'child_process';
+import pkg from './package.json' with { type: 'json' };
+
+const task = process.argv.slice(2).join(' ');
+const taskArgs = process.argv.slice(3).join(' ');
+
+// Paths for ESLint to check. Converted to string for convenience.
+const ESLINT_PATHS = [
+	'eslint.config.mjs',
+	// "jest.config.mjs",
+	'npm-scripts.mjs',
+	'src',
+	'test',
+].join(' ');
+
+// Paths for ESLint to ignore. Converted to string argument for convenience.
+const ESLINT_IGNORE_PATTERN_ARGS = ['src/Grammar.pegs', 'src/Grammar.js']
+	.map(entry => `--ignore-pattern ${entry}`)
+	.join(' ');
+
+logInfo(`running task "${task}"`);
+logInfo(taskArgs ? `[args:"${taskArgs}"]` : '');
+
+void run();
+
+async function run() {
+	switch (task) {
+		case 'grammar': {
+			grammar();
+			break;
+		}
+
+		case 'lint': {
+			lint();
+			break;
+		}
+
+		case 'lint:fix': {
+			lint(true);
+			break;
+		}
+
+		case 'test': {
+			test();
+			break;
+		}
+
+		case 'build': {
+			buildTypescript();
+			await build(true /* minify */);
+			await build(false /* minify */);
+
+			break;
+		}
+
+		case 'typescript:build': {
+			buildTypescript();
+
+			break;
+		}
+
+		case 'release': {
+			lint();
+			test();
+			executeCmd(`git commit -am '${pkg.version}'`);
+			executeCmd(`git tag -a ${pkg.version} -m '${pkg.version}'`);
+			executeCmd('git push origin master && git push origin --tags');
+			executeCmd('npm publish');
+
+			// eslint-disable-next-line no-console
+			console.log('update tryit-jssip and JsSIP website');
+			break;
+		}
+
+		default: {
+			throw new TypeError(`unknown task "${task}"`);
+		}
+	}
+}
+
+function lint(fix = false) {
+	logInfo(`lint() [fix:${fix}]`);
+
+	executeCmd(
+		`eslint -c eslint.config.mjs --max-warnings 0 ${fix ? '--fix' : ''} ${ESLINT_PATHS} ${ESLINT_IGNORE_PATTERN_ARGS}`
+	);
+}
+
+function test() {
+	logInfo('test()');
+
+	// TODO: remove when tests are written in TS.
+	buildTypescript();
+	executeCmd('jest');
+}
+
+function grammar() {
+	logInfo('grammar()');
+
+	const local_pegjs = path.resolve('./node_modules/.bin/pegjs');
+	const Grammar_pegjs = path.resolve('src/Grammar.pegjs');
+	const Grammar_js = path.resolve('src/Grammar.js');
+
+	logInfo('compiling Grammar.pegjs into Grammar.js...');
+
+	executeCmd(`${local_pegjs} ${Grammar_pegjs} ${Grammar_js}`);
+
+	logInfo('grammar compiled');
+
+	// Modify the generated Grammar.js file with custom changes.
+	logInfo('applying custom changes to Grammar.js...');
+
+	const current_grammar = fs.readFileSync('src/Grammar.js').toString();
+	let modified_grammar = current_grammar.replace(
+		/throw new this\.SyntaxError\(([\s\S]*?)\);([\s\S]*?)}([\s\S]*?)return result;/,
+		'new this.SyntaxError($1);\n        return -1;$2}$3return data;'
+	);
+
+	modified_grammar = modified_grammar.replace(/\s+$/gm, '');
+	fs.writeFileSync('src/Grammar.js', modified_grammar);
+
+	logInfo('grammar done');
+}
+
+// Build sources into a file for publishing.
+async function build(minify = true) {
+	const entry = path.resolve('lib/JsSIP.js');
+	const outfile = path.resolve('./dist', `jssip${minify ? '.min' : ''}.js`);
+	const banner = `
+ /*
+  * JsSIP ${pkg.version}
+  * ${pkg.description}
+  * Copyright: 2012-${new Date().getFullYear()} ${pkg.contributors.join(' ')}
+  * Homepage: ${pkg.homepage}
+  * License: ${pkg.license}
+  */`;
+
+	await esbuild.build({
+		entryPoints: [entry],
+		outfile,
+		bundle: true,
+		minify,
+		sourcemap: false,
+		// https://esbuild.github.io/api/#global-name.
+		format: 'iife',
+		globalName: 'JsSIP',
+		platform: 'browser',
+		target: ['es2015'],
+		// Make the generated output a single line.
+		supported: {
+			'template-literal': false,
+		},
+		// Add banner.
+		banner: {
+			js: banner,
+		},
+	});
+}
+
+function buildTypescript() {
+	logInfo('buildTypescript()');
+
+	deleteLib();
+
+	// Generate .js CommonJS files in lib/.
+	executeCmd(`tsc ${taskArgs}`);
+
+	// Copy manual .d.ts files to lib/ until code is moved to TS and declaration files
+	// are automatically created.
+	executeCmd("cpx 'src/**/**.d.ts' lib/");
+}
+
+function deleteLib() {
+	if (!fs.existsSync('lib')) {
+		return;
+	}
+
+	logInfo('deleteLib()');
+
+	fs.rmSync('lib', { recursive: true, force: true });
+}
+
+function executeCmd(command) {
+	// eslint-disable-next-line no-console
+	console.log(`npm-scripts.js [INFO] executing command: ${command}`);
+
+	try {
+		execSync(command, { stdio: ['ignore', process.stdout, process.stderr] });
+	} catch (error) {
+		process.exit(1);
+	}
+}
+
+function logInfo(...args) {
+	// eslint-disable-next-line no-console
+	console.log(`npm-scripts.mjs \x1b[36m[INFO] [${task}]\x1b[0m`, ...args);
+}
diff --git a/package.json b/package.json
index d03357a..603ef16 100644
--- a/package.json
+++ b/package.json
@@ -1,52 +1,67 @@
 {
-  "name": "jssip",
-  "title": "JsSIP",
-  "description": "The Javascript SIP library",
-  "version": "3.12.0",
-  "homepage": "https://jssip.net",
-  "contributors": [
-    "José Luis Millán <jmillan@aliax.net> (https://github.com/jmillan)",
-    "Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)"
-  ],
-  "types": "src/JsSIP.d.ts",
-  "main": "src/JsSIP.js",
-  "keywords": [
-    "sip",
-    "websocket",
-    "webrtc",
-    "node",
-    "browser",
-    "library"
-  ],
-  "license": "MIT",
-  "repository": {
-    "type": "git",
-    "url": "https://github.com/versatica/JsSIP.git"
-  },
-  "bugs": {
-    "url": "https://github.com/versatica/JsSIP/issues"
-  },
-  "dependencies": {
-    "debug": "^4.3.1",
-    "events": "^3.3.0",
-    "sdp-transform": "^2.14.1"
-  },
-  "devDependencies": {
-    "@eslint/eslintrc": "^3.3.3",
-    "@eslint/js": "^9.39.2",
-    "@types/debug": "^4.1.12",
-    "@types/events": "^3.0.3",
-    "esbuild": "^0.27.2",
-    "eslint": "^9.39.1",
-    "eslint-plugin-jest": "^29.12.1",
-    "globals": "^17.0.0",
-    "jest": "^30.2.0",
-    "pegjs": "^0.7.0"
-  },
-  "scripts": {
-    "lint": "node npm-scripts.js lint",
-    "test": "node npm-scripts.js test",
-    "build": "node npm-scripts.js build",
-    "release": "node npm-scripts.js release"
-  }
+	"name": "jssip",
+	"title": "JsSIP",
+	"description": "The Javascript SIP library",
+	"version": "3.12.0",
+	"homepage": "https://jssip.net",
+	"contributors": [
+		"José Luis Millán <jmillan@aliax.net> (https://github.com/jmillan)",
+		"Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)"
+	],
+	"types": "lib/JsSIP.d.ts",
+	"main": "lib/JsSIP.js",
+	"keywords": [
+		"sip",
+		"websocket",
+		"webrtc",
+		"node",
+		"browser",
+		"library"
+	],
+	"license": "MIT",
+	"repository": {
+		"type": "git",
+		"url": "https://github.com/versatica/JsSIP.git"
+	},
+	"bugs": {
+		"url": "https://github.com/versatica/JsSIP/issues"
+	},
+	"files": [
+		"LICENSE",
+		"README.md",
+		"npm-scripts.mjs",
+		"lib"
+	],
+	"scripts": {
+		"lint": "node npm-scripts.mjs lint",
+		"lint:fix": "node npm-scripts.mjs lint:fix",
+		"test": "node npm-scripts.mjs test",
+		"build": "node npm-scripts.mjs build",
+		"typescript:build": "node npm-scripts.mjs typescript:build",
+		"release": "node npm-scripts.js release"
+	},
+	"dependencies": {
+		"debug": "^4.3.1",
+		"events": "^3.3.0",
+		"sdp-transform": "^2.14.1"
+	},
+	"devDependencies": {
+		"@eslint/eslintrc": "^3.3.3",
+		"@eslint/js": "^9.39.2",
+		"@types/debug": "^4.1.12",
+		"@types/events": "^3.0.3",
+		"@types/node": "^25.0.10",
+		"cpx": "^1.5.0",
+		"esbuild": "^0.27.2",
+		"eslint": "^9.39.1",
+		"eslint-config-prettier": "^10.1.8",
+		"eslint-plugin-jest": "^29.12.1",
+		"eslint-plugin-prettier": "^5.5.5",
+		"globals": "^17.0.0",
+		"jest": "^30.2.0",
+		"pegjs": "^0.7.0",
+		"prettier": "^3.8.1",
+		"typescript": "^5.9.3",
+		"typescript-eslint": "^8.53.1"
+	}
 }
diff --git a/src/Config.js b/src/Config.js
index 46f929e..64689ff 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -7,397 +7,308 @@ const Exceptions = require('./Exceptions');

 // Default settings.
 exports.settings = {
-  // SIP authentication.
-  authorization_user : null,
-  password           : null,
-  realm              : null,
-  ha1                : null,
-  authorization_jwt  : null,
-
-  // SIP account.
-  display_name : null,
-  uri          : null,
-  contact_uri  : null,
-
-  // SIP instance id (GRUU).
-  instance_id : null,
-
-  // Preloaded SIP Route header field.
-  use_preloaded_route : false,
-
-  // Session parameters.
-  session_timers                 : true,
-  session_timers_refresh_method  : JsSIP_C.UPDATE,
-  session_timers_force_refresher : false,
-  no_answer_timeout              : 60,
-
-  // Registration parameters.
-  register                : true,
-  register_expires        : 600,
-  register_from_tag_trail : '',
-  registrar_server        : null,
-
-  // Connection options.
-  sockets                          : null,
-  connection_recovery_max_interval : JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL,
-  connection_recovery_min_interval : JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
-
-  // Global extra headers, to be added to every request and response
-  extra_headers : null,
-
-  /*
-   * Host address.
-   * Value to be set in Via sent_by and host part of Contact FQDN.
-  */
-  via_host : `${Utils.createRandomToken(12)}.invalid`
+	// SIP authentication.
+	authorization_user: null,
+	password: null,
+	realm: null,
+	ha1: null,
+	authorization_jwt: null,
+
+	// SIP account.
+	display_name: null,
+	uri: null,
+	contact_uri: null,
+
+	// SIP instance id (GRUU).
+	instance_id: null,
+
+	// Preloaded SIP Route header field.
+	use_preloaded_route: false,
+
+	// Session parameters.
+	session_timers: true,
+	session_timers_refresh_method: JsSIP_C.UPDATE,
+	session_timers_force_refresher: false,
+	no_answer_timeout: 60,
+
+	// Registration parameters.
+	register: true,
+	register_expires: 600,
+	register_from_tag_trail: '',
+	registrar_server: null,
+
+	// Connection options.
+	sockets: null,
+	connection_recovery_max_interval: JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL,
+	connection_recovery_min_interval: JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
+
+	// Global extra headers, to be added to every request and response
+	extra_headers: null,
+
+	/*
+	 * Host address.
+	 * Value to be set in Via sent_by and host part of Contact FQDN.
+	 */
+	via_host: `${Utils.createRandomToken(12)}.invalid`,
 };

 // Configuration checks.
 const checks = {
-  mandatory : {
-
-    sockets(sockets)
-    {
-      /* Allow defining sockets parameter as:
-       *  Socket: socket
-       *  Array of Socket: [socket1, socket2]
-       *  Array of Objects: [{socket: socket1, weight:1}, {socket: Socket2, weight:0}]
-       *  Array of Objects and Socket: [{socket: socket1}, socket2]
-       */
-      const _sockets = [];
-
-      if (Socket.isSocket(sockets))
-      {
-        _sockets.push({ socket: sockets });
-      }
-      else if (Array.isArray(sockets) && sockets.length)
-      {
-        for (const socket of sockets)
-        {
-          if (Object.prototype.hasOwnProperty.call(socket, 'socket') &&
-              Socket.isSocket(socket.socket))
-          {
-            _sockets.push(socket);
-          }
-          else if (Socket.isSocket(socket))
-          {
-            _sockets.push({ socket: socket });
-          }
-        }
-      }
-      else
-      {
-        return;
-      }
-
-      return _sockets;
-    },
-
-    uri(uri)
-    {
-      if (!/^sip:/i.test(uri))
-      {
-        uri = `${JsSIP_C.SIP}:${uri}`;
-      }
-      const parsed = URI.parse(uri);
-
-      if (!parsed)
-      {
-        return;
-      }
-      else if (!parsed.user)
-      {
-        return;
-      }
-      else
-      {
-        return parsed;
-      }
-    }
-  },
-
-  optional : {
-
-    authorization_user(authorization_user)
-    {
-      if (Grammar.parse(`"${authorization_user}"`, 'quoted_string') === -1)
-      {
-        return;
-      }
-      else
-      {
-        return authorization_user;
-      }
-    },
-    authorization_jwt(authorization_jwt)
-    {
-      if (typeof authorization_jwt === 'string')
-      {
-        return authorization_jwt;
-      }
-    },
-    user_agent(user_agent)
-    {
-      if (typeof user_agent === 'string')
-      {
-        return user_agent;
-      }
-    },
-
-    connection_recovery_max_interval(connection_recovery_max_interval)
-    {
-      if (Utils.isDecimal(connection_recovery_max_interval))
-      {
-        const value = Number(connection_recovery_max_interval);
-
-        if (value > 0)
-        {
-          return value;
-        }
-      }
-    },
-
-    connection_recovery_min_interval(connection_recovery_min_interval)
-    {
-      if (Utils.isDecimal(connection_recovery_min_interval))
-      {
-        const value = Number(connection_recovery_min_interval);
-
-        if (value > 0)
-        {
-          return value;
-        }
-      }
-    },
-
-    contact_uri(contact_uri)
-    {
-      if (typeof contact_uri === 'string')
-      {
-        const uri = Grammar.parse(contact_uri, 'SIP_URI');
-
-        if (uri !== -1)
-        {
-          return uri;
-        }
-      }
-    },
-
-    display_name(display_name)
-    {
-      return display_name;
-    },
-
-    instance_id(instance_id)
-    {
-      if ((/^uuid:/i.test(instance_id)))
-      {
-        instance_id = instance_id.substr(5);
-      }
-
-      if (Grammar.parse(instance_id, 'uuid') === -1)
-      {
-        return;
-      }
-      else
-      {
-        return instance_id;
-      }
-    },
-
-    no_answer_timeout(no_answer_timeout)
-    {
-      if (Utils.isDecimal(no_answer_timeout))
-      {
-        const value = Number(no_answer_timeout);
-
-        if (value > 0)
-        {
-          return value;
-        }
-      }
-    },
-
-    session_timers(session_timers)
-    {
-      if (typeof session_timers === 'boolean')
-      {
-        return session_timers;
-      }
-    },
-
-    session_timers_refresh_method(method)
-    {
-      if (typeof method === 'string')
-      {
-        method = method.toUpperCase();
-
-        if (method === JsSIP_C.INVITE || method === JsSIP_C.UPDATE)
-        {
-          return method;
-        }
-      }
-    },
-
-    session_timers_force_refresher(session_timers_force_refresher)
-    {
-      if (typeof session_timers_force_refresher === 'boolean')
-      {
-        return session_timers_force_refresher;
-      }
-    },
-
-    password(password)
-    {
-      return String(password);
-    },
-
-    realm(realm)
-    {
-      return String(realm);
-    },
-
-    ha1(ha1)
-    {
-      return String(ha1);
-    },
-
-    register(register)
-    {
-      if (typeof register === 'boolean')
-      {
-        return register;
-      }
-    },
-
-    register_expires(register_expires)
-    {
-      if (Utils.isDecimal(register_expires))
-      {
-        const value = Number(register_expires);
-
-        if (value >= 0)
-        {
-          return value;
-        }
-      }
-    },
-
-    register_from_tag_trail(register_from_tag_trail)
-    {
-      if (typeof register_from_tag_trail === 'function')
-      {
-        return register_from_tag_trail;
-      }
-
-      return String(register_from_tag_trail);
-    },
-
-    registrar_server(registrar_server)
-    {
-      if (!/^sip:/i.test(registrar_server))
-      {
-        registrar_server = `${JsSIP_C.SIP}:${registrar_server}`;
-      }
-
-      const parsed = URI.parse(registrar_server);
-
-      if (!parsed)
-      {
-        return;
-      }
-      else if (parsed.user)
-      {
-        return;
-      }
-      else
-      {
-        return parsed;
-      }
-    },
-
-    use_preloaded_route(use_preloaded_route)
-    {
-      if (typeof use_preloaded_route === 'boolean')
-      {
-        return use_preloaded_route;
-      }
-    },
-
-    extra_headers(extra_headers)
-    {
-      const _extraHeaders = [];
-
-      if (Array.isArray(extra_headers) && extra_headers.length)
-      {
-        for (const header of extra_headers)
-        {
-          if (typeof header === 'string')
-          {
-            _extraHeaders.push(header);
-          }
-        }
-      }
-      else
-      {
-        return;
-      }
-
-      return _extraHeaders;
-    }
-  }
+	mandatory: {
+		sockets(sockets) {
+			/* Allow defining sockets parameter as:
+			 *  Socket: socket
+			 *  Array of Socket: [socket1, socket2]
+			 *  Array of Objects: [{socket: socket1, weight:1}, {socket: Socket2, weight:0}]
+			 *  Array of Objects and Socket: [{socket: socket1}, socket2]
+			 */
+			const _sockets = [];
+
+			if (Socket.isSocket(sockets)) {
+				_sockets.push({ socket: sockets });
+			} else if (Array.isArray(sockets) && sockets.length) {
+				for (const socket of sockets) {
+					if (
+						Object.prototype.hasOwnProperty.call(socket, 'socket') &&
+						Socket.isSocket(socket.socket)
+					) {
+						_sockets.push(socket);
+					} else if (Socket.isSocket(socket)) {
+						_sockets.push({ socket: socket });
+					}
+				}
+			} else {
+				return;
+			}
+
+			return _sockets;
+		},
+
+		uri(uri) {
+			if (!/^sip:/i.test(uri)) {
+				uri = `${JsSIP_C.SIP}:${uri}`;
+			}
+			const parsed = URI.parse(uri);
+
+			if (!parsed) {
+				return;
+			} else if (!parsed.user) {
+				return;
+			} else {
+				return parsed;
+			}
+		},
+	},
+
+	optional: {
+		authorization_user(authorization_user) {
+			if (Grammar.parse(`"${authorization_user}"`, 'quoted_string') === -1) {
+				return;
+			} else {
+				return authorization_user;
+			}
+		},
+		authorization_jwt(authorization_jwt) {
+			if (typeof authorization_jwt === 'string') {
+				return authorization_jwt;
+			}
+		},
+		user_agent(user_agent) {
+			if (typeof user_agent === 'string') {
+				return user_agent;
+			}
+		},
+
+		connection_recovery_max_interval(connection_recovery_max_interval) {
+			if (Utils.isDecimal(connection_recovery_max_interval)) {
+				const value = Number(connection_recovery_max_interval);
+
+				if (value > 0) {
+					return value;
+				}
+			}
+		},
+
+		connection_recovery_min_interval(connection_recovery_min_interval) {
+			if (Utils.isDecimal(connection_recovery_min_interval)) {
+				const value = Number(connection_recovery_min_interval);
+
+				if (value > 0) {
+					return value;
+				}
+			}
+		},
+
+		contact_uri(contact_uri) {
+			if (typeof contact_uri === 'string') {
+				const uri = Grammar.parse(contact_uri, 'SIP_URI');
+
+				if (uri !== -1) {
+					return uri;
+				}
+			}
+		},
+
+		display_name(display_name) {
+			return display_name;
+		},
+
+		instance_id(instance_id) {
+			if (/^uuid:/i.test(instance_id)) {
+				instance_id = instance_id.substr(5);
+			}
+
+			if (Grammar.parse(instance_id, 'uuid') === -1) {
+				return;
+			} else {
+				return instance_id;
+			}
+		},
+
+		no_answer_timeout(no_answer_timeout) {
+			if (Utils.isDecimal(no_answer_timeout)) {
+				const value = Number(no_answer_timeout);
+
+				if (value > 0) {
+					return value;
+				}
+			}
+		},
+
+		session_timers(session_timers) {
+			if (typeof session_timers === 'boolean') {
+				return session_timers;
+			}
+		},
+
+		session_timers_refresh_method(method) {
+			if (typeof method === 'string') {
+				method = method.toUpperCase();
+
+				if (method === JsSIP_C.INVITE || method === JsSIP_C.UPDATE) {
+					return method;
+				}
+			}
+		},
+
+		session_timers_force_refresher(session_timers_force_refresher) {
+			if (typeof session_timers_force_refresher === 'boolean') {
+				return session_timers_force_refresher;
+			}
+		},
+
+		password(password) {
+			return String(password);
+		},
+
+		realm(realm) {
+			return String(realm);
+		},
+
+		ha1(ha1) {
+			return String(ha1);
+		},
+
+		register(register) {
+			if (typeof register === 'boolean') {
+				return register;
+			}
+		},
+
+		register_expires(register_expires) {
+			if (Utils.isDecimal(register_expires)) {
+				const value = Number(register_expires);
+
+				if (value >= 0) {
+					return value;
+				}
+			}
+		},
+
+		register_from_tag_trail(register_from_tag_trail) {
+			if (typeof register_from_tag_trail === 'function') {
+				return register_from_tag_trail;
+			}
+
+			return String(register_from_tag_trail);
+		},
+
+		registrar_server(registrar_server) {
+			if (!/^sip:/i.test(registrar_server)) {
+				registrar_server = `${JsSIP_C.SIP}:${registrar_server}`;
+			}
+
+			const parsed = URI.parse(registrar_server);
+
+			if (!parsed) {
+				return;
+			} else if (parsed.user) {
+				return;
+			} else {
+				return parsed;
+			}
+		},
+
+		use_preloaded_route(use_preloaded_route) {
+			if (typeof use_preloaded_route === 'boolean') {
+				return use_preloaded_route;
+			}
+		},
+
+		extra_headers(extra_headers) {
+			const _extraHeaders = [];
+
+			if (Array.isArray(extra_headers) && extra_headers.length) {
+				for (const header of extra_headers) {
+					if (typeof header === 'string') {
+						_extraHeaders.push(header);
+					}
+				}
+			} else {
+				return;
+			}
+
+			return _extraHeaders;
+		},
+	},
 };

-exports.load = (dst, src) =>
-{
-  // Check Mandatory parameters.
-  for (const parameter in checks.mandatory)
-  {
-    if (!src.hasOwnProperty(parameter))
-    {
-      throw new Exceptions.ConfigurationError(parameter);
-    }
-    else
-    {
-      const value = src[parameter];
-      const checked_value = checks.mandatory[parameter](value);
-
-      if (checked_value !== undefined)
-      {
-        dst[parameter] = checked_value;
-      }
-      else
-      {
-        throw new Exceptions.ConfigurationError(parameter, value);
-      }
-    }
-  }
-
-  // Check Optional parameters.
-  for (const parameter in checks.optional)
-  {
-    if (src.hasOwnProperty(parameter))
-    {
-      const value = src[parameter];
-
-      /* If the parameter value is null, empty string, undefined, empty array
-       * or it's a number with NaN value, then apply its default value.
-       */
-      if (Utils.isEmpty(value))
-      {
-        continue;
-      }
-
-      const checked_value = checks.optional[parameter](value);
-
-      if (checked_value !== undefined)
-      {
-        dst[parameter] = checked_value;
-      }
-      else
-      {
-        throw new Exceptions.ConfigurationError(parameter, value);
-      }
-    }
-  }
+exports.load = (dst, src) => {
+	// Check Mandatory parameters.
+	for (const parameter in checks.mandatory) {
+		if (!src.hasOwnProperty(parameter)) {
+			throw new Exceptions.ConfigurationError(parameter);
+		} else {
+			const value = src[parameter];
+			const checked_value = checks.mandatory[parameter](value);
+
+			if (checked_value !== undefined) {
+				dst[parameter] = checked_value;
+			} else {
+				throw new Exceptions.ConfigurationError(parameter, value);
+			}
+		}
+	}
+
+	// Check Optional parameters.
+	for (const parameter in checks.optional) {
+		if (src.hasOwnProperty(parameter)) {
+			const value = src[parameter];
+
+			/* If the parameter value is null, empty string, undefined, empty array
+			 * or it's a number with NaN value, then apply its default value.
+			 */
+			if (Utils.isEmpty(value)) {
+				continue;
+			}
+
+			const checked_value = checks.optional[parameter](value);
+
+			if (checked_value !== undefined) {
+				dst[parameter] = checked_value;
+			} else {
+				throw new Exceptions.ConfigurationError(parameter, value);
+			}
+		}
+	}
 };
diff --git a/src/Constants.d.ts b/src/Constants.d.ts
index 31297cc..b12d171 100644
--- a/src/Constants.d.ts
+++ b/src/Constants.d.ts
@@ -1,66 +1,68 @@
-export const USER_AGENT: string
-export const SIP = 'sip'
-export const SIPS = 'sips'
+export const USER_AGENT: string;
+export const SIP = 'sip';
+export const SIPS = 'sips';

 export declare enum causes {
-  CONNECTION_ERROR = 'Connection Error',
-  REQUEST_TIMEOUT = 'Request Timeout',
-  SIP_FAILURE_CODE = 'SIP Failure Code',
-  INTERNAL_ERROR = 'Internal Error',
-  BUSY = 'Busy',
-  REJECTED = 'Rejected',
-  REDIRECTED = 'Redirected',
-  UNAVAILABLE = 'Unavailable',
-  NOT_FOUND = 'Not Found',
-  ADDRESS_INCOMPLETE = 'Address Incomplete',
-  INCOMPATIBLE_SDP = 'Incompatible SDP',
-  MISSING_SDP = 'Missing SDP',
-  AUTHENTICATION_ERROR = 'Authentication Error',
-  BYE = 'Terminated',
-  WEBRTC_ERROR = 'WebRTC Error',
-  CANCELED = 'Canceled',
-  NO_ANSWER = 'No Answer',
-  EXPIRES = 'Expires',
-  NO_ACK = 'No ACK',
-  DIALOG_ERROR = 'Dialog Error',
-  USER_DENIED_MEDIA_ACCESS = 'User Denied Media Access',
-  BAD_MEDIA_DESCRIPTION = 'Bad Media Description',
-  RTP_TIMEOUT = 'RTP Timeout',
+	CONNECTION_ERROR = 'Connection Error',
+	REQUEST_TIMEOUT = 'Request Timeout',
+	SIP_FAILURE_CODE = 'SIP Failure Code',
+	INTERNAL_ERROR = 'Internal Error',
+	BUSY = 'Busy',
+	REJECTED = 'Rejected',
+	REDIRECTED = 'Redirected',
+	UNAVAILABLE = 'Unavailable',
+	NOT_FOUND = 'Not Found',
+	ADDRESS_INCOMPLETE = 'Address Incomplete',
+	INCOMPATIBLE_SDP = 'Incompatible SDP',
+	MISSING_SDP = 'Missing SDP',
+	AUTHENTICATION_ERROR = 'Authentication Error',
+	BYE = 'Terminated',
+	WEBRTC_ERROR = 'WebRTC Error',
+	CANCELED = 'Canceled',
+	NO_ANSWER = 'No Answer',
+	EXPIRES = 'Expires',
+	NO_ACK = 'No ACK',
+	DIALOG_ERROR = 'Dialog Error',
+	USER_DENIED_MEDIA_ACCESS = 'User Denied Media Access',
+	BAD_MEDIA_DESCRIPTION = 'Bad Media Description',
+	RTP_TIMEOUT = 'RTP Timeout',
 }

 export const SIP_ERROR_CAUSES: {
-  REDIRECTED: [300, 301, 302, 305, 380],
-  BUSY: [486, 600],
-  REJECTED: [403, 603],
-  NOT_FOUND: [404, 604],
-  UNAVAILABLE: [480, 410, 408, 430],
-  ADDRESS_INCOMPLETE: [484, 424],
-  INCOMPATIBLE_SDP: [488, 606],
-  AUTHENTICATION_ERROR: [401, 407]
-}
-export const ACK = 'ACK'
-export const BYE = 'BYE'
-export const CANCEL = 'CANCEL'
-export const INFO = 'INFO'
-export const INVITE = 'INVITE'
-export const MESSAGE = 'MESSAGE'
-export const NOTIFY = 'NOTIFY'
-export const OPTIONS = 'OPTIONS'
-export const REGISTER = 'REGISTER'
-export const REFER = 'REFER'
-export const UPDATE = 'UPDATE'
-export const SUBSCRIBE = 'SUBSCRIBE'
+	REDIRECTED: [300, 301, 302, 305, 380];
+	BUSY: [486, 600];
+	REJECTED: [403, 603];
+	NOT_FOUND: [404, 604];
+	UNAVAILABLE: [480, 410, 408, 430];
+	ADDRESS_INCOMPLETE: [484, 424];
+	INCOMPATIBLE_SDP: [488, 606];
+	AUTHENTICATION_ERROR: [401, 407];
+};
+export const ACK = 'ACK';
+export const BYE = 'BYE';
+export const CANCEL = 'CANCEL';
+export const INFO = 'INFO';
+export const INVITE = 'INVITE';
+export const MESSAGE = 'MESSAGE';
+export const NOTIFY = 'NOTIFY';
+export const OPTIONS = 'OPTIONS';
+export const REGISTER = 'REGISTER';
+export const REFER = 'REFER';
+export const UPDATE = 'UPDATE';
+export const SUBSCRIBE = 'SUBSCRIBE';

 export declare enum DTMF_TRANSPORT {
-  INFO = 'INFO',
-  RFC2833 = 'RFC2833',
+	// eslint-disable-next-line no-shadow
+	INFO = 'INFO',
+	RFC2833 = 'RFC2833',
 }

-export const REASON_PHRASE: Record<number, string>
-export const ALLOWED_METHODS = 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE'
-export const ACCEPTED_BODY_TYPES = 'application/sdp, application/dtmf-relay'
-export const MAX_FORWARDS = 69
-export const SESSION_EXPIRES = 90
-export const MIN_SESSION_EXPIRES = 60
-export const CONNECTION_RECOVERY_MAX_INTERVAL = 30
-export const CONNECTION_RECOVERY_MIN_INTERVAL = 2
+export const REASON_PHRASE: Record<number, string>;
+export const ALLOWED_METHODS =
+	'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE';
+export const ACCEPTED_BODY_TYPES = 'application/sdp, application/dtmf-relay';
+export const MAX_FORWARDS = 69;
+export const SESSION_EXPIRES = 90;
+export const MIN_SESSION_EXPIRES = 60;
+export const CONNECTION_RECOVERY_MAX_INTERVAL = 30;
+export const CONNECTION_RECOVERY_MIN_INTERVAL = 2;
diff --git a/src/Constants.js b/src/Constants.js
index bb10550..0212f56 100644
--- a/src/Constants.js
+++ b/src/Constants.js
@@ -1,159 +1,160 @@
 const pkg = require('../package.json');

 module.exports = {
-  USER_AGENT : `${pkg.title} ${pkg.version}`,
+	USER_AGENT: `${pkg.title} ${pkg.version}`,

-  // SIP scheme.
-  SIP  : 'sip',
-  SIPS : 'sips',
+	// SIP scheme.
+	SIP: 'sip',
+	SIPS: 'sips',

-  // End and Failure causes.
-  causes : {
-    // Generic error causes.
-    CONNECTION_ERROR : 'Connection Error',
-    REQUEST_TIMEOUT  : 'Request Timeout',
-    SIP_FAILURE_CODE : 'SIP Failure Code',
-    INTERNAL_ERROR   : 'Internal Error',
+	// End and Failure causes.
+	causes: {
+		// Generic error causes.
+		CONNECTION_ERROR: 'Connection Error',
+		REQUEST_TIMEOUT: 'Request Timeout',
+		SIP_FAILURE_CODE: 'SIP Failure Code',
+		INTERNAL_ERROR: 'Internal Error',

-    // SIP error causes.
-    BUSY                 : 'Busy',
-    REJECTED             : 'Rejected',
-    REDIRECTED           : 'Redirected',
-    UNAVAILABLE          : 'Unavailable',
-    NOT_FOUND            : 'Not Found',
-    ADDRESS_INCOMPLETE   : 'Address Incomplete',
-    INCOMPATIBLE_SDP     : 'Incompatible SDP',
-    MISSING_SDP          : 'Missing SDP',
-    AUTHENTICATION_ERROR : 'Authentication Error',
+		// SIP error causes.
+		BUSY: 'Busy',
+		REJECTED: 'Rejected',
+		REDIRECTED: 'Redirected',
+		UNAVAILABLE: 'Unavailable',
+		NOT_FOUND: 'Not Found',
+		ADDRESS_INCOMPLETE: 'Address Incomplete',
+		INCOMPATIBLE_SDP: 'Incompatible SDP',
+		MISSING_SDP: 'Missing SDP',
+		AUTHENTICATION_ERROR: 'Authentication Error',

-    // Session error causes.
-    BYE                      : 'Terminated',
-    WEBRTC_ERROR             : 'WebRTC Error',
-    CANCELED                 : 'Canceled',
-    NO_ANSWER                : 'No Answer',
-    EXPIRES                  : 'Expires',
-    NO_ACK                   : 'No ACK',
-    DIALOG_ERROR             : 'Dialog Error',
-    USER_DENIED_MEDIA_ACCESS : 'User Denied Media Access',
-    BAD_MEDIA_DESCRIPTION    : 'Bad Media Description',
-    RTP_TIMEOUT              : 'RTP Timeout'
-  },
+		// Session error causes.
+		BYE: 'Terminated',
+		WEBRTC_ERROR: 'WebRTC Error',
+		CANCELED: 'Canceled',
+		NO_ANSWER: 'No Answer',
+		EXPIRES: 'Expires',
+		NO_ACK: 'No ACK',
+		DIALOG_ERROR: 'Dialog Error',
+		USER_DENIED_MEDIA_ACCESS: 'User Denied Media Access',
+		BAD_MEDIA_DESCRIPTION: 'Bad Media Description',
+		RTP_TIMEOUT: 'RTP Timeout',
+	},

-  SIP_ERROR_CAUSES : {
-    REDIRECTED           : [ 300, 301, 302, 305, 380 ],
-    BUSY                 : [ 486, 600 ],
-    REJECTED             : [ 403, 603 ],
-    NOT_FOUND            : [ 404, 604 ],
-    UNAVAILABLE          : [ 480, 410, 408, 430 ],
-    ADDRESS_INCOMPLETE   : [ 484, 424 ],
-    INCOMPATIBLE_SDP     : [ 488, 606 ],
-    AUTHENTICATION_ERROR : [ 401, 407 ]
-  },
+	SIP_ERROR_CAUSES: {
+		REDIRECTED: [300, 301, 302, 305, 380],
+		BUSY: [486, 600],
+		REJECTED: [403, 603],
+		NOT_FOUND: [404, 604],
+		UNAVAILABLE: [480, 410, 408, 430],
+		ADDRESS_INCOMPLETE: [484, 424],
+		INCOMPATIBLE_SDP: [488, 606],
+		AUTHENTICATION_ERROR: [401, 407],
+	},

-  // SIP Methods.
-  ACK       : 'ACK',
-  BYE       : 'BYE',
-  CANCEL    : 'CANCEL',
-  INFO      : 'INFO',
-  INVITE    : 'INVITE',
-  MESSAGE   : 'MESSAGE',
-  NOTIFY    : 'NOTIFY',
-  OPTIONS   : 'OPTIONS',
-  REGISTER  : 'REGISTER',
-  REFER     : 'REFER',
-  UPDATE    : 'UPDATE',
-  SUBSCRIBE : 'SUBSCRIBE',
+	// SIP Methods.
+	ACK: 'ACK',
+	BYE: 'BYE',
+	CANCEL: 'CANCEL',
+	INFO: 'INFO',
+	INVITE: 'INVITE',
+	MESSAGE: 'MESSAGE',
+	NOTIFY: 'NOTIFY',
+	OPTIONS: 'OPTIONS',
+	REGISTER: 'REGISTER',
+	REFER: 'REFER',
+	UPDATE: 'UPDATE',
+	SUBSCRIBE: 'SUBSCRIBE',

-  // DTMF transport methods.
-  DTMF_TRANSPORT : {
-    INFO    : 'INFO',
-    RFC2833 : 'RFC2833'
-  },
+	// DTMF transport methods.
+	DTMF_TRANSPORT: {
+		INFO: 'INFO',
+		RFC2833: 'RFC2833',
+	},

-  /* SIP Response Reasons
-   * DOC: https://www.iana.org/assignments/sip-parameters
-   * Copied from https://github.com/versatica/OverSIP/blob/master/lib/oversip/sip/constants.rb#L7
-   */
-  REASON_PHRASE : {
-    100 : 'Trying',
-    180 : 'Ringing',
-    181 : 'Call Is Being Forwarded',
-    182 : 'Queued',
-    183 : 'Session Progress',
-    199 : 'Early Dialog Terminated', // draft-ietf-sipcore-199
-    200 : 'OK',
-    202 : 'Accepted', // RFC 3265
-    204 : 'No Notification', // RFC 5839
-    300 : 'Multiple Choices',
-    301 : 'Moved Permanently',
-    302 : 'Moved Temporarily',
-    305 : 'Use Proxy',
-    380 : 'Alternative Service',
-    400 : 'Bad Request',
-    401 : 'Unauthorized',
-    402 : 'Payment Required',
-    403 : 'Forbidden',
-    404 : 'Not Found',
-    405 : 'Method Not Allowed',
-    406 : 'Not Acceptable',
-    407 : 'Proxy Authentication Required',
-    408 : 'Request Timeout',
-    410 : 'Gone',
-    412 : 'Conditional Request Failed', // RFC 3903
-    413 : 'Request Entity Too Large',
-    414 : 'Request-URI Too Long',
-    415 : 'Unsupported Media Type',
-    416 : 'Unsupported URI Scheme',
-    417 : 'Unknown Resource-Priority', // RFC 4412
-    420 : 'Bad Extension',
-    421 : 'Extension Required',
-    422 : 'Session Interval Too Small', // RFC 4028
-    423 : 'Interval Too Brief',
-    424 : 'Bad Location Information', // RFC 6442
-    428 : 'Use Identity Header', // RFC 4474
-    429 : 'Provide Referrer Identity', // RFC 3892
-    430 : 'Flow Failed', // RFC 5626
-    433 : 'Anonymity Disallowed', // RFC 5079
-    436 : 'Bad Identity-Info', // RFC 4474
-    437 : 'Unsupported Certificate', // RFC 4744
-    438 : 'Invalid Identity Header', // RFC 4744
-    439 : 'First Hop Lacks Outbound Support', // RFC 5626
-    440 : 'Max-Breadth Exceeded', // RFC 5393
-    469 : 'Bad Info Package', // draft-ietf-sipcore-info-events
-    470 : 'Consent Needed', // RFC 5360
-    478 : 'Unresolvable Destination', // Custom code copied from Kamailio.
-    480 : 'Temporarily Unavailable',
-    481 : 'Call/Transaction Does Not Exist',
-    482 : 'Loop Detected',
-    483 : 'Too Many Hops',
-    484 : 'Address Incomplete',
-    485 : 'Ambiguous',
-    486 : 'Busy Here',
-    487 : 'Request Terminated',
-    488 : 'Not Acceptable Here',
-    489 : 'Bad Event', // RFC 3265
-    491 : 'Request Pending',
-    493 : 'Undecipherable',
-    494 : 'Security Agreement Required', // RFC 3329
-    500 : 'JsSIP Internal Error',
-    501 : 'Not Implemented',
-    502 : 'Bad Gateway',
-    503 : 'Service Unavailable',
-    504 : 'Server Time-out',
-    505 : 'Version Not Supported',
-    513 : 'Message Too Large',
-    580 : 'Precondition Failure', // RFC 3312
-    600 : 'Busy Everywhere',
-    603 : 'Decline',
-    604 : 'Does Not Exist Anywhere',
-    606 : 'Not Acceptable'
-  },
+	/* SIP Response Reasons
+	 * DOC: https://www.iana.org/assignments/sip-parameters
+	 * Copied from https://github.com/versatica/OverSIP/blob/master/lib/oversip/sip/constants.rb#L7
+	 */
+	REASON_PHRASE: {
+		100: 'Trying',
+		180: 'Ringing',
+		181: 'Call Is Being Forwarded',
+		182: 'Queued',
+		183: 'Session Progress',
+		199: 'Early Dialog Terminated', // draft-ietf-sipcore-199
+		200: 'OK',
+		202: 'Accepted', // RFC 3265
+		204: 'No Notification', // RFC 5839
+		300: 'Multiple Choices',
+		301: 'Moved Permanently',
+		302: 'Moved Temporarily',
+		305: 'Use Proxy',
+		380: 'Alternative Service',
+		400: 'Bad Request',
+		401: 'Unauthorized',
+		402: 'Payment Required',
+		403: 'Forbidden',
+		404: 'Not Found',
+		405: 'Method Not Allowed',
+		406: 'Not Acceptable',
+		407: 'Proxy Authentication Required',
+		408: 'Request Timeout',
+		410: 'Gone',
+		412: 'Conditional Request Failed', // RFC 3903
+		413: 'Request Entity Too Large',
+		414: 'Request-URI Too Long',
+		415: 'Unsupported Media Type',
+		416: 'Unsupported URI Scheme',
+		417: 'Unknown Resource-Priority', // RFC 4412
+		420: 'Bad Extension',
+		421: 'Extension Required',
+		422: 'Session Interval Too Small', // RFC 4028
+		423: 'Interval Too Brief',
+		424: 'Bad Location Information', // RFC 6442
+		428: 'Use Identity Header', // RFC 4474
+		429: 'Provide Referrer Identity', // RFC 3892
+		430: 'Flow Failed', // RFC 5626
+		433: 'Anonymity Disallowed', // RFC 5079
+		436: 'Bad Identity-Info', // RFC 4474
+		437: 'Unsupported Certificate', // RFC 4744
+		438: 'Invalid Identity Header', // RFC 4744
+		439: 'First Hop Lacks Outbound Support', // RFC 5626
+		440: 'Max-Breadth Exceeded', // RFC 5393
+		469: 'Bad Info Package', // draft-ietf-sipcore-info-events
+		470: 'Consent Needed', // RFC 5360
+		478: 'Unresolvable Destination', // Custom code copied from Kamailio.
+		480: 'Temporarily Unavailable',
+		481: 'Call/Transaction Does Not Exist',
+		482: 'Loop Detected',
+		483: 'Too Many Hops',
+		484: 'Address Incomplete',
+		485: 'Ambiguous',
+		486: 'Busy Here',
+		487: 'Request Terminated',
+		488: 'Not Acceptable Here',
+		489: 'Bad Event', // RFC 3265
+		491: 'Request Pending',
+		493: 'Undecipherable',
+		494: 'Security Agreement Required', // RFC 3329
+		500: 'JsSIP Internal Error',
+		501: 'Not Implemented',
+		502: 'Bad Gateway',
+		503: 'Service Unavailable',
+		504: 'Server Time-out',
+		505: 'Version Not Supported',
+		513: 'Message Too Large',
+		580: 'Precondition Failure', // RFC 3312
+		600: 'Busy Everywhere',
+		603: 'Decline',
+		604: 'Does Not Exist Anywhere',
+		606: 'Not Acceptable',
+	},

-  ALLOWED_METHODS                  : 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE',
-  ACCEPTED_BODY_TYPES              : 'application/sdp, application/dtmf-relay',
-  MAX_FORWARDS                     : 69,
-  SESSION_EXPIRES                  : 90,
-  MIN_SESSION_EXPIRES              : 60,
-  CONNECTION_RECOVERY_MAX_INTERVAL : 30,
-  CONNECTION_RECOVERY_MIN_INTERVAL : 2
+	ALLOWED_METHODS:
+		'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE',
+	ACCEPTED_BODY_TYPES: 'application/sdp, application/dtmf-relay',
+	MAX_FORWARDS: 69,
+	SESSION_EXPIRES: 90,
+	MIN_SESSION_EXPIRES: 60,
+	CONNECTION_RECOVERY_MAX_INTERVAL: 30,
+	CONNECTION_RECOVERY_MIN_INTERVAL: 2,
 };
diff --git a/src/Dialog.js b/src/Dialog.js
index 8ec47a6..c8f68cc 100644
--- a/src/Dialog.js
+++ b/src/Dialog.js
@@ -8,329 +8,300 @@ const Utils = require('./Utils');
 const logger = new Logger('Dialog');

 const C = {
-  // Dialog states.
-  STATUS_EARLY      : 1,
-  STATUS_CONFIRMED  : 2,
-  STATUS_TERMINATED : 3
+	// Dialog states.
+	STATUS_EARLY: 1,
+	STATUS_CONFIRMED: 2,
+	STATUS_TERMINATED: 3,
 };

 // RFC 3261 12.1.
-module.exports = class Dialog
-{
-  // Expose C object.
-  static get C()
-  {
-    return C;
-  }
-
-  constructor(owner, message, type, state = C.STATUS_CONFIRMED)
-  {
-    this._owner = owner;
-    this._ua = owner._ua;
-
-    this._uac_pending_reply = false;
-    this._uas_pending_reply = false;
-
-    if (!message.hasHeader('contact'))
-    {
-      return {
-        error : 'unable to create a Dialog without Contact header field'
-      };
-    }
-
-    if (message instanceof SIPMessage.IncomingResponse)
-    {
-      state = (message.status_code < 200) ? C.STATUS_EARLY : C.STATUS_CONFIRMED;
-    }
-
-    const contact = message.parseHeader('contact');
-
-    // RFC 3261 12.1.1.
-    if (type === 'UAS')
-    {
-      this._id = {
-        call_id    : message.call_id,
-        local_tag  : message.to_tag,
-        remote_tag : message.from_tag,
-        toString()
-        {
-          return this.call_id + this.local_tag + this.remote_tag;
-        }
-      };
-      this._state = state;
-      this._remote_seqnum = message.cseq;
-      this._local_uri = message.parseHeader('to').uri;
-      this._remote_uri = message.parseHeader('from').uri;
-      this._remote_target = contact.uri;
-      this._route_set = message.getHeaders('record-route');
-      this.incoming_ack_seqnum = message.cseq;
-      this.outgoing_ack_seqnum = null;
-    }
-    // RFC 3261 12.1.2.
-    else if (type === 'UAC')
-    {
-      this._id = {
-        call_id    : message.call_id,
-        local_tag  : message.from_tag,
-        remote_tag : message.to_tag,
-        toString()
-        {
-          return this.call_id + this.local_tag + this.remote_tag;
-        }
-      };
-      this._state = state;
-      this._local_seqnum = message.cseq;
-      this._local_uri = message.parseHeader('from').uri;
-      this._remote_uri = message.parseHeader('to').uri;
-      this._remote_target = contact.uri;
-      this._route_set = message.getHeaders('record-route').reverse();
-      this.incoming_ack_seqnum = null;
-      this.outgoing_ack_seqnum = this._local_seqnum;
-
-    }
-
-    this._ua.newDialog(this);
-    logger.debug(`new ${type} dialog created with status ${this._state === C.STATUS_EARLY ? 'EARLY': 'CONFIRMED'}`);
-  }
-
-  get id()
-  {
-    return this._id;
-  }
-
-  get local_seqnum()
-  {
-    return this._local_seqnum;
-  }
-
-  set local_seqnum(num)
-  {
-    this._local_seqnum = num;
-  }
-
-  get owner()
-  {
-    return this._owner;
-  }
-
-  get uac_pending_reply()
-  {
-    return this._uac_pending_reply;
-  }
-
-  set uac_pending_reply(pending)
-  {
-    this._uac_pending_reply = pending;
-  }
-
-  get uas_pending_reply()
-  {
-    return this._uas_pending_reply;
-  }
-
-  isTerminated()
-  {
-    return this._status === C.STATUS_TERMINATED;
-  }
-
-  update(message, type)
-  {
-    this._state = C.STATUS_CONFIRMED;
-
-    logger.debug(`dialog ${this._id.toString()}  changed to CONFIRMED state`);
-
-    if (type === 'UAC')
-    {
-      // RFC 3261 13.2.2.4.
-      this._route_set = message.getHeaders('record-route').reverse();
-    }
-  }
-
-  terminate()
-  {
-    logger.debug(`dialog ${this._id.toString()} deleted`);
-
-    this._ua.destroyDialog(this);
-    this._state = C.STATUS_TERMINATED;
-  }
-
-  sendRequest(method, options = {})
-  {
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const eventHandlers = Utils.cloneObject(options.eventHandlers);
-    const body = options.body || null;
-    const request = this._createRequest(method, extraHeaders, body);
-
-    // Increase the local CSeq on authentication.
-    eventHandlers.onAuthenticated = () =>
-    {
-      this._local_seqnum += 1;
-
-      // In case of re-INVITE store outgoing ack_seqnum for its CANCEL or ACK.
-      if (request.method === JsSIP_C.INVITE)
-      {
-        this._outgoing_ack_seqnum = this._local_seqnum;
-      }
-    };
-
-    const request_sender = new Dialog_RequestSender(this, request, eventHandlers);
-
-    request_sender.send();
-
-    // Return the instance of OutgoingRequest.
-    return request;
-  }
-
-  receiveRequest(request)
-  {
-    // Check in-dialog request.
-    if (!this._checkInDialogRequest(request))
-    {
-      return;
-    }
-
-    // ACK received. Cleanup this._ack_seqnum.
-    if (request.method === JsSIP_C.ACK && this.incoming_ack_seqnum !== null)
-    {
-      this.incoming_ack_seqnum = null;
-    }
-    // INVITE received. Set this._ack_seqnum.
-    else if (request.method === JsSIP_C.INVITE)
-    {
-      this.incoming_ack_seqnum = request.cseq;
-    }
-
-    this._owner.receiveRequest(request);
-  }
-
-  // RFC 3261 12.2.1.1.
-  _createRequest(method, extraHeaders, body)
-  {
-    extraHeaders = Utils.cloneArray(extraHeaders);
-
-    if (!this._local_seqnum) { this._local_seqnum = Math.floor(Math.random() * 10000); }
-
-    // CANCEL and ACK must use the same sequence number as the INVITE.
-    const cseq = (method === JsSIP_C.CANCEL || method === JsSIP_C.ACK) ?
-      this.outgoing_ack_seqnum :
-      this._local_seqnum += 1;
-
-    // In case of re-INVITE store ack_seqnum for future CANCEL or ACK.
-    if (method === JsSIP_C.INVITE)
-    {
-      this.outgoing_ack_seqnum = cseq;
-    }
-
-    const request = new SIPMessage.OutgoingRequest(
-      method,
-      this._remote_target,
-      this._ua, {
-        'cseq'      : cseq,
-        'call_id'   : this._id.call_id,
-        'from_uri'  : this._local_uri,
-        'from_tag'  : this._id.local_tag,
-        'to_uri'    : this._remote_uri,
-        'to_tag'    : this._id.remote_tag,
-        'route_set' : this._route_set
-      }, extraHeaders, body);
-
-    return request;
-  }
-
-  // RFC 3261 12.2.2.
-  _checkInDialogRequest(request)
-  {
-
-    if (!this._remote_seqnum)
-    {
-      this._remote_seqnum = request.cseq;
-    }
-    else if (request.cseq < this._remote_seqnum)
-    {
-      if (request.method === JsSIP_C.ACK)
-      {
-        // We are not expecting any ACK with lower seqnum than the current one.
-        // Or this is not the ACK we are waiting for.
-        if (this.incoming_ack_seqnum === null ||
-            request.cseq !== this.incoming_ack_seqnum)
-        {
-          return false;
-        }
-      }
-      else
-      {
-        request.reply(500);
-
-        return false;
-      }
-    }
-    else if (request.cseq > this._remote_seqnum)
-    {
-      this._remote_seqnum = request.cseq;
-    }
-
-    // RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR-.
-    if (request.method === JsSIP_C.INVITE ||
-        (request.method === JsSIP_C.UPDATE && request.body))
-    {
-      if (this._uac_pending_reply === true)
-      {
-        request.reply(491);
-      }
-      else if (this._uas_pending_reply === true)
-      {
-        const retryAfter = (Math.random() * 10 | 0) + 1;
-
-        request.reply(500, null, [ `Retry-After:${retryAfter}` ]);
-
-        return false;
-      }
-      else
-      {
-        this._uas_pending_reply = true;
-
-        const stateChanged = () =>
-        {
-          if (request.server_transaction.state === Transactions.C.STATUS_ACCEPTED ||
-              request.server_transaction.state === Transactions.C.STATUS_COMPLETED ||
-              request.server_transaction.state === Transactions.C.STATUS_TERMINATED)
-          {
-
-            request.server_transaction.removeListener('stateChanged', stateChanged);
-            this._uas_pending_reply = false;
-          }
-        };
-
-        request.server_transaction.on('stateChanged', stateChanged);
-      }
-
-      // RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted.
-      if (request.hasHeader('contact'))
-      {
-        request.server_transaction.on('stateChanged', () =>
-        {
-          if (request.server_transaction.state === Transactions.C.STATUS_ACCEPTED)
-          {
-            this._remote_target = request.parseHeader('contact').uri;
-          }
-        });
-      }
-    }
-    else if (request.method === JsSIP_C.NOTIFY)
-    {
-      // RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted.
-      if (request.hasHeader('contact'))
-      {
-        request.server_transaction.on('stateChanged', () =>
-        {
-          if (request.server_transaction.state === Transactions.C.STATUS_COMPLETED)
-          {
-            this._remote_target = request.parseHeader('contact').uri;
-          }
-        });
-      }
-    }
-
-    return true;
-  }
+module.exports = class Dialog {
+	// Expose C object.
+	static get C() {
+		return C;
+	}
+
+	constructor(owner, message, type, state = C.STATUS_CONFIRMED) {
+		this._owner = owner;
+		this._ua = owner._ua;
+
+		this._uac_pending_reply = false;
+		this._uas_pending_reply = false;
+
+		if (!message.hasHeader('contact')) {
+			return {
+				error: 'unable to create a Dialog without Contact header field',
+			};
+		}
+
+		if (message instanceof SIPMessage.IncomingResponse) {
+			state = message.status_code < 200 ? C.STATUS_EARLY : C.STATUS_CONFIRMED;
+		}
+
+		const contact = message.parseHeader('contact');
+
+		// RFC 3261 12.1.1.
+		if (type === 'UAS') {
+			this._id = {
+				call_id: message.call_id,
+				local_tag: message.to_tag,
+				remote_tag: message.from_tag,
+				toString() {
+					return this.call_id + this.local_tag + this.remote_tag;
+				},
+			};
+			this._state = state;
+			this._remote_seqnum = message.cseq;
+			this._local_uri = message.parseHeader('to').uri;
+			this._remote_uri = message.parseHeader('from').uri;
+			this._remote_target = contact.uri;
+			this._route_set = message.getHeaders('record-route');
+			this.incoming_ack_seqnum = message.cseq;
+			this.outgoing_ack_seqnum = null;
+		}
+		// RFC 3261 12.1.2.
+		else if (type === 'UAC') {
+			this._id = {
+				call_id: message.call_id,
+				local_tag: message.from_tag,
+				remote_tag: message.to_tag,
+				toString() {
+					return this.call_id + this.local_tag + this.remote_tag;
+				},
+			};
+			this._state = state;
+			this._local_seqnum = message.cseq;
+			this._local_uri = message.parseHeader('from').uri;
+			this._remote_uri = message.parseHeader('to').uri;
+			this._remote_target = contact.uri;
+			this._route_set = message.getHeaders('record-route').reverse();
+			this.incoming_ack_seqnum = null;
+			this.outgoing_ack_seqnum = this._local_seqnum;
+		}
+
+		this._ua.newDialog(this);
+		logger.debug(
+			`new ${type} dialog created with status ${this._state === C.STATUS_EARLY ? 'EARLY' : 'CONFIRMED'}`
+		);
+	}
+
+	get id() {
+		return this._id;
+	}
+
+	get local_seqnum() {
+		return this._local_seqnum;
+	}
+
+	set local_seqnum(num) {
+		this._local_seqnum = num;
+	}
+
+	get owner() {
+		return this._owner;
+	}
+
+	get uac_pending_reply() {
+		return this._uac_pending_reply;
+	}
+
+	set uac_pending_reply(pending) {
+		this._uac_pending_reply = pending;
+	}
+
+	get uas_pending_reply() {
+		return this._uas_pending_reply;
+	}
+
+	isTerminated() {
+		return this._status === C.STATUS_TERMINATED;
+	}
+
+	update(message, type) {
+		this._state = C.STATUS_CONFIRMED;
+
+		logger.debug(`dialog ${this._id.toString()}  changed to CONFIRMED state`);
+
+		if (type === 'UAC') {
+			// RFC 3261 13.2.2.4.
+			this._route_set = message.getHeaders('record-route').reverse();
+		}
+	}
+
+	terminate() {
+		logger.debug(`dialog ${this._id.toString()} deleted`);
+
+		this._ua.destroyDialog(this);
+		this._state = C.STATUS_TERMINATED;
+	}
+
+	sendRequest(method, options = {}) {
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const eventHandlers = Utils.cloneObject(options.eventHandlers);
+		const body = options.body || null;
+		const request = this._createRequest(method, extraHeaders, body);
+
+		// Increase the local CSeq on authentication.
+		eventHandlers.onAuthenticated = () => {
+			this._local_seqnum += 1;
+
+			// In case of re-INVITE store outgoing ack_seqnum for its CANCEL or ACK.
+			if (request.method === JsSIP_C.INVITE) {
+				this._outgoing_ack_seqnum = this._local_seqnum;
+			}
+		};
+
+		const request_sender = new Dialog_RequestSender(
+			this,
+			request,
+			eventHandlers
+		);
+
+		request_sender.send();
+
+		// Return the instance of OutgoingRequest.
+		return request;
+	}
+
+	receiveRequest(request) {
+		// Check in-dialog request.
+		if (!this._checkInDialogRequest(request)) {
+			return;
+		}
+
+		// ACK received. Cleanup this._ack_seqnum.
+		if (request.method === JsSIP_C.ACK && this.incoming_ack_seqnum !== null) {
+			this.incoming_ack_seqnum = null;
+		}
+		// INVITE received. Set this._ack_seqnum.
+		else if (request.method === JsSIP_C.INVITE) {
+			this.incoming_ack_seqnum = request.cseq;
+		}
+
+		this._owner.receiveRequest(request);
+	}
+
+	// RFC 3261 12.2.1.1.
+	_createRequest(method, extraHeaders, body) {
+		extraHeaders = Utils.cloneArray(extraHeaders);
+
+		if (!this._local_seqnum) {
+			this._local_seqnum = Math.floor(Math.random() * 10000);
+		}
+
+		// CANCEL and ACK must use the same sequence number as the INVITE.
+		const cseq =
+			method === JsSIP_C.CANCEL || method === JsSIP_C.ACK
+				? this.outgoing_ack_seqnum
+				: (this._local_seqnum += 1);
+
+		// In case of re-INVITE store ack_seqnum for future CANCEL or ACK.
+		if (method === JsSIP_C.INVITE) {
+			this.outgoing_ack_seqnum = cseq;
+		}
+
+		const request = new SIPMessage.OutgoingRequest(
+			method,
+			this._remote_target,
+			this._ua,
+			{
+				cseq: cseq,
+				call_id: this._id.call_id,
+				from_uri: this._local_uri,
+				from_tag: this._id.local_tag,
+				to_uri: this._remote_uri,
+				to_tag: this._id.remote_tag,
+				route_set: this._route_set,
+			},
+			extraHeaders,
+			body
+		);
+
+		return request;
+	}
+
+	// RFC 3261 12.2.2.
+	_checkInDialogRequest(request) {
+		if (!this._remote_seqnum) {
+			this._remote_seqnum = request.cseq;
+		} else if (request.cseq < this._remote_seqnum) {
+			if (request.method === JsSIP_C.ACK) {
+				// We are not expecting any ACK with lower seqnum than the current one.
+				// Or this is not the ACK we are waiting for.
+				if (
+					this.incoming_ack_seqnum === null ||
+					request.cseq !== this.incoming_ack_seqnum
+				) {
+					return false;
+				}
+			} else {
+				request.reply(500);
+
+				return false;
+			}
+		} else if (request.cseq > this._remote_seqnum) {
+			this._remote_seqnum = request.cseq;
+		}
+
+		// RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR-.
+		if (
+			request.method === JsSIP_C.INVITE ||
+			(request.method === JsSIP_C.UPDATE && request.body)
+		) {
+			if (this._uac_pending_reply === true) {
+				request.reply(491);
+			} else if (this._uas_pending_reply === true) {
+				const retryAfter = ((Math.random() * 10) | 0) + 1;
+
+				request.reply(500, null, [`Retry-After:${retryAfter}`]);
+
+				return false;
+			} else {
+				this._uas_pending_reply = true;
+
+				const stateChanged = () => {
+					if (
+						request.server_transaction.state ===
+							Transactions.C.STATUS_ACCEPTED ||
+						request.server_transaction.state ===
+							Transactions.C.STATUS_COMPLETED ||
+						request.server_transaction.state ===
+							Transactions.C.STATUS_TERMINATED
+					) {
+						request.server_transaction.removeListener(
+							'stateChanged',
+							stateChanged
+						);
+						this._uas_pending_reply = false;
+					}
+				};
+
+				request.server_transaction.on('stateChanged', stateChanged);
+			}
+
+			// RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted.
+			if (request.hasHeader('contact')) {
+				request.server_transaction.on('stateChanged', () => {
+					if (
+						request.server_transaction.state === Transactions.C.STATUS_ACCEPTED
+					) {
+						this._remote_target = request.parseHeader('contact').uri;
+					}
+				});
+			}
+		} else if (request.method === JsSIP_C.NOTIFY) {
+			// RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted.
+			if (request.hasHeader('contact')) {
+				request.server_transaction.on('stateChanged', () => {
+					if (
+						request.server_transaction.state === Transactions.C.STATUS_COMPLETED
+					) {
+						this._remote_target = request.parseHeader('contact').uri;
+					}
+				});
+			}
+		}
+
+		return true;
+	}
 };
diff --git a/src/Dialog/RequestSender.js b/src/Dialog/RequestSender.js
index 88d1b7f..adeeb39 100644
--- a/src/Dialog/RequestSender.js
+++ b/src/Dialog/RequestSender.js
@@ -4,123 +4,110 @@ const RequestSender = require('../RequestSender');

 // Default event handlers.
 const EventHandlers = {
-  onRequestTimeout  : () => {},
-  onTransportError  : () => {},
-  onSuccessResponse : () => {},
-  onErrorResponse   : () => {},
-  onAuthenticated   : () => {},
-  onDialogError     : () => {}
+	onRequestTimeout: () => {},
+	onTransportError: () => {},
+	onSuccessResponse: () => {},
+	onErrorResponse: () => {},
+	onAuthenticated: () => {},
+	onDialogError: () => {},
 };

-module.exports = class DialogRequestSender
-{
-  constructor(dialog, request, eventHandlers)
-  {
-    this._dialog = dialog;
-    this._ua = dialog._ua;
-    this._request = request;
-    this._eventHandlers = eventHandlers;
+module.exports = class DialogRequestSender {
+	constructor(dialog, request, eventHandlers) {
+		this._dialog = dialog;
+		this._ua = dialog._ua;
+		this._request = request;
+		this._eventHandlers = eventHandlers;

-    // RFC3261 14.1 Modifying an Existing Session. UAC Behavior.
-    this._reattempt = false;
-    this._reattemptTimer = null;
+		// RFC3261 14.1 Modifying an Existing Session. UAC Behavior.
+		this._reattempt = false;
+		this._reattemptTimer = null;

-    // Define the undefined handlers.
-    for (const handler in EventHandlers)
-    {
-      if (Object.prototype.hasOwnProperty.call(EventHandlers, handler))
-      {
-        if (!this._eventHandlers[handler])
-        {
-          this._eventHandlers[handler] = EventHandlers[handler];
-        }
-      }
-    }
-  }
+		// Define the undefined handlers.
+		for (const handler in EventHandlers) {
+			if (Object.prototype.hasOwnProperty.call(EventHandlers, handler)) {
+				if (!this._eventHandlers[handler]) {
+					this._eventHandlers[handler] = EventHandlers[handler];
+				}
+			}
+		}
+	}

-  get request()
-  {
-    return this._request;
-  }
+	get request() {
+		return this._request;
+	}

-  send()
-  {
-    const request_sender = new RequestSender(this._ua, this._request, {
-      onRequestTimeout : () =>
-      {
-        this._eventHandlers.onRequestTimeout();
-      },
-      onTransportError : () =>
-      {
-        this._eventHandlers.onTransportError();
-      },
-      onAuthenticated : (request) =>
-      {
-        this._eventHandlers.onAuthenticated(request);
-      },
-      onReceiveResponse : (response) =>
-      {
-        this._receiveResponse(response);
-      }
-    });
+	send() {
+		const request_sender = new RequestSender(this._ua, this._request, {
+			onRequestTimeout: () => {
+				this._eventHandlers.onRequestTimeout();
+			},
+			onTransportError: () => {
+				this._eventHandlers.onTransportError();
+			},
+			onAuthenticated: request => {
+				this._eventHandlers.onAuthenticated(request);
+			},
+			onReceiveResponse: response => {
+				this._receiveResponse(response);
+			},
+		});

-    request_sender.send();
+		request_sender.send();

-    // RFC3261 14.2 Modifying an Existing Session -UAC BEHAVIOR-.
-    if ((this._request.method === JsSIP_C.INVITE ||
-          (this._request.method === JsSIP_C.UPDATE && this._request.body)) &&
-        request_sender.clientTransaction.state !== Transactions.C.STATUS_TERMINATED)
-    {
-      this._dialog.uac_pending_reply = true;
+		// RFC3261 14.2 Modifying an Existing Session -UAC BEHAVIOR-.
+		if (
+			(this._request.method === JsSIP_C.INVITE ||
+				(this._request.method === JsSIP_C.UPDATE && this._request.body)) &&
+			request_sender.clientTransaction.state !==
+				Transactions.C.STATUS_TERMINATED
+		) {
+			this._dialog.uac_pending_reply = true;

-      const stateChanged = () =>
-      {
-        if (request_sender.clientTransaction.state === Transactions.C.STATUS_ACCEPTED ||
-            request_sender.clientTransaction.state === Transactions.C.STATUS_COMPLETED ||
-            request_sender.clientTransaction.state === Transactions.C.STATUS_TERMINATED)
-        {
-          request_sender.clientTransaction.removeListener('stateChanged', stateChanged);
-          this._dialog.uac_pending_reply = false;
-        }
-      };
+			const stateChanged = () => {
+				if (
+					request_sender.clientTransaction.state ===
+						Transactions.C.STATUS_ACCEPTED ||
+					request_sender.clientTransaction.state ===
+						Transactions.C.STATUS_COMPLETED ||
+					request_sender.clientTransaction.state ===
+						Transactions.C.STATUS_TERMINATED
+				) {
+					request_sender.clientTransaction.removeListener(
+						'stateChanged',
+						stateChanged
+					);
+					this._dialog.uac_pending_reply = false;
+				}
+			};

-      request_sender.clientTransaction.on('stateChanged', stateChanged);
-    }
-  }
+			request_sender.clientTransaction.on('stateChanged', stateChanged);
+		}
+	}

-  _receiveResponse(response)
-  {
-    // RFC3261 12.2.1.2 408 or 481 is received for a request within a dialog.
-    if (response.status_code === 408 || response.status_code === 481)
-    {
-      this._eventHandlers.onDialogError(response);
-    }
-    else if (response.method === JsSIP_C.INVITE && response.status_code === 491)
-    {
-      if (this._reattempt)
-      {
-        this._eventHandlers.onErrorResponse(response);
-      }
-      else
-      {
-        this._request.cseq = this._dialog.local_seqnum += 1;
-        this._reattemptTimer = setTimeout(() =>
-        {
-          if (!this._dialog.isTerminated())
-          {
-            this._reattempt = true;
-            this.send();
-          }
-        }, 1000);
-      }
-    }
-    else if (response.status_code >= 200 && response.status_code < 300)
-    {
-      this._eventHandlers.onSuccessResponse(response);
-    }
-    else if (response.status_code >= 300)
-    {
-      this._eventHandlers.onErrorResponse(response);
-    }
-  }
+	_receiveResponse(response) {
+		// RFC3261 12.2.1.2 408 or 481 is received for a request within a dialog.
+		if (response.status_code === 408 || response.status_code === 481) {
+			this._eventHandlers.onDialogError(response);
+		} else if (
+			response.method === JsSIP_C.INVITE &&
+			response.status_code === 491
+		) {
+			if (this._reattempt) {
+				this._eventHandlers.onErrorResponse(response);
+			} else {
+				this._request.cseq = this._dialog.local_seqnum += 1;
+				this._reattemptTimer = setTimeout(() => {
+					if (!this._dialog.isTerminated()) {
+						this._reattempt = true;
+						this.send();
+					}
+				}, 1000);
+			}
+		} else if (response.status_code >= 200 && response.status_code < 300) {
+			this._eventHandlers.onSuccessResponse(response);
+		} else if (response.status_code >= 300) {
+			this._eventHandlers.onErrorResponse(response);
+		}
+	}
 };
diff --git a/src/DigestAuthentication.js b/src/DigestAuthentication.js
index 44b439d..4758cad 100644
--- a/src/DigestAuthentication.js
+++ b/src/DigestAuthentication.js
@@ -3,232 +3,225 @@ const Utils = require('./Utils');

 const logger = new Logger('DigestAuthentication');

-module.exports = class DigestAuthentication
-{
-  constructor(credentials)
-  {
-    this._credentials = credentials;
-    this._cnonce = null;
-    this._nc = 0;
-    this._ncHex = '00000000';
-    this._algorithm = null;
-    this._realm = null;
-    this._nonce = null;
-    this._opaque = null;
-    this._stale = null;
-    this._qop = null;
-    this._method = null;
-    this._uri = null;
-    this._ha1 = null;
-    this._response = null;
-  }
-
-  get(parameter)
-  {
-    switch (parameter)
-    {
-      case 'realm':
-        return this._realm;
-
-      case 'ha1':
-        return this._ha1;
-
-      default:
-        logger.warn('get() | cannot get "%s" parameter', parameter);
-
-        return undefined;
-    }
-  }
-
-  /**
-  * Performs Digest authentication given a SIP request and the challenge
-  * received in a response to that request.
-  * Returns true if auth was successfully generated, false otherwise.
-  */
-  authenticate({ method, ruri, body }, challenge, cnonce = null /* test interface */)
-  {
-    this._algorithm = challenge.algorithm;
-    this._realm = challenge.realm;
-    this._nonce = challenge.nonce;
-    this._opaque = challenge.opaque;
-    this._stale = challenge.stale;
-
-    if (this._algorithm)
-    {
-      if (this._algorithm !== 'MD5')
-      {
-        logger.warn('authenticate() | challenge with Digest algorithm different than "MD5", authentication aborted');
-
-        return false;
-      }
-    }
-    else
-    {
-      this._algorithm = 'MD5';
-    }
-
-    if (!this._nonce)
-    {
-      logger.warn('authenticate() | challenge without Digest nonce, authentication aborted');
-
-      return false;
-    }
-
-    if (!this._realm)
-    {
-      logger.warn('authenticate() | challenge without Digest realm, authentication aborted');
-
-      return false;
-    }
-
-    // If no plain SIP password is provided.
-    if (!this._credentials.password)
-    {
-      // If ha1 is not provided we cannot authenticate.
-      if (!this._credentials.ha1)
-      {
-        logger.warn('authenticate() | no plain SIP password nor ha1 provided, authentication aborted');
-
-        return false;
-      }
-
-      // If the realm does not match the stored realm we cannot authenticate.
-      if (this._credentials.realm !== this._realm)
-      {
-        logger.warn('authenticate() | no plain SIP password, and stored `realm` does not match the given `realm`, cannot authenticate [stored:"%s", given:"%s"]', this._credentials.realm, this._realm);
-
-        return false;
-      }
-    }
-
-    // 'qop' can contain a list of values (Array). Let's choose just one.
-    if (challenge.qop)
-    {
-      if (challenge.qop.indexOf('auth-int') > -1)
-      {
-        this._qop = 'auth-int';
-      }
-      else if (challenge.qop.indexOf('auth') > -1)
-      {
-        this._qop = 'auth';
-      }
-      else
-      {
-        // Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here.
-        logger.warn('authenticate() | challenge without Digest qop different than "auth" or "auth-int", authentication aborted');
-
-        return false;
-      }
-    }
-    else
-    {
-      this._qop = null;
-    }
-
-    // Fill other attributes.
-
-    this._method = method;
-    this._uri = ruri;
-    this._cnonce = cnonce || Utils.createRandomToken(12);
-    this._nc += 1;
-    const hex = Number(this._nc).toString(16);
-
-    this._ncHex = '00000000'.substr(0, 8-hex.length) + hex;
-
-    // Nc-value = 8LHEX. Max value = 'FFFFFFFF'.
-    if (this._nc === 4294967296)
-    {
-      this._nc = 1;
-      this._ncHex = '00000001';
-    }
-
-    // Calculate the Digest "response" value.
-
-    // If we have plain SIP password then regenerate ha1.
-    if (this._credentials.password)
-    {
-      // HA1 = MD5(A1) = MD5(username:realm:password).
-      this._ha1 = Utils.calculateMD5(`${this._credentials.username}:${this._realm}:${this._credentials.password}`);
-    }
-    // Otherwise reuse the stored ha1.
-    else
-    {
-      this._ha1 = this._credentials.ha1;
-    }
-
-    let a2;
-    let ha2;
-
-    if (this._qop === 'auth')
-    {
-      // HA2 = MD5(A2) = MD5(method:digestURI).
-      a2 = `${this._method}:${this._uri}`;
-      ha2 = Utils.calculateMD5(a2);
-
-      logger.debug('authenticate() | using qop=auth [a2:"%s"]', a2);
-
-      // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
-      this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth:${ha2}`);
-
-    }
-    else if (this._qop === 'auth-int')
-    {
-      // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)).
-      a2 = `${this._method}:${this._uri}:${Utils.calculateMD5(body ? body : '')}`;
-      ha2 = Utils.calculateMD5(a2);
-
-      logger.debug('authenticate() | using qop=auth-int [a2:"%s"]', a2);
-
-      // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
-      this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth-int:${ha2}`);
-
-    }
-    else if (this._qop === null)
-    {
-      // HA2 = MD5(A2) = MD5(method:digestURI).
-      a2 = `${this._method}:${this._uri}`;
-      ha2 = Utils.calculateMD5(a2);
-
-      logger.debug('authenticate() | using qop=null [a2:"%s"]', a2);
-
-      // Response = MD5(HA1:nonce:HA2).
-      this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${ha2}`);
-    }
-
-    logger.debug('authenticate() | response generated');
-
-    return true;
-  }
-
-  /**
-  * Return the Proxy-Authorization or WWW-Authorization header value.
-  */
-  toString()
-  {
-    const auth_params = [];
-
-    if (!this._response)
-    {
-      throw new Error('response field does not exist, cannot generate Authorization header');
-    }
-
-    auth_params.push(`algorithm=${this._algorithm}`);
-    auth_params.push(`username="${this._credentials.username}"`);
-    auth_params.push(`realm="${this._realm}"`);
-    auth_params.push(`nonce="${this._nonce}"`);
-    auth_params.push(`uri="${this._uri}"`);
-    auth_params.push(`response="${this._response}"`);
-    if (this._opaque)
-    {
-      auth_params.push(`opaque="${this._opaque}"`);
-    }
-    if (this._qop)
-    {
-      auth_params.push(`qop=${this._qop}`);
-      auth_params.push(`cnonce="${this._cnonce}"`);
-      auth_params.push(`nc=${this._ncHex}`);
-    }
-
-    return `Digest ${auth_params.join(', ')}`;
-  }
+module.exports = class DigestAuthentication {
+	constructor(credentials) {
+		this._credentials = credentials;
+		this._cnonce = null;
+		this._nc = 0;
+		this._ncHex = '00000000';
+		this._algorithm = null;
+		this._realm = null;
+		this._nonce = null;
+		this._opaque = null;
+		this._stale = null;
+		this._qop = null;
+		this._method = null;
+		this._uri = null;
+		this._ha1 = null;
+		this._response = null;
+	}
+
+	get(parameter) {
+		switch (parameter) {
+			case 'realm': {
+				return this._realm;
+			}
+
+			case 'ha1': {
+				return this._ha1;
+			}
+
+			default: {
+				logger.warn('get() | cannot get "%s" parameter', parameter);
+
+				return undefined;
+			}
+		}
+	}
+
+	/**
+	 * Performs Digest authentication given a SIP request and the challenge
+	 * received in a response to that request.
+	 * Returns true if auth was successfully generated, false otherwise.
+	 */
+	authenticate(
+		{ method, ruri, body },
+		challenge,
+		cnonce = null /* test interface */
+	) {
+		this._algorithm = challenge.algorithm;
+		this._realm = challenge.realm;
+		this._nonce = challenge.nonce;
+		this._opaque = challenge.opaque;
+		this._stale = challenge.stale;
+
+		if (this._algorithm) {
+			if (this._algorithm !== 'MD5') {
+				logger.warn(
+					'authenticate() | challenge with Digest algorithm different than "MD5", authentication aborted'
+				);
+
+				return false;
+			}
+		} else {
+			this._algorithm = 'MD5';
+		}
+
+		if (!this._nonce) {
+			logger.warn(
+				'authenticate() | challenge without Digest nonce, authentication aborted'
+			);
+
+			return false;
+		}
+
+		if (!this._realm) {
+			logger.warn(
+				'authenticate() | challenge without Digest realm, authentication aborted'
+			);
+
+			return false;
+		}
+
+		// If no plain SIP password is provided.
+		if (!this._credentials.password) {
+			// If ha1 is not provided we cannot authenticate.
+			if (!this._credentials.ha1) {
+				logger.warn(
+					'authenticate() | no plain SIP password nor ha1 provided, authentication aborted'
+				);
+
+				return false;
+			}
+
+			// If the realm does not match the stored realm we cannot authenticate.
+			if (this._credentials.realm !== this._realm) {
+				logger.warn(
+					'authenticate() | no plain SIP password, and stored `realm` does not match the given `realm`, cannot authenticate [stored:"%s", given:"%s"]',
+					this._credentials.realm,
+					this._realm
+				);
+
+				return false;
+			}
+		}
+
+		// 'qop' can contain a list of values (Array). Let's choose just one.
+		if (challenge.qop) {
+			if (challenge.qop.indexOf('auth-int') > -1) {
+				this._qop = 'auth-int';
+			} else if (challenge.qop.indexOf('auth') > -1) {
+				this._qop = 'auth';
+			} else {
+				// Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here.
+				logger.warn(
+					'authenticate() | challenge without Digest qop different than "auth" or "auth-int", authentication aborted'
+				);
+
+				return false;
+			}
+		} else {
+			this._qop = null;
+		}
+
+		// Fill other attributes.
+
+		this._method = method;
+		this._uri = ruri;
+		this._cnonce = cnonce || Utils.createRandomToken(12);
+		this._nc += 1;
+		const hex = Number(this._nc).toString(16);
+
+		this._ncHex = '00000000'.substr(0, 8 - hex.length) + hex;
+
+		// Nc-value = 8LHEX. Max value = 'FFFFFFFF'.
+		if (this._nc === 4294967296) {
+			this._nc = 1;
+			this._ncHex = '00000001';
+		}
+
+		// Calculate the Digest "response" value.
+
+		// If we have plain SIP password then regenerate ha1.
+		if (this._credentials.password) {
+			// HA1 = MD5(A1) = MD5(username:realm:password).
+			this._ha1 = Utils.calculateMD5(
+				`${this._credentials.username}:${this._realm}:${this._credentials.password}`
+			);
+		}
+		// Otherwise reuse the stored ha1.
+		else {
+			this._ha1 = this._credentials.ha1;
+		}
+
+		let a2;
+		let ha2;
+
+		if (this._qop === 'auth') {
+			// HA2 = MD5(A2) = MD5(method:digestURI).
+			a2 = `${this._method}:${this._uri}`;
+			ha2 = Utils.calculateMD5(a2);
+
+			logger.debug('authenticate() | using qop=auth [a2:"%s"]', a2);
+
+			// Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
+			this._response = Utils.calculateMD5(
+				`${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth:${ha2}`
+			);
+		} else if (this._qop === 'auth-int') {
+			// HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)).
+			a2 = `${this._method}:${this._uri}:${Utils.calculateMD5(body ? body : '')}`;
+			ha2 = Utils.calculateMD5(a2);
+
+			logger.debug('authenticate() | using qop=auth-int [a2:"%s"]', a2);
+
+			// Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
+			this._response = Utils.calculateMD5(
+				`${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth-int:${ha2}`
+			);
+		} else if (this._qop === null) {
+			// HA2 = MD5(A2) = MD5(method:digestURI).
+			a2 = `${this._method}:${this._uri}`;
+			ha2 = Utils.calculateMD5(a2);
+
+			logger.debug('authenticate() | using qop=null [a2:"%s"]', a2);
+
+			// Response = MD5(HA1:nonce:HA2).
+			this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${ha2}`);
+		}
+
+		logger.debug('authenticate() | response generated');
+
+		return true;
+	}
+
+	/**
+	 * Return the Proxy-Authorization or WWW-Authorization header value.
+	 */
+	toString() {
+		const auth_params = [];
+
+		if (!this._response) {
+			throw new Error(
+				'response field does not exist, cannot generate Authorization header'
+			);
+		}
+
+		auth_params.push(`algorithm=${this._algorithm}`);
+		auth_params.push(`username="${this._credentials.username}"`);
+		auth_params.push(`realm="${this._realm}"`);
+		auth_params.push(`nonce="${this._nonce}"`);
+		auth_params.push(`uri="${this._uri}"`);
+		auth_params.push(`response="${this._response}"`);
+		if (this._opaque) {
+			auth_params.push(`opaque="${this._opaque}"`);
+		}
+		if (this._qop) {
+			auth_params.push(`qop=${this._qop}`);
+			auth_params.push(`cnonce="${this._cnonce}"`);
+			auth_params.push(`nc=${this._ncHex}`);
+		}
+
+		return `Digest ${auth_params.join(', ')}`;
+	}
 };
diff --git a/src/Exceptions.d.ts b/src/Exceptions.d.ts
index 03c9fd8..818bf4a 100644
--- a/src/Exceptions.d.ts
+++ b/src/Exceptions.d.ts
@@ -1,24 +1,26 @@
 declare class BaseError extends Error {
-  code: number
+	code: number;
 }

 export class ConfigurationError extends BaseError {
-  parameter: string
-  value: any
+	parameter: string;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	value: any;

-  constructor(parameter: string, value?: any);
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	constructor(parameter: string, value?: any);
 }

 export class InvalidStateError extends BaseError {
-  status: number
+	status: number;

-  constructor(status: number);
+	constructor(status: number);
 }

 export class NotSupportedError extends BaseError {
-  constructor(message: string);
+	constructor(message: string);
 }

 export class NotReadyError extends BaseError {
-  constructor(message: string);
+	constructor(message: string);
 }
diff --git a/src/Exceptions.js b/src/Exceptions.js
index 70492c4..00d9ebc 100644
--- a/src/Exceptions.js
+++ b/src/Exceptions.js
@@ -1,59 +1,51 @@
-class ConfigurationError extends Error
-{
-  constructor(parameter, value)
-  {
-    super();
-
-    this.code = 1;
-    this.name = 'CONFIGURATION_ERROR';
-    this.parameter = parameter;
-    this.value = value;
-    this.message = (!this.value)?
-      `Missing parameter: ${this.parameter}` :
-      `Invalid value ${JSON.stringify(this.value)} for parameter "${this.parameter}"`;
-  }
+class ConfigurationError extends Error {
+	constructor(parameter, value) {
+		super();
+
+		this.code = 1;
+		this.name = 'CONFIGURATION_ERROR';
+		this.parameter = parameter;
+		this.value = value;
+		this.message = !this.value
+			? `Missing parameter: ${this.parameter}`
+			: `Invalid value ${JSON.stringify(this.value)} for parameter "${this.parameter}"`;
+	}
 }

-class InvalidStateError extends Error
-{
-  constructor(status)
-  {
-    super();
-
-    this.code = 2;
-    this.name = 'INVALID_STATE_ERROR';
-    this.status = status;
-    this.message = `Invalid status: ${status}`;
-  }
+class InvalidStateError extends Error {
+	constructor(status) {
+		super();
+
+		this.code = 2;
+		this.name = 'INVALID_STATE_ERROR';
+		this.status = status;
+		this.message = `Invalid status: ${status}`;
+	}
 }

-class NotSupportedError extends Error
-{
-  constructor(message)
-  {
-    super();
+class NotSupportedError extends Error {
+	constructor(message) {
+		super();

-    this.code = 3;
-    this.name = 'NOT_SUPPORTED_ERROR';
-    this.message = message;
-  }
+		this.code = 3;
+		this.name = 'NOT_SUPPORTED_ERROR';
+		this.message = message;
+	}
 }

-class NotReadyError extends Error
-{
-  constructor(message)
-  {
-    super();
+class NotReadyError extends Error {
+	constructor(message) {
+		super();

-    this.code = 4;
-    this.name = 'NOT_READY_ERROR';
-    this.message = message;
-  }
+		this.code = 4;
+		this.name = 'NOT_READY_ERROR';
+		this.message = message;
+	}
 }

 module.exports = {
-  ConfigurationError,
-  InvalidStateError,
-  NotSupportedError,
-  NotReadyError
+	ConfigurationError,
+	InvalidStateError,
+	NotSupportedError,
+	NotReadyError,
 };
diff --git a/src/Grammar.d.ts b/src/Grammar.d.ts
index 6ac7501..3baa70c 100644
--- a/src/Grammar.d.ts
+++ b/src/Grammar.d.ts
@@ -1,5 +1,7 @@
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export type Grammar = any;

 export function parse(input: string, startRule?: string): Grammar;

+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export function toSource(): any;
diff --git a/src/JsSIP.d.ts b/src/JsSIP.d.ts
index 42edaff..e9a896e 100644
--- a/src/JsSIP.d.ts
+++ b/src/JsSIP.d.ts
@@ -1,18 +1,18 @@
-import {Debug} from 'debug'
+import { Debug } from 'debug';

-import * as C from './Constants'
-import * as Exceptions from './Exceptions'
-import * as Grammar from './Grammar'
-import * as Utils from './Utils'
+import * as C from './Constants';
+import * as Exceptions from './Exceptions';
+import * as Grammar from './Grammar';
+import * as Utils from './Utils';

 export { C, Exceptions, Grammar, Utils };

-export {UA} from './UA'
-export {URI} from './URI'
-export {NameAddrHeader} from './NameAddrHeader'
-export {WebSocketInterface} from './WebSocketInterface'
-export {Socket, WeightedSocket} from './Socket'
+export { UA } from './UA';
+export { URI } from './URI';
+export { NameAddrHeader } from './NameAddrHeader';
+export { WebSocketInterface } from './WebSocketInterface';
+export { Socket, WeightedSocket } from './Socket';

-export const debug: Debug
-export const name: string
-export const version: string
+export const debug: Debug;
+export const name: string;
+export const version: string;
diff --git a/src/JsSIP.js b/src/JsSIP.js
index bb9fa24..39481d1 100644
--- a/src/JsSIP.js
+++ b/src/JsSIP.js
@@ -16,17 +16,21 @@ debug('version %s', pkg.version);
  * Expose the JsSIP module.
  */
 module.exports = {
-  C,
-  Exceptions,
-  Utils,
-  UA,
-  URI,
-  NameAddrHeader,
-  WebSocketInterface,
-  Grammar,
-  RTCSession,
-  // Expose the debug module.
-  debug : require('debug'),
-  get name() { return pkg.title; },
-  get version() { return pkg.version; }
+	C,
+	Exceptions,
+	Utils,
+	UA,
+	URI,
+	NameAddrHeader,
+	WebSocketInterface,
+	Grammar,
+	RTCSession,
+	// Expose the debug module.
+	debug: require('debug'),
+	get name() {
+		return pkg.title;
+	},
+	get version() {
+		return pkg.version;
+	},
 };
diff --git a/src/Logger.js b/src/Logger.js
index 4feeceb..4abb8e7 100644
--- a/src/Logger.js
+++ b/src/Logger.js
@@ -2,41 +2,33 @@ const debug = require('debug');

 const APP_NAME = 'JsSIP';

-module.exports = class Logger
-{
-  constructor(prefix)
-  {
-    if (prefix)
-    {
-      this._debug = debug.default(`${APP_NAME}:${prefix}`);
-      this._warn = debug.default(`${APP_NAME}:WARN:${prefix}`);
-      this._error = debug.default(`${APP_NAME}:ERROR:${prefix}`);
-    }
-    else
-    {
-      this._debug = debug.default(APP_NAME);
-      this._warn = debug.default(`${APP_NAME}:WARN`);
-      this._error = debug.default(`${APP_NAME}:ERROR`);
-    }
-    /* eslint-disable no-console */
-    this._debug.log = console.info.bind(console);
-    this._warn.log = console.warn.bind(console);
-    this._error.log = console.error.bind(console);
-    /* eslint-enable no-console */
-  }
+module.exports = class Logger {
+	constructor(prefix) {
+		if (prefix) {
+			this._debug = debug.default(`${APP_NAME}:${prefix}`);
+			this._warn = debug.default(`${APP_NAME}:WARN:${prefix}`);
+			this._error = debug.default(`${APP_NAME}:ERROR:${prefix}`);
+		} else {
+			this._debug = debug.default(APP_NAME);
+			this._warn = debug.default(`${APP_NAME}:WARN`);
+			this._error = debug.default(`${APP_NAME}:ERROR`);
+		}
+		/* eslint-disable no-console */
+		this._debug.log = console.info.bind(console);
+		this._warn.log = console.warn.bind(console);
+		this._error.log = console.error.bind(console);
+		/* eslint-enable no-console */
+	}

-  get debug()
-  {
-    return this._debug;
-  }
+	get debug() {
+		return this._debug;
+	}

-  get warn()
-  {
-    return this._warn;
-  }
+	get warn() {
+		return this._warn;
+	}

-  get error()
-  {
-    return this._error;
-  }
+	get error() {
+		return this._error;
+	}
 };
diff --git a/src/Message.d.ts b/src/Message.d.ts
index 8ee3685..e0cc24b 100644
--- a/src/Message.d.ts
+++ b/src/Message.d.ts
@@ -1,46 +1,55 @@
-import {EventEmitter} from 'events'
-
-import {ExtraHeaders, Originator, OutgoingListener, SessionDirection, TerminateOptions} from './RTCSession'
-import {IncomingResponse} from './SIPMessage'
-import {NameAddrHeader} from './NameAddrHeader'
-import {causes} from './Constants';
+import { EventEmitter } from 'events';
+
+import {
+	ExtraHeaders,
+	Originator,
+	OutgoingListener,
+	SessionDirection,
+	TerminateOptions,
+} from './RTCSession';
+import { IncomingResponse } from './SIPMessage';
+import { NameAddrHeader } from './NameAddrHeader';
+import { causes } from './Constants';

 export interface AcceptOptions extends ExtraHeaders {
-  body?: string;
+	body?: string;
 }

 export interface MessageFailedEvent {
-  originator: Originator;
-  response: IncomingResponse;
-  cause?: causes;
+	originator: Originator;
+	response: IncomingResponse;
+	cause?: causes;
 }

 export type MessageFailedListener = (event: MessageFailedEvent) => void;

 export interface MessageEventMap {
-  succeeded: OutgoingListener;
-  failed: MessageFailedListener;
+	succeeded: OutgoingListener;
+	failed: MessageFailedListener;
 }

 export interface SendMessageOptions extends ExtraHeaders {
-  contentType?: string;
-  eventHandlers?: Partial<MessageEventMap>;
-  fromUserName?: string;
-  fromDisplayName?: string;
+	contentType?: string;
+	eventHandlers?: Partial<MessageEventMap>;
+	fromUserName?: string;
+	fromDisplayName?: string;
 }

 export class Message extends EventEmitter {
-  get direction(): SessionDirection;
+	get direction(): SessionDirection;

-  get local_identity(): NameAddrHeader;
+	get local_identity(): NameAddrHeader;

-  get remote_identity(): NameAddrHeader;
+	get remote_identity(): NameAddrHeader;

-  send(target: string, body: string, options?: SendMessageOptions): void;
+	send(target: string, body: string, options?: SendMessageOptions): void;

-  accept(options: AcceptOptions): void;
+	accept(options: AcceptOptions): void;

-  reject(options: TerminateOptions): void;
+	reject(options: TerminateOptions): void;

-  on<T extends keyof MessageEventMap>(type: T, listener: MessageEventMap[T]): this;
+	on<T extends keyof MessageEventMap>(
+		type: T,
+		listener: MessageEventMap[T]
+	): this;
 }
diff --git a/src/Message.js b/src/Message.js
index daadbc5..801f823 100644
--- a/src/Message.js
+++ b/src/Message.js
@@ -9,287 +9,264 @@ const URI = require('./URI');

 const logger = new Logger('Message');

-module.exports = class Message extends EventEmitter
-{
-  constructor(ua)
-  {
-    super();
-
-    this._ua = ua;
-    this._request = null;
-    this._closed = false;
-
-    this._direction = null;
-    this._local_identity = null;
-    this._remote_identity = null;
-
-    // Whether an incoming message has been replied.
-    this._is_replied = false;
-
-    // Custom message empty object for high level use.
-    this._data = {};
-  }
-
-  get direction()
-  {
-    return this._direction;
-  }
-
-  get local_identity()
-  {
-    return this._local_identity;
-  }
-
-  get remote_identity()
-  {
-    return this._remote_identity;
-  }
-
-  send(target, body, options = {})
-  {
-    const originalTarget = target;
-
-    if (target === undefined || body === undefined)
-    {
-      throw new TypeError('Not enough arguments');
-    }
-
-    // Check target validity.
-    target = this._ua.normalizeTarget(target);
-    if (!target)
-    {
-      throw new TypeError(`Invalid target: ${originalTarget}`);
-    }
-
-    // Get call options.
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const eventHandlers = Utils.cloneObject(options.eventHandlers);
-    const contentType = options.contentType || 'text/plain';
-
-    const requestParams = {};
-
-    if (options.fromUserName)
-    {
-      requestParams.from_uri = new URI('sip', options.fromUserName, this._ua.configuration.uri.host);
-
-      extraHeaders.push(`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`);
-    }
-
-    if (options.fromDisplayName)
-    {
-      requestParams.from_display_name = options.fromDisplayName;
-    }
-
-    // Set event handlers.
-    for (const event in eventHandlers)
-    {
-      if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
-      {
-        this.on(event, eventHandlers[event]);
-      }
-    }
-
-    extraHeaders.push(`Content-Type: ${contentType}`);
-
-    this._request = new SIPMessage.OutgoingRequest(
-      JsSIP_C.MESSAGE, target, this._ua, requestParams, extraHeaders);
-
-    if (body)
-    {
-      this._request.body = body;
-    }
-
-    const request_sender = new RequestSender(this._ua, this._request, {
-      onRequestTimeout : () =>
-      {
-        this._onRequestTimeout();
-      },
-      onTransportError : () =>
-      {
-        this._onTransportError();
-      },
-      onReceiveResponse : (response) =>
-      {
-        this._receiveResponse(response);
-      }
-    });
-
-    this._newMessage('local', this._request);
-
-    request_sender.send();
-  }
-
-  init_incoming(request)
-  {
-    this._request = request;
-
-    this._newMessage('remote', request);
-
-    // Reply with a 200 OK if the user didn't reply.
-    if (!this._is_replied)
-    {
-      this._is_replied = true;
-      request.reply(200);
-    }
-
-    this._close();
-  }
-
-  /**
-   * Accept the incoming Message
-   * Only valid for incoming Messages
-   */
-  accept(options = {})
-  {
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const body = options.body;
-
-    if (this._direction !== 'incoming')
-    {
-      throw new Exceptions.NotSupportedError('"accept" not supported for outgoing Message');
-    }
-
-    if (this._is_replied)
-    {
-      throw new Error('incoming Message already replied');
-    }
-
-    this._is_replied = true;
-    this._request.reply(200, null, extraHeaders, body);
-  }
-
-  /**
-   * Reject the incoming Message
-   * Only valid for incoming Messages
-   */
-  reject(options = {})
-  {
-    const status_code = options.status_code || 480;
-    const reason_phrase = options.reason_phrase;
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const body = options.body;
-
-    if (this._direction !== 'incoming')
-    {
-      throw new Exceptions.NotSupportedError('"reject" not supported for outgoing Message');
-    }
-
-    if (this._is_replied)
-    {
-      throw new Error('incoming Message already replied');
-    }
-
-    if (status_code < 300 || status_code >= 700)
-    {
-      throw new TypeError(`Invalid status_code: ${status_code}`);
-    }
-
-    this._is_replied = true;
-    this._request.reply(status_code, reason_phrase, extraHeaders, body);
-  }
-
-  _receiveResponse(response)
-  {
-    if (this._closed)
-    {
-      return;
-    }
-    switch (true)
-    {
-      case /^1[0-9]{2}$/.test(response.status_code):
-        // Ignore provisional responses.
-        break;
-
-      case /^2[0-9]{2}$/.test(response.status_code):
-        this._succeeded('remote', response);
-        break;
-
-      default:
-      {
-        const cause = Utils.sipErrorCause(response.status_code);
-
-        this._failed('remote', response, cause);
-        break;
-      }
-    }
-  }
-
-  _onRequestTimeout()
-  {
-    if (this._closed)
-    {
-      return;
-    }
-    this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
-  }
-
-  _onTransportError()
-  {
-    if (this._closed)
-    {
-      return;
-    }
-    this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
-  }
-
-  _close()
-  {
-    this._closed = true;
-    this._ua.destroyMessage(this);
-  }
-
-  /**
-   * Internal Callbacks
-   */
-
-  _newMessage(originator, request)
-  {
-    if (originator === 'remote')
-    {
-      this._direction = 'incoming';
-      this._local_identity = request.to;
-      this._remote_identity = request.from;
-    }
-    else if (originator === 'local')
-    {
-      this._direction = 'outgoing';
-      this._local_identity = request.from;
-      this._remote_identity = request.to;
-    }
-
-    this._ua.newMessage(this, {
-      originator,
-      message : this,
-      request
-    });
-  }
-
-  _failed(originator, response, cause)
-  {
-    logger.debug('MESSAGE failed');
-
-    this._close();
-
-    logger.debug('emit "failed"');
-
-    this.emit('failed', {
-      originator,
-      response : response || null,
-      cause
-    });
-  }
-
-  _succeeded(originator, response)
-  {
-    logger.debug('MESSAGE succeeded');
-
-    this._close();
-
-    logger.debug('emit "succeeded"');
-
-    this.emit('succeeded', {
-      originator,
-      response
-    });
-  }
+module.exports = class Message extends EventEmitter {
+	constructor(ua) {
+		super();
+
+		this._ua = ua;
+		this._request = null;
+		this._closed = false;
+
+		this._direction = null;
+		this._local_identity = null;
+		this._remote_identity = null;
+
+		// Whether an incoming message has been replied.
+		this._is_replied = false;
+
+		// Custom message empty object for high level use.
+		this._data = {};
+	}
+
+	get direction() {
+		return this._direction;
+	}
+
+	get local_identity() {
+		return this._local_identity;
+	}
+
+	get remote_identity() {
+		return this._remote_identity;
+	}
+
+	send(target, body, options = {}) {
+		const originalTarget = target;
+
+		if (target === undefined || body === undefined) {
+			throw new TypeError('Not enough arguments');
+		}
+
+		// Check target validity.
+		target = this._ua.normalizeTarget(target);
+		if (!target) {
+			throw new TypeError(`Invalid target: ${originalTarget}`);
+		}
+
+		// Get call options.
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const eventHandlers = Utils.cloneObject(options.eventHandlers);
+		const contentType = options.contentType || 'text/plain';
+
+		const requestParams = {};
+
+		if (options.fromUserName) {
+			requestParams.from_uri = new URI(
+				'sip',
+				options.fromUserName,
+				this._ua.configuration.uri.host
+			);
+
+			extraHeaders.push(
+				`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`
+			);
+		}
+
+		if (options.fromDisplayName) {
+			requestParams.from_display_name = options.fromDisplayName;
+		}
+
+		// Set event handlers.
+		for (const event in eventHandlers) {
+			if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+				this.on(event, eventHandlers[event]);
+			}
+		}
+
+		extraHeaders.push(`Content-Type: ${contentType}`);
+
+		this._request = new SIPMessage.OutgoingRequest(
+			JsSIP_C.MESSAGE,
+			target,
+			this._ua,
+			requestParams,
+			extraHeaders
+		);
+
+		if (body) {
+			this._request.body = body;
+		}
+
+		const request_sender = new RequestSender(this._ua, this._request, {
+			onRequestTimeout: () => {
+				this._onRequestTimeout();
+			},
+			onTransportError: () => {
+				this._onTransportError();
+			},
+			onReceiveResponse: response => {
+				this._receiveResponse(response);
+			},
+		});
+
+		this._newMessage('local', this._request);
+
+		request_sender.send();
+	}
+
+	init_incoming(request) {
+		this._request = request;
+
+		this._newMessage('remote', request);
+
+		// Reply with a 200 OK if the user didn't reply.
+		if (!this._is_replied) {
+			this._is_replied = true;
+			request.reply(200);
+		}
+
+		this._close();
+	}
+
+	/**
+	 * Accept the incoming Message
+	 * Only valid for incoming Messages
+	 */
+	accept(options = {}) {
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const body = options.body;
+
+		if (this._direction !== 'incoming') {
+			throw new Exceptions.NotSupportedError(
+				'"accept" not supported for outgoing Message'
+			);
+		}
+
+		if (this._is_replied) {
+			throw new Error('incoming Message already replied');
+		}
+
+		this._is_replied = true;
+		this._request.reply(200, null, extraHeaders, body);
+	}
+
+	/**
+	 * Reject the incoming Message
+	 * Only valid for incoming Messages
+	 */
+	reject(options = {}) {
+		const status_code = options.status_code || 480;
+		const reason_phrase = options.reason_phrase;
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const body = options.body;
+
+		if (this._direction !== 'incoming') {
+			throw new Exceptions.NotSupportedError(
+				'"reject" not supported for outgoing Message'
+			);
+		}
+
+		if (this._is_replied) {
+			throw new Error('incoming Message already replied');
+		}
+
+		if (status_code < 300 || status_code >= 700) {
+			throw new TypeError(`Invalid status_code: ${status_code}`);
+		}
+
+		this._is_replied = true;
+		this._request.reply(status_code, reason_phrase, extraHeaders, body);
+	}
+
+	_receiveResponse(response) {
+		if (this._closed) {
+			return;
+		}
+		switch (true) {
+			case /^1[0-9]{2}$/.test(response.status_code): {
+				// Ignore provisional responses.
+				break;
+			}
+
+			case /^2[0-9]{2}$/.test(response.status_code): {
+				this._succeeded('remote', response);
+				break;
+			}
+
+			default: {
+				const cause = Utils.sipErrorCause(response.status_code);
+
+				this._failed('remote', response, cause);
+				break;
+			}
+		}
+	}
+
+	_onRequestTimeout() {
+		if (this._closed) {
+			return;
+		}
+		this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
+	}
+
+	_onTransportError() {
+		if (this._closed) {
+			return;
+		}
+		this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
+	}
+
+	_close() {
+		this._closed = true;
+		this._ua.destroyMessage(this);
+	}
+
+	/**
+	 * Internal Callbacks
+	 */
+
+	_newMessage(originator, request) {
+		if (originator === 'remote') {
+			this._direction = 'incoming';
+			this._local_identity = request.to;
+			this._remote_identity = request.from;
+		} else if (originator === 'local') {
+			this._direction = 'outgoing';
+			this._local_identity = request.from;
+			this._remote_identity = request.to;
+		}
+
+		this._ua.newMessage(this, {
+			originator,
+			message: this,
+			request,
+		});
+	}
+
+	_failed(originator, response, cause) {
+		logger.debug('MESSAGE failed');
+
+		this._close();
+
+		logger.debug('emit "failed"');
+
+		this.emit('failed', {
+			originator,
+			response: response || null,
+			cause,
+		});
+	}
+
+	_succeeded(originator, response) {
+		logger.debug('MESSAGE succeeded');
+
+		this._close();
+
+		logger.debug('emit "succeeded"');
+
+		this.emit('succeeded', {
+			originator,
+			response,
+		});
+	}
 };
diff --git a/src/NameAddrHeader.d.ts b/src/NameAddrHeader.d.ts
index 6529c10..59e15cb 100644
--- a/src/NameAddrHeader.d.ts
+++ b/src/NameAddrHeader.d.ts
@@ -1,27 +1,28 @@
-import {Parameters, URI} from './URI'
-import {Grammar} from './Grammar'
+import { Parameters, URI } from './URI';
+import { Grammar } from './Grammar';

 export class NameAddrHeader {
-  get display_name(): string;
-  set display_name(value: string);
+	get display_name(): string;
+	set display_name(value: string);

-  get uri(): URI;
+	get uri(): URI;

-  constructor(uri: URI, display_name?: string, parameters?: Parameters);
+	constructor(uri: URI, display_name?: string, parameters?: Parameters);

-  setParam(key: string, value?: string): void;
+	setParam(key: string, value?: string): void;

-  getParam<T = any>(key: string): T;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	getParam<T = any>(key: string): T;

-  hasParam(key: string): boolean;
+	hasParam(key: string): boolean;

-  deleteParam(key: string): void;
+	deleteParam(key: string): void;

-  clearParams(): void;
+	clearParams(): void;

-  clone(): this;
+	clone(): this;

-  toString(): string;
+	toString(): string;

-  static parse(uri: string): Grammar | undefined;
+	static parse(uri: string): Grammar | undefined;
 }
diff --git a/src/NameAddrHeader.js b/src/NameAddrHeader.js
index dd4e454..8119331 100644
--- a/src/NameAddrHeader.js
+++ b/src/NameAddrHeader.js
@@ -1,139 +1,118 @@
 const URI = require('./URI');
 const Grammar = require('./Grammar');

-module.exports = class NameAddrHeader
-{
-  /**
-   * Parse the given string and returns a NameAddrHeader instance or undefined if
-   * it is an invalid NameAddrHeader.
-   */
-  static parse(name_addr_header)
-  {
-    name_addr_header = Grammar.parse(name_addr_header, 'Name_Addr_Header');
-
-    if (name_addr_header !== -1)
-    {
-      return name_addr_header;
-    }
-    else
-    {
-      return undefined;
-    }
-  }
-
-  constructor(uri, display_name, parameters)
-  {
-    // Checks.
-    if (!uri || !(uri instanceof URI))
-    {
-      throw new TypeError('missing or invalid "uri" parameter');
-    }
-
-    // Initialize parameters.
-    this._uri = uri;
-    this._parameters = {};
-    this.display_name = display_name;
-
-    for (const param in parameters)
-    {
-      if (Object.prototype.hasOwnProperty.call(parameters, param))
-      {
-        this.setParam(param, parameters[param]);
-      }
-    }
-  }
-
-  get uri()
-  {
-    return this._uri;
-  }
-
-  get display_name()
-  {
-    return this._display_name;
-  }
-
-  set display_name(value)
-  {
-    this._display_name = (value === 0) ? '0' : value;
-  }
-
-  setParam(key, value)
-  {
-    if (key)
-    {
-      this._parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString();
-    }
-  }
-
-  getParam(key)
-  {
-    if (key)
-    {
-      return this._parameters[key.toLowerCase()];
-    }
-  }
-
-  hasParam(key)
-  {
-    if (key)
-    {
-      return (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false;
-    }
-  }
-
-  deleteParam(parameter)
-  {
-    parameter = parameter.toLowerCase();
-    if (this._parameters.hasOwnProperty(parameter))
-    {
-      const value = this._parameters[parameter];
-
-      delete this._parameters[parameter];
-
-      return value;
-    }
-  }
-
-  clearParams()
-  {
-    this._parameters = {};
-  }
-
-  clone()
-  {
-    return new NameAddrHeader(
-      this._uri.clone(),
-      this._display_name,
-      JSON.parse(JSON.stringify(this._parameters)));
-  }
-
-  _quote(str)
-  {
-    return str
-      .replace(/\\/g, '\\\\')
-      .replace(/"/g, '\\"');
-  }
-
-  toString()
-  {
-    let body = this._display_name ? `"${this._quote(this._display_name)}" ` : '';
-
-    body += `<${this._uri.toString()}>`;
-
-    for (const parameter in this._parameters)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._parameters, parameter))
-      {
-        body += `;${parameter}`;
-
-        if (this._parameters[parameter] !== null)
-        {
-          body += `=${this._parameters[parameter]}`;
-        }
-      }
-    }
-
-    return body;
-  }
+module.exports = class NameAddrHeader {
+	/**
+	 * Parse the given string and returns a NameAddrHeader instance or undefined if
+	 * it is an invalid NameAddrHeader.
+	 */
+	static parse(name_addr_header) {
+		name_addr_header = Grammar.parse(name_addr_header, 'Name_Addr_Header');
+
+		if (name_addr_header !== -1) {
+			return name_addr_header;
+		} else {
+			return undefined;
+		}
+	}
+
+	constructor(uri, display_name, parameters) {
+		// Checks.
+		if (!uri || !(uri instanceof URI)) {
+			throw new TypeError('missing or invalid "uri" parameter');
+		}
+
+		// Initialize parameters.
+		this._uri = uri;
+		this._parameters = {};
+		this.display_name = display_name;
+
+		for (const param in parameters) {
+			if (Object.prototype.hasOwnProperty.call(parameters, param)) {
+				this.setParam(param, parameters[param]);
+			}
+		}
+	}
+
+	get uri() {
+		return this._uri;
+	}
+
+	get display_name() {
+		return this._display_name;
+	}
+
+	set display_name(value) {
+		this._display_name = value === 0 ? '0' : value;
+	}
+
+	setParam(key, value) {
+		if (key) {
+			this._parameters[key.toLowerCase()] =
+				typeof value === 'undefined' || value === null
+					? null
+					: value.toString();
+		}
+	}
+
+	getParam(key) {
+		if (key) {
+			return this._parameters[key.toLowerCase()];
+		}
+	}
+
+	hasParam(key) {
+		if (key) {
+			return (
+				(this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false
+			);
+		}
+	}
+
+	deleteParam(parameter) {
+		parameter = parameter.toLowerCase();
+		if (this._parameters.hasOwnProperty(parameter)) {
+			const value = this._parameters[parameter];
+
+			delete this._parameters[parameter];
+
+			return value;
+		}
+	}
+
+	clearParams() {
+		this._parameters = {};
+	}
+
+	clone() {
+		return new NameAddrHeader(
+			this._uri.clone(),
+			this._display_name,
+			JSON.parse(JSON.stringify(this._parameters))
+		);
+	}
+
+	_quote(str) {
+		return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+	}
+
+	toString() {
+		let body = this._display_name
+			? `"${this._quote(this._display_name)}" `
+			: '';
+
+		body += `<${this._uri.toString()}>`;
+
+		for (const parameter in this._parameters) {
+			if (Object.prototype.hasOwnProperty.call(this._parameters, parameter)) {
+				body += `;${parameter}`;
+
+				if (this._parameters[parameter] !== null) {
+					body += `=${this._parameters[parameter]}`;
+				}
+			}
+		}
+
+		return body;
+	}
 };
diff --git a/src/Notifier.d.ts b/src/Notifier.d.ts
index aa863ea..85d2bbb 100644
--- a/src/Notifier.d.ts
+++ b/src/Notifier.d.ts
@@ -1,40 +1,52 @@
-import {EventEmitter} from 'events'
-import {IncomingRequest} from './SIPMessage'
-import {UA} from './UA'
+import { EventEmitter } from 'events';
+import { IncomingRequest } from './SIPMessage';
+import { UA } from './UA';

 declare enum NotifierTerminationReason {
-    NOTIFY_RESPONSE_TIMEOUT = 0,
-    NOTIFY_TRANSPORT_ERROR = 1,
-    NOTIFY_NON_OK_RESPONSE = 2,
-    NOTIFY_AUTHENTICATION_FAILED = 3,
-    FINAL_NOTIFY_SENT = 4,
-    UNSUBSCRIBE_RECEIVED = 5,
-    SUBSCRIPTION_EXPIRED = 6
+	NOTIFY_RESPONSE_TIMEOUT = 0,
+	NOTIFY_TRANSPORT_ERROR = 1,
+	NOTIFY_NON_OK_RESPONSE = 2,
+	NOTIFY_AUTHENTICATION_FAILED = 3,
+	FINAL_NOTIFY_SENT = 4,
+	UNSUBSCRIBE_RECEIVED = 5,
+	SUBSCRIPTION_EXPIRED = 6,
 }

 export interface MessageEventMap {
-  terminated: [terminationCode: NotifierTerminationReason];
-  subscribe: [isUnsubscribe: boolean, request: IncomingRequest, body: string | undefined, contentType: string | undefined];
-  expired: [];
+	terminated: [terminationCode: NotifierTerminationReason];
+	subscribe: [
+		isUnsubscribe: boolean,
+		request: IncomingRequest,
+		body: string | undefined,
+		contentType: string | undefined,
+	];
+	expired: [];
 }

 interface NotifierOptions {
-  extraHeaders?: Array<string>;
-  allowEvents?: string;
-  pending?: boolean;
-  defaultExpires?: number;
+	extraHeaders?: string[];
+	allowEvents?: string;
+	pending?: boolean;
+	defaultExpires?: number;
 }

 export class Notifier extends EventEmitter<MessageEventMap> {
-  constructor(ua: UA, subscribe: IncomingRequest, contentType: string, options: NotifierOptions)
-  start(): void;
-  setActiveState(): void;
-  notify(body?: string): void;
-  terminate(body?: string, reason?: string, retryAfter?: number): void;
-  get state(): string;
-  get id(): string;
-  set data(_data: any);
-  get data(): any;
-  static get C(): typeof NotifierTerminationReason;
-  get C(): typeof NotifierTerminationReason;
+	constructor(
+		ua: UA,
+		subscribe: IncomingRequest,
+		contentType: string,
+		options: NotifierOptions
+	);
+	start(): void;
+	setActiveState(): void;
+	notify(body?: string): void;
+	terminate(body?: string, reason?: string, retryAfter?: number): void;
+	get state(): string;
+	get id(): string;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	set data(_data: any);
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	get data(): any;
+	static get C(): typeof NotifierTerminationReason;
+	get C(): typeof NotifierTerminationReason;
 }
diff --git a/src/Notifier.js b/src/Notifier.js
index 529db4a..deea8bb 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -11,459 +11,420 @@ const logger = new Logger('Notifier');
  * Termination codes.
  */
 const C = {
-  // Termination codes.
-  NOTIFY_RESPONSE_TIMEOUT      : 0,
-  NOTIFY_TRANSPORT_ERROR       : 1,
-  NOTIFY_NON_OK_RESPONSE       : 2,
-  NOTIFY_AUTHENTICATION_FAILED : 3,
-  FINAL_NOTIFY_SENT            : 4,
-  UNSUBSCRIBE_RECEIVED         : 5,
-  SUBSCRIPTION_EXPIRED         : 6,
-
-  // Notifer states.
-  STATE_PENDING    : 0,
-  STATE_ACTIVE     : 1,
-  STATE_TERMINATED : 2,
-
-  // RFC 6665 3.1.1, default expires value.
-  DEFAULT_EXPIRES_SEC : 900
+	// Termination codes.
+	NOTIFY_RESPONSE_TIMEOUT: 0,
+	NOTIFY_TRANSPORT_ERROR: 1,
+	NOTIFY_NON_OK_RESPONSE: 2,
+	NOTIFY_AUTHENTICATION_FAILED: 3,
+	FINAL_NOTIFY_SENT: 4,
+	UNSUBSCRIBE_RECEIVED: 5,
+	SUBSCRIPTION_EXPIRED: 6,
+
+	// Notifer states.
+	STATE_PENDING: 0,
+	STATE_ACTIVE: 1,
+	STATE_TERMINATED: 2,
+
+	// RFC 6665 3.1.1, default expires value.
+	DEFAULT_EXPIRES_SEC: 900,
 };

 /**
  * RFC 6665 Notifier implementation.
  */
-module.exports = class Notifier extends EventEmitter
-{
-  /**
-   * Expose C object.
-   */
-  static get C()
-  {
-    return C;
-  }
-
-  static init_incoming(request, callback)
-  {
-    try
-    {
-      Notifier.checkSubscribe(request);
-    }
-    catch (error)
-    {
-      logger.warn('Notifier.init_incoming: invalid request. Error: ', error.message);
-
-      request.reply(405);
-
-      return;
-    }
-
-    callback();
-  }
-
-  static checkSubscribe(subscribe)
-  {
-    if (!subscribe)
-    {
-      throw new TypeError('Not enough arguments. Missing subscribe request');
-    }
-    if (subscribe.method !== JsSIP_C.SUBSCRIBE)
-    {
-      throw new TypeError('Invalid method for Subscribe request');
-    }
-    if (!subscribe.hasHeader('contact'))
-    {
-      throw new TypeError('Missing Contact header in subscribe request');
-    }
-    if (!subscribe.hasHeader('event'))
-    {
-      throw new TypeError('Missing Event header in subscribe request');
-    }
-    const expires = subscribe.getHeader('expires');
-
-    if (expires)
-    {
-      const parsed_expires = parseInt(expires);
-
-      if (!Utils.isDecimal(parsed_expires) || parsed_expires < 0)
-      {
-        throw new TypeError('Invalid Expires header field in subscribe request');
-      }
-    }
-  }
-
-  /**
-   * @param {UA} ua - JsSIP User Agent instance.
-   * @param {IncomingRequest} subscribe - Subscribe request.
-   * @param {string} contentType - Content-Type header value.
-   * @param {NotifierOptions} options - Optional parameters.
-   *   @param {Array<string>}  extraHeaders - Additional SIP headers.
-   *   @param {string} allowEvents - Allow-Events header value.
-   *   @param {boolean} pending - Set initial dialog state as "pending".
-   *   @param {number} defaultExpires - Default expires value (seconds).
-   */
-  constructor(ua, subscribe, contentType, {
-    extraHeaders, allowEvents, pending, defaultExpires
-  })
-  {
-    logger.debug('new');
-
-    super();
-
-    if (!contentType)
-    {
-      throw new TypeError('Not enough arguments. Missing contentType');
-    }
-
-    Notifier.checkSubscribe(subscribe);
-
-    const eventName = subscribe.getHeader('event');
-
-    this._ua = ua;
-    this._initial_subscribe = subscribe;
-    this._expires_timestamp = null;
-    this._expires_timer = null;
-    this._defaultExpires = defaultExpires || C.DEFAULT_EXPIRES_SEC;
-
-    // Notifier state: pending, active, terminated.
-    this._state = pending ? C.STATE_PENDING : C.STATE_ACTIVE;
-
-    this._content_type = contentType;
-    this._headers = Utils.cloneArray(extraHeaders);
-    this._headers.push(`Event: ${eventName}`);
-
-    // Use contact from extraHeaders or create it.
-    this._contact = this._headers.find((header) => header.startsWith('Contact'));
-    if (!this._contact)
-    {
-      this._contact = `Contact: ${this._ua._contact.toString()}`;
-
-      this._headers.push(this._contact);
-    }
-
-    if (allowEvents)
-    {
-      this._headers.push(`Allow-Events: ${allowEvents}`);
-    }
-
-    this._target = subscribe.from.uri.user;
-    subscribe.to_tag = Utils.newTag();
-
-    // Custom session empty object for high level use.
-    this._data = {};
-  }
-
-  // Expose Notifier constants as a property of the Notifier instance.
-  get C()
-  {
-    return C;
-  }
-
-  /**
-   * Get dialog state.
-   */
-  get state()
-  {
-    return this._state;
-  }
-
-  /**
-   * Get dialog id.
-   */
-  get id()
-  {
-    return this._dialog ? this._dialog.id : null;
-  }
-
-  get data()
-  {
-    return this._data;
-  }
-
-  set data(_data)
-  {
-    this._data = _data;
-  }
-
-  /**
-   * Dialog callback.
-   * Called also for initial subscribe.
-   * Supported RFC 6665 4.4.3: initial fetch subscribe (with expires: 0).
-   */
-  receiveRequest(request)
-  {
-    if (request.method !== JsSIP_C.SUBSCRIBE)
-    {
-      request.reply(405);
-
-      return;
-    }
-
-    this._setExpires(request);
-
-    // Create dialog for normal and fetch-subscribe.
-    if (!this._dialog)
-    {
-      this._dialog = new Dialog(this, request, 'UAS');
-    }
-
-    request.reply(200, null, [ `Expires: ${this._expires}`, `${this._contact}` ]);
-
-    const body = request.body;
-    const content_type = request.getHeader('content-type');
-    const is_unsubscribe = this._expires === 0;
-
-    if (!is_unsubscribe)
-    {
-      this._setExpiresTimer();
-    }
-
-    logger.debug('emit "subscribe"');
-
-    this.emit('subscribe', is_unsubscribe, request, body, content_type);
-
-    if (is_unsubscribe)
-    {
-      this._terminateDialog(C.UNSUBSCRIBE_RECEIVED);
-    }
-  }
-
-  /**
-   * User API
-   */
-
-  /**
-   * Call this method after creating the Notifier instance and setting the event handlers.
-   */
-  start()
-  {
-    logger.debug('start()');
-
-    if (this._state === C.STATE_TERMINATED)
-    {
-      throw new Exceptions.InvalidStateError(this._state);
-    }
-
-    this.receiveRequest(this._initial_subscribe);
-  }
-
-  /**
-   * Switch pending dialog state to active.
-   */
-  setActiveState()
-  {
-    logger.debug('setActiveState()');
-
-    if (this._state === C.STATE_TERMINATED)
-    {
-      throw new Exceptions.InvalidStateError(this._state);
-    }
-
-    if (this._state === C.STATE_PENDING)
-    {
-      this._state = C.STATE_ACTIVE;
-    }
-  }
-
-  /**
-   *  Send the initial and subsequent notify request.
-   *  @param {string} body - notify request body.
-   */
-  notify(body=null)
-  {
-    logger.debug('notify()');
-
-    if (this._state === C.STATE_TERMINATED)
-    {
-      throw new Exceptions.InvalidStateError(this._state);
-    }
-
-    const expires = Math.floor((this._expires_timestamp - new Date().getTime()) / 1000);
-
-    // expires_timer is about to trigger. Clean up the timer and terminate.
-    if (expires <= 0)
-    {
-      if (!this._expires_timer)
-      {
-        logger.error('expires timer is not set');
-      }
-
-      clearTimeout(this._expires_timer);
-
-      this.terminate(body, 'timeout');
-    }
-    else
-    {
-      this._sendNotify([ `;expires=${expires}` ], body);
-    }
-  }
-
-  /**
-   *  Terminate. (Send the final NOTIFY request).
-   *
-   * @param {string} body - Notify message body.
-   * @param {string} reason - Set Subscription-State reason parameter.
-   * @param {number} retryAfter - Set Subscription-State retry-after parameter.
-   */
-  terminate(body = null, reason = null, retryAfter = null)
-  {
-    logger.debug('terminate()');
-
-    if (this._state === C.STATE_TERMINATED)
-    {
-      return;
-    }
-
-    const subsStateParameters = [];
-
-    if (reason)
-    {
-      subsStateParameters.push(`;reason=${reason}`);
-    }
-
-    if (retryAfter !== null)
-    {
-      subsStateParameters.push(`;retry-after=${retryAfter}`);
-    }
-
-    this._sendNotify(subsStateParameters, body, null, 'terminated');
-
-    this._terminateDialog(reason === 'timeout' ? C.SUBSCRIPTION_EXPIRED : C.FINAL_NOTIFY_SENT);
-  }
-
-  /**
-   * Private API
-   */
-
-  _terminateDialog(termination_code)
-  {
-    if (this._state === C.STATE_TERMINATED)
-    {
-      return;
-    }
-    this._state = C.STATE_TERMINATED;
-
-    clearTimeout(this._expires_timer);
-    if (this._dialog)
-    {
-      this._dialog.terminate();
-      this._dialog = null;
-    }
-    logger.debug(`emit "terminated" code=${termination_code}`);
-
-    this.emit('terminated', termination_code);
-  }
-
-  _setExpires(request)
-  {
-    if (request.hasHeader('expires'))
-    {
-      this._expires = parseInt(request.getHeader('expires'));
-    }
-    else
-    {
-      this._expires = this._defaultExpires;
-      logger.debug(`missing Expires header field, default value set: ${this._expires}`);
-    }
-  }
-
-  /**
-   * @param {Array<string>} subsStateParams subscription state parameters.
-   * @param {String} body Notify body
-   * @param {Array<string>} extraHeaders
-   */
-  _sendNotify(subsStateParameters, body=null, extraHeaders=null, state=null)
-  {
-    // Prevent send notify after final notify.
-    if (this._state === C.STATE_TERMINATED)
-    {
-      logger.warn('final notify already sent');
-
-      return;
-    }
-
-    // Build Subscription-State header with parameters.
-    let subsState = `Subscription-State: ${state || this._parseState()}`;
-
-    for (const param of subsStateParameters)
-    {
-      subsState += param;
-    }
-
-    let headers = Utils.cloneArray(this._headers);
-
-    headers.push(subsState);
-
-    if (extraHeaders)
-    {
-      headers = headers.concat(extraHeaders);
-    }
-
-    if (body)
-    {
-      headers.push(`Content-Type: ${this._content_type}`);
-    }
-
-    this._dialog.sendRequest(JsSIP_C.NOTIFY, {
-      body,
-      extraHeaders  : headers,
-      eventHandlers : {
-        onRequestTimeout : () =>
-        {
-          this._terminateDialog(C.NOTIFY_RESPONSE_TIMEOUT);
-        },
-        onTransportError : () =>
-        {
-          this._terminateDialog(C.NOTIFY_TRANSPORT_ERROR);
-        },
-        onErrorResponse : (response) =>
-        {
-          if (response.status_code === 401 || response.status_code === 407)
-          {
-            this._terminateDialog(C.NOTIFY_AUTHENTICATION_FAILED);
-          }
-          else
-          {
-            this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
-          }
-        },
-        onDialogError : () =>
-        {
-          this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
-        }
-      }
-    });
-  }
-
-  _setExpiresTimer()
-  {
-    this._expires_timestamp = new Date().getTime() + (this._expires * 1000);
-
-    clearTimeout(this._expires_timer);
-    this._expires_timer = setTimeout(() =>
-    {
-      if (this._state === C.STATE_TERMINATED)
-      {
-        return;
-      }
-
-      logger.debug('emit "expired"');
-
-      // Client can hook into the 'expired' event and call terminate to send a custom notify.
-      this.emit('expired');
-
-      // This will be no-op if the client already called `terminate()`.
-      this.terminate(null, 'timeout');
-    }, this._expires * 1000);
-  }
-
-  _parseState()
-  {
-    switch (this._state)
-    {
-      case C.STATE_PENDING: return 'pending';
-      case C.STATE_ACTIVE: return 'active';
-      case C.STATE_TERMINATED: return 'terminated';
-      default: throw new TypeError('wrong state value');
-    }
-  }
+module.exports = class Notifier extends EventEmitter {
+	/**
+	 * Expose C object.
+	 */
+	static get C() {
+		return C;
+	}
+
+	static init_incoming(request, callback) {
+		try {
+			Notifier.checkSubscribe(request);
+		} catch (error) {
+			logger.warn(
+				'Notifier.init_incoming: invalid request. Error: ',
+				error.message
+			);
+
+			request.reply(405);
+
+			return;
+		}
+
+		callback();
+	}
+
+	static checkSubscribe(subscribe) {
+		if (!subscribe) {
+			throw new TypeError('Not enough arguments. Missing subscribe request');
+		}
+		if (subscribe.method !== JsSIP_C.SUBSCRIBE) {
+			throw new TypeError('Invalid method for Subscribe request');
+		}
+		if (!subscribe.hasHeader('contact')) {
+			throw new TypeError('Missing Contact header in subscribe request');
+		}
+		if (!subscribe.hasHeader('event')) {
+			throw new TypeError('Missing Event header in subscribe request');
+		}
+		const expires = subscribe.getHeader('expires');
+
+		if (expires) {
+			const parsed_expires = parseInt(expires);
+
+			if (!Utils.isDecimal(parsed_expires) || parsed_expires < 0) {
+				throw new TypeError(
+					'Invalid Expires header field in subscribe request'
+				);
+			}
+		}
+	}
+
+	/**
+	 * @param {UA} ua - JsSIP User Agent instance.
+	 * @param {IncomingRequest} subscribe - Subscribe request.
+	 * @param {string} contentType - Content-Type header value.
+	 * @param {NotifierOptions} options - Optional parameters.
+	 *   @param {Array<string>}  extraHeaders - Additional SIP headers.
+	 *   @param {string} allowEvents - Allow-Events header value.
+	 *   @param {boolean} pending - Set initial dialog state as "pending".
+	 *   @param {number} defaultExpires - Default expires value (seconds).
+	 */
+	constructor(
+		ua,
+		subscribe,
+		contentType,
+		{ extraHeaders, allowEvents, pending, defaultExpires }
+	) {
+		logger.debug('new');
+
+		super();
+
+		if (!contentType) {
+			throw new TypeError('Not enough arguments. Missing contentType');
+		}
+
+		Notifier.checkSubscribe(subscribe);
+
+		const eventName = subscribe.getHeader('event');
+
+		this._ua = ua;
+		this._initial_subscribe = subscribe;
+		this._expires_timestamp = null;
+		this._expires_timer = null;
+		this._defaultExpires = defaultExpires || C.DEFAULT_EXPIRES_SEC;
+
+		// Notifier state: pending, active, terminated.
+		this._state = pending ? C.STATE_PENDING : C.STATE_ACTIVE;
+
+		this._content_type = contentType;
+		this._headers = Utils.cloneArray(extraHeaders);
+		this._headers.push(`Event: ${eventName}`);
+
+		// Use contact from extraHeaders or create it.
+		this._contact = this._headers.find(header => header.startsWith('Contact'));
+		if (!this._contact) {
+			this._contact = `Contact: ${this._ua._contact.toString()}`;
+
+			this._headers.push(this._contact);
+		}
+
+		if (allowEvents) {
+			this._headers.push(`Allow-Events: ${allowEvents}`);
+		}
+
+		this._target = subscribe.from.uri.user;
+		subscribe.to_tag = Utils.newTag();
+
+		// Custom session empty object for high level use.
+		this._data = {};
+	}
+
+	// Expose Notifier constants as a property of the Notifier instance.
+	get C() {
+		return C;
+	}
+
+	/**
+	 * Get dialog state.
+	 */
+	get state() {
+		return this._state;
+	}
+
+	/**
+	 * Get dialog id.
+	 */
+	get id() {
+		return this._dialog ? this._dialog.id : null;
+	}
+
+	get data() {
+		return this._data;
+	}
+
+	set data(_data) {
+		this._data = _data;
+	}
+
+	/**
+	 * Dialog callback.
+	 * Called also for initial subscribe.
+	 * Supported RFC 6665 4.4.3: initial fetch subscribe (with expires: 0).
+	 */
+	receiveRequest(request) {
+		if (request.method !== JsSIP_C.SUBSCRIBE) {
+			request.reply(405);
+
+			return;
+		}
+
+		this._setExpires(request);
+
+		// Create dialog for normal and fetch-subscribe.
+		if (!this._dialog) {
+			this._dialog = new Dialog(this, request, 'UAS');
+		}
+
+		request.reply(200, null, [`Expires: ${this._expires}`, `${this._contact}`]);
+
+		const body = request.body;
+		const content_type = request.getHeader('content-type');
+		const is_unsubscribe = this._expires === 0;
+
+		if (!is_unsubscribe) {
+			this._setExpiresTimer();
+		}
+
+		logger.debug('emit "subscribe"');
+
+		this.emit('subscribe', is_unsubscribe, request, body, content_type);
+
+		if (is_unsubscribe) {
+			this._terminateDialog(C.UNSUBSCRIBE_RECEIVED);
+		}
+	}
+
+	/**
+	 * User API
+	 */
+
+	/**
+	 * Call this method after creating the Notifier instance and setting the event handlers.
+	 */
+	start() {
+		logger.debug('start()');
+
+		if (this._state === C.STATE_TERMINATED) {
+			throw new Exceptions.InvalidStateError(this._state);
+		}
+
+		this.receiveRequest(this._initial_subscribe);
+	}
+
+	/**
+	 * Switch pending dialog state to active.
+	 */
+	setActiveState() {
+		logger.debug('setActiveState()');
+
+		if (this._state === C.STATE_TERMINATED) {
+			throw new Exceptions.InvalidStateError(this._state);
+		}
+
+		if (this._state === C.STATE_PENDING) {
+			this._state = C.STATE_ACTIVE;
+		}
+	}
+
+	/**
+	 *  Send the initial and subsequent notify request.
+	 *  @param {string} body - notify request body.
+	 */
+	notify(body = null) {
+		logger.debug('notify()');
+
+		if (this._state === C.STATE_TERMINATED) {
+			throw new Exceptions.InvalidStateError(this._state);
+		}
+
+		const expires = Math.floor(
+			(this._expires_timestamp - new Date().getTime()) / 1000
+		);
+
+		// expires_timer is about to trigger. Clean up the timer and terminate.
+		if (expires <= 0) {
+			if (!this._expires_timer) {
+				logger.error('expires timer is not set');
+			}
+
+			clearTimeout(this._expires_timer);
+
+			this.terminate(body, 'timeout');
+		} else {
+			this._sendNotify([`;expires=${expires}`], body);
+		}
+	}
+
+	/**
+	 *  Terminate. (Send the final NOTIFY request).
+	 *
+	 * @param {string} body - Notify message body.
+	 * @param {string} reason - Set Subscription-State reason parameter.
+	 * @param {number} retryAfter - Set Subscription-State retry-after parameter.
+	 */
+	terminate(body = null, reason = null, retryAfter = null) {
+		logger.debug('terminate()');
+
+		if (this._state === C.STATE_TERMINATED) {
+			return;
+		}
+
+		const subsStateParameters = [];
+
+		if (reason) {
+			subsStateParameters.push(`;reason=${reason}`);
+		}
+
+		if (retryAfter !== null) {
+			subsStateParameters.push(`;retry-after=${retryAfter}`);
+		}
+
+		this._sendNotify(subsStateParameters, body, null, 'terminated');
+
+		this._terminateDialog(
+			reason === 'timeout' ? C.SUBSCRIPTION_EXPIRED : C.FINAL_NOTIFY_SENT
+		);
+	}
+
+	/**
+	 * Private API
+	 */
+
+	_terminateDialog(termination_code) {
+		if (this._state === C.STATE_TERMINATED) {
+			return;
+		}
+		this._state = C.STATE_TERMINATED;
+
+		clearTimeout(this._expires_timer);
+		if (this._dialog) {
+			this._dialog.terminate();
+			this._dialog = null;
+		}
+		logger.debug(`emit "terminated" code=${termination_code}`);
+
+		this.emit('terminated', termination_code);
+	}
+
+	_setExpires(request) {
+		if (request.hasHeader('expires')) {
+			this._expires = parseInt(request.getHeader('expires'));
+		} else {
+			this._expires = this._defaultExpires;
+			logger.debug(
+				`missing Expires header field, default value set: ${this._expires}`
+			);
+		}
+	}
+
+	/**
+	 * @param {Array<string>} subsStateParams subscription state parameters.
+	 * @param {String} body Notify body
+	 * @param {Array<string>} extraHeaders
+	 */
+	_sendNotify(
+		subsStateParameters,
+		body = null,
+		extraHeaders = null,
+		state = null
+	) {
+		// Prevent send notify after final notify.
+		if (this._state === C.STATE_TERMINATED) {
+			logger.warn('final notify already sent');
+
+			return;
+		}
+
+		// Build Subscription-State header with parameters.
+		let subsState = `Subscription-State: ${state || this._parseState()}`;
+
+		for (const param of subsStateParameters) {
+			subsState += param;
+		}
+
+		let headers = Utils.cloneArray(this._headers);
+
+		headers.push(subsState);
+
+		if (extraHeaders) {
+			headers = headers.concat(extraHeaders);
+		}
+
+		if (body) {
+			headers.push(`Content-Type: ${this._content_type}`);
+		}
+
+		this._dialog.sendRequest(JsSIP_C.NOTIFY, {
+			body,
+			extraHeaders: headers,
+			eventHandlers: {
+				onRequestTimeout: () => {
+					this._terminateDialog(C.NOTIFY_RESPONSE_TIMEOUT);
+				},
+				onTransportError: () => {
+					this._terminateDialog(C.NOTIFY_TRANSPORT_ERROR);
+				},
+				onErrorResponse: response => {
+					if (response.status_code === 401 || response.status_code === 407) {
+						this._terminateDialog(C.NOTIFY_AUTHENTICATION_FAILED);
+					} else {
+						this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
+					}
+				},
+				onDialogError: () => {
+					this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
+				},
+			},
+		});
+	}
+
+	_setExpiresTimer() {
+		this._expires_timestamp = new Date().getTime() + this._expires * 1000;
+
+		clearTimeout(this._expires_timer);
+		this._expires_timer = setTimeout(() => {
+			if (this._state === C.STATE_TERMINATED) {
+				return;
+			}
+
+			logger.debug('emit "expired"');
+
+			// Client can hook into the 'expired' event and call terminate to send a custom notify.
+			this.emit('expired');
+
+			// This will be no-op if the client already called `terminate()`.
+			this.terminate(null, 'timeout');
+		}, this._expires * 1000);
+	}
+
+	_parseState() {
+		switch (this._state) {
+			case C.STATE_PENDING: {
+				return 'pending';
+			}
+			case C.STATE_ACTIVE: {
+				return 'active';
+			}
+			case C.STATE_TERMINATED: {
+				return 'terminated';
+			}
+			default: {
+				throw new TypeError('wrong state value');
+			}
+		}
+	}
 };
diff --git a/src/Options.js b/src/Options.js
index 86b2888..f01c35d 100644
--- a/src/Options.js
+++ b/src/Options.js
@@ -8,273 +8,246 @@ const Exceptions = require('./Exceptions');

 const logger = new Logger('Options');

-module.exports = class Options extends EventEmitter
-{
-  constructor(ua)
-  {
-    super();
-
-    this._ua = ua;
-    this._request = null;
-    this._closed = false;
-
-    this._direction = null;
-    this._local_identity = null;
-    this._remote_identity = null;
-
-    // Whether an incoming message has been replied.
-    this._is_replied = false;
-
-    // Custom message empty object for high level use.
-    this._data = {};
-  }
-
-  get direction()
-  {
-    return this._direction;
-  }
-
-  get local_identity()
-  {
-    return this._local_identity;
-  }
-
-  get remote_identity()
-  {
-    return this._remote_identity;
-  }
-
-  send(target, body, options = {})
-  {
-    const originalTarget = target;
-
-    if (target === undefined)
-    {
-      throw new TypeError('A target is required for OPTIONS');
-    }
-
-    // Check target validity.
-    target = this._ua.normalizeTarget(target);
-    if (!target)
-    {
-      throw new TypeError(`Invalid target: ${originalTarget}`);
-    }
-
-    // Get call options.
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const eventHandlers = Utils.cloneObject(options.eventHandlers);
-    const contentType = options.contentType || 'application/sdp';
-
-    // Set event handlers.
-    for (const event in eventHandlers)
-    {
-      if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
-      {
-        this.on(event, eventHandlers[event]);
-      }
-    }
-
-    extraHeaders.push(`Content-Type: ${contentType}`);
-
-    this._request = new SIPMessage.OutgoingRequest(
-      JsSIP_C.OPTIONS, target, this._ua, null, extraHeaders);
-
-    if (body)
-    {
-      this._request.body = body;
-    }
-
-    const request_sender = new RequestSender(this._ua, this._request, {
-      onRequestTimeout : () =>
-      {
-        this._onRequestTimeout();
-      },
-      onTransportError : () =>
-      {
-        this._onTransportError();
-      },
-      onReceiveResponse : (response) =>
-      {
-        this._receiveResponse(response);
-      }
-    });
-
-    this._newOptions('local', this._request);
-
-    request_sender.send();
-  }
-
-  init_incoming(request)
-  {
-    this._request = request;
-
-    this._newOptions('remote', request);
-
-    // Reply with a 200 OK if the user didn't reply.
-    if (!this._is_replied)
-    {
-      this._is_replied = true;
-      request.reply(200);
-    }
-
-    this._close();
-  }
-
-  /**
-   * Accept the incoming Options
-   * Only valid for incoming Options
-   */
-  accept(options = {})
-  {
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const body = options.body;
-
-    if (this._direction !== 'incoming')
-    {
-      throw new Exceptions.NotSupportedError('"accept" not supported for outgoing Options');
-    }
-
-    if (this._is_replied)
-    {
-      throw new Error('incoming Options already replied');
-    }
-
-    this._is_replied = true;
-    this._request.reply(200, null, extraHeaders, body);
-  }
-
-  /**
-   * Reject the incoming Options
-   * Only valid for incoming Options
-   */
-  reject(options = {})
-  {
-    const status_code = options.status_code || 480;
-    const reason_phrase = options.reason_phrase;
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const body = options.body;
-
-    if (this._direction !== 'incoming')
-    {
-      throw new Exceptions.NotSupportedError('"reject" not supported for outgoing Options');
-    }
-
-    if (this._is_replied)
-    {
-      throw new Error('incoming Options already replied');
-    }
-
-    if (status_code < 300 || status_code >= 700)
-    {
-      throw new TypeError(`Invalid status_code: ${status_code}`);
-    }
-
-    this._is_replied = true;
-    this._request.reply(status_code, reason_phrase, extraHeaders, body);
-  }
-
-  _receiveResponse(response)
-  {
-    if (this._closed)
-    {
-      return;
-    }
-    switch (true)
-    {
-      case /^1[0-9]{2}$/.test(response.status_code):
-        // Ignore provisional responses.
-        break;
-
-      case /^2[0-9]{2}$/.test(response.status_code):
-        this._succeeded('remote', response);
-        break;
-
-      default:
-      {
-        const cause = Utils.sipErrorCause(response.status_code);
-
-        this._failed('remote', response, cause);
-        break;
-      }
-    }
-  }
-
-  _onRequestTimeout()
-  {
-    if (this._closed)
-    {
-      return;
-    }
-    this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
-  }
-
-  _onTransportError()
-  {
-    if (this._closed)
-    {
-      return;
-    }
-    this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
-  }
-
-  _close()
-  {
-    this._closed = true;
-    this._ua.destroyMessage(this);
-  }
-
-  /**
-   * Internal Callbacks
-   */
-
-  _newOptions(originator, request)
-  {
-    if (originator === 'remote')
-    {
-      this._direction = 'incoming';
-      this._local_identity = request.to;
-      this._remote_identity = request.from;
-    }
-    else if (originator === 'local')
-    {
-      this._direction = 'outgoing';
-      this._local_identity = request.from;
-      this._remote_identity = request.to;
-    }
-
-    this._ua.newOptions(this, {
-      originator,
-      message : this,
-      request
-    });
-  }
-
-  _failed(originator, response, cause)
-  {
-    logger.debug('OPTIONS failed');
-
-    this._close();
-
-    logger.debug('emit "failed"');
-
-    this.emit('failed', {
-      originator,
-      response : response || null,
-      cause
-    });
-  }
-
-  _succeeded(originator, response)
-  {
-    logger.debug('OPTIONS succeeded');
-
-    this._close();
-
-    logger.debug('emit "succeeded"');
-
-    this.emit('succeeded', {
-      originator,
-      response
-    });
-  }
+module.exports = class Options extends EventEmitter {
+	constructor(ua) {
+		super();
+
+		this._ua = ua;
+		this._request = null;
+		this._closed = false;
+
+		this._direction = null;
+		this._local_identity = null;
+		this._remote_identity = null;
+
+		// Whether an incoming message has been replied.
+		this._is_replied = false;
+
+		// Custom message empty object for high level use.
+		this._data = {};
+	}
+
+	get direction() {
+		return this._direction;
+	}
+
+	get local_identity() {
+		return this._local_identity;
+	}
+
+	get remote_identity() {
+		return this._remote_identity;
+	}
+
+	send(target, body, options = {}) {
+		const originalTarget = target;
+
+		if (target === undefined) {
+			throw new TypeError('A target is required for OPTIONS');
+		}
+
+		// Check target validity.
+		target = this._ua.normalizeTarget(target);
+		if (!target) {
+			throw new TypeError(`Invalid target: ${originalTarget}`);
+		}
+
+		// Get call options.
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const eventHandlers = Utils.cloneObject(options.eventHandlers);
+		const contentType = options.contentType || 'application/sdp';
+
+		// Set event handlers.
+		for (const event in eventHandlers) {
+			if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+				this.on(event, eventHandlers[event]);
+			}
+		}
+
+		extraHeaders.push(`Content-Type: ${contentType}`);
+
+		this._request = new SIPMessage.OutgoingRequest(
+			JsSIP_C.OPTIONS,
+			target,
+			this._ua,
+			null,
+			extraHeaders
+		);
+
+		if (body) {
+			this._request.body = body;
+		}
+
+		const request_sender = new RequestSender(this._ua, this._request, {
+			onRequestTimeout: () => {
+				this._onRequestTimeout();
+			},
+			onTransportError: () => {
+				this._onTransportError();
+			},
+			onReceiveResponse: response => {
+				this._receiveResponse(response);
+			},
+		});
+
+		this._newOptions('local', this._request);
+
+		request_sender.send();
+	}
+
+	init_incoming(request) {
+		this._request = request;
+
+		this._newOptions('remote', request);
+
+		// Reply with a 200 OK if the user didn't reply.
+		if (!this._is_replied) {
+			this._is_replied = true;
+			request.reply(200);
+		}
+
+		this._close();
+	}
+
+	/**
+	 * Accept the incoming Options
+	 * Only valid for incoming Options
+	 */
+	accept(options = {}) {
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const body = options.body;
+
+		if (this._direction !== 'incoming') {
+			throw new Exceptions.NotSupportedError(
+				'"accept" not supported for outgoing Options'
+			);
+		}
+
+		if (this._is_replied) {
+			throw new Error('incoming Options already replied');
+		}
+
+		this._is_replied = true;
+		this._request.reply(200, null, extraHeaders, body);
+	}
+
+	/**
+	 * Reject the incoming Options
+	 * Only valid for incoming Options
+	 */
+	reject(options = {}) {
+		const status_code = options.status_code || 480;
+		const reason_phrase = options.reason_phrase;
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const body = options.body;
+
+		if (this._direction !== 'incoming') {
+			throw new Exceptions.NotSupportedError(
+				'"reject" not supported for outgoing Options'
+			);
+		}
+
+		if (this._is_replied) {
+			throw new Error('incoming Options already replied');
+		}
+
+		if (status_code < 300 || status_code >= 700) {
+			throw new TypeError(`Invalid status_code: ${status_code}`);
+		}
+
+		this._is_replied = true;
+		this._request.reply(status_code, reason_phrase, extraHeaders, body);
+	}
+
+	_receiveResponse(response) {
+		if (this._closed) {
+			return;
+		}
+		switch (true) {
+			case /^1[0-9]{2}$/.test(response.status_code): {
+				// Ignore provisional responses.
+				break;
+			}
+
+			case /^2[0-9]{2}$/.test(response.status_code): {
+				this._succeeded('remote', response);
+				break;
+			}
+
+			default: {
+				const cause = Utils.sipErrorCause(response.status_code);
+
+				this._failed('remote', response, cause);
+				break;
+			}
+		}
+	}
+
+	_onRequestTimeout() {
+		if (this._closed) {
+			return;
+		}
+		this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
+	}
+
+	_onTransportError() {
+		if (this._closed) {
+			return;
+		}
+		this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
+	}
+
+	_close() {
+		this._closed = true;
+		this._ua.destroyMessage(this);
+	}
+
+	/**
+	 * Internal Callbacks
+	 */
+
+	_newOptions(originator, request) {
+		if (originator === 'remote') {
+			this._direction = 'incoming';
+			this._local_identity = request.to;
+			this._remote_identity = request.from;
+		} else if (originator === 'local') {
+			this._direction = 'outgoing';
+			this._local_identity = request.from;
+			this._remote_identity = request.to;
+		}
+
+		this._ua.newOptions(this, {
+			originator,
+			message: this,
+			request,
+		});
+	}
+
+	_failed(originator, response, cause) {
+		logger.debug('OPTIONS failed');
+
+		this._close();
+
+		logger.debug('emit "failed"');
+
+		this.emit('failed', {
+			originator,
+			response: response || null,
+			cause,
+		});
+	}
+
+	_succeeded(originator, response) {
+		logger.debug('OPTIONS succeeded');
+
+		this._close();
+
+		logger.debug('emit "succeeded"');
+
+		this.emit('succeeded', {
+			originator,
+			response,
+		});
+	}
 };
diff --git a/src/Parser.js b/src/Parser.js
index e7a5090..79013d9 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -7,312 +7,297 @@ const logger = new Logger('Parser');
 /**
  * Parse SIP Message
  */
-exports.parseMessage = (data, ua) =>
-{
-  let message;
-  let bodyStart;
-  let headerEnd = data.indexOf('\r\n');
+exports.parseMessage = (data, ua) => {
+	let message;
+	let bodyStart;
+	let headerEnd = data.indexOf('\r\n');

-  if (headerEnd === -1)
-  {
-    logger.warn('parseMessage() | no CRLF found, not a SIP message');
+	if (headerEnd === -1) {
+		logger.warn('parseMessage() | no CRLF found, not a SIP message');

-    return;
-  }
+		return;
+	}

-  // Parse first line. Check if it is a Request or a Reply.
-  const firstLine = data.substring(0, headerEnd);
-  let parsed = Grammar.parse(firstLine, 'Request_Response');
+	// Parse first line. Check if it is a Request or a Reply.
+	const firstLine = data.substring(0, headerEnd);
+	let parsed = Grammar.parse(firstLine, 'Request_Response');

-  if (parsed === -1)
-  {
-    logger.warn(`parseMessage() | error parsing first line of SIP message: "${firstLine}"`);
+	if (parsed === -1) {
+		logger.warn(
+			`parseMessage() | error parsing first line of SIP message: "${firstLine}"`
+		);

-    return;
-  }
-  else if (!parsed.status_code)
-  {
-    message = new SIPMessage.IncomingRequest(ua);
-    message.method = parsed.method;
-    message.ruri = parsed.uri;
-  }
-  else
-  {
-    message = new SIPMessage.IncomingResponse();
-    message.status_code = parsed.status_code;
-    message.reason_phrase = parsed.reason_phrase;
-  }
+		return;
+	} else if (!parsed.status_code) {
+		message = new SIPMessage.IncomingRequest(ua);
+		message.method = parsed.method;
+		message.ruri = parsed.uri;
+	} else {
+		message = new SIPMessage.IncomingResponse();
+		message.status_code = parsed.status_code;
+		message.reason_phrase = parsed.reason_phrase;
+	}

-  message.data = data;
-  let headerStart = headerEnd + 2;
+	message.data = data;
+	let headerStart = headerEnd + 2;

-  /* Loop over every line in data. Detect the end of each header and parse
-  * it or simply add to the headers collection.
-  */
-  while (true)
-  {
-    headerEnd = getHeader(data, headerStart);
+	/* Loop over every line in data. Detect the end of each header and parse
+	 * it or simply add to the headers collection.
+	 */
+	while (true) {
+		headerEnd = getHeader(data, headerStart);

-    // The SIP message has normally finished.
-    if (headerEnd === -2)
-    {
-      bodyStart = headerStart + 2;
-      break;
-    }
-    // Data.indexOf returned -1 due to a malformed message.
-    else if (headerEnd === -1)
-    {
-      logger.warn('parseMessage() | malformed message');
+		// The SIP message has normally finished.
+		if (headerEnd === -2) {
+			bodyStart = headerStart + 2;
+			break;
+		}
+		// Data.indexOf returned -1 due to a malformed message.
+		else if (headerEnd === -1) {
+			logger.warn('parseMessage() | malformed message');

-      return;
-    }
+			return;
+		}

-    parsed = parseHeader(message, data, headerStart, headerEnd);
+		parsed = parseHeader(message, data, headerStart, headerEnd);

-    if (parsed !== true)
-    {
-      logger.warn('parseMessage() |', parsed.error);
+		if (parsed !== true) {
+			logger.warn('parseMessage() |', parsed.error);

-      return;
-    }
+			return;
+		}

-    headerStart = headerEnd + 2;
-  }
+		headerStart = headerEnd + 2;
+	}

-  /* RFC3261 18.3.
-   * If there are additional bytes in the transport packet
-   * beyond the end of the body, they MUST be discarded.
-   */
-  if (message.hasHeader('content-length'))
-  {
-    const contentLength = message.getHeader('content-length');
+	/* RFC3261 18.3.
+	 * If there are additional bytes in the transport packet
+	 * beyond the end of the body, they MUST be discarded.
+	 */
+	if (message.hasHeader('content-length')) {
+		const contentLength = message.getHeader('content-length');

-    message.body = data.substr(bodyStart, contentLength);
-  }
-  else
-  {
-    message.body = data.substring(bodyStart);
-  }
+		message.body = data.substr(bodyStart, contentLength);
+	} else {
+		message.body = data.substring(bodyStart);
+	}

-  return message;
+	return message;
 };

 /**
  * Extract and parse every header of a SIP message.
  */
-function getHeader(data, headerStart)
-{
-  // 'start' position of the header.
-  let start = headerStart;
-  // 'end' position of the header.
-  let end = 0;
-  // 'partial end' position of the header.
-  let partialEnd = 0;
+function getHeader(data, headerStart) {
+	// 'start' position of the header.
+	let start = headerStart;
+	// 'end' position of the header.
+	let end = 0;
+	// 'partial end' position of the header.
+	let partialEnd = 0;

-  // End of message.
-  if (data.substring(start, start + 2).match(/(^\r\n)/))
-  {
-    return -2;
-  }
+	// End of message.
+	if (data.substring(start, start + 2).match(/(^\r\n)/)) {
+		return -2;
+	}

-  while (end === 0)
-  {
-    // Partial End of Header.
-    partialEnd = data.indexOf('\r\n', start);
+	while (end === 0) {
+		// Partial End of Header.
+		partialEnd = data.indexOf('\r\n', start);

-    // 'indexOf' returns -1 if the value to be found never occurs.
-    if (partialEnd === -1)
-    {
-      return partialEnd;
-    }
+		// 'indexOf' returns -1 if the value to be found never occurs.
+		if (partialEnd === -1) {
+			return partialEnd;
+		}

-    if (!data.substring(partialEnd + 2, partialEnd + 4).match(/(^\r\n)/) && data.charAt(partialEnd + 2).match(/(^\s+)/))
-    {
-      // Not the end of the message. Continue from the next position.
-      start = partialEnd + 2;
-    }
-    else
-    {
-      end = partialEnd;
-    }
-  }
+		if (
+			!data.substring(partialEnd + 2, partialEnd + 4).match(/(^\r\n)/) &&
+			data.charAt(partialEnd + 2).match(/(^\s+)/)
+		) {
+			// Not the end of the message. Continue from the next position.
+			start = partialEnd + 2;
+		} else {
+			end = partialEnd;
+		}
+	}

-  return end;
+	return end;
 }

-function parseHeader(message, data, headerStart, headerEnd)
-{
-  let parsed;
-  const hcolonIndex = data.indexOf(':', headerStart);
-  const headerName = data.substring(headerStart, hcolonIndex).trim();
-  const headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();
+function parseHeader(message, data, headerStart, headerEnd) {
+	let parsed;
+	const hcolonIndex = data.indexOf(':', headerStart);
+	const headerName = data.substring(headerStart, hcolonIndex).trim();
+	const headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();

-  // If header-field is well-known, parse it.
-  switch (headerName.toLowerCase())
-  {
-    case 'via':
-    case 'v':
-      message.addHeader('via', headerValue);
-      if (message.getHeaders('via').length === 1)
-      {
-        parsed = message.parseHeader('Via');
-        if (parsed)
-        {
-          message.via = parsed;
-          message.via_branch = parsed.branch;
-        }
-      }
-      else
-      {
-        parsed = 0;
-      }
-      break;
-    case 'from':
-    case 'f':
-      message.setHeader('from', headerValue);
-      parsed = message.parseHeader('from');
-      if (parsed)
-      {
-        message.from = parsed;
-        message.from_tag = parsed.getParam('tag');
-      }
-      break;
-    case 'to':
-    case 't':
-      message.setHeader('to', headerValue);
-      parsed = message.parseHeader('to');
-      if (parsed)
-      {
-        message.to = parsed;
-        message.to_tag = parsed.getParam('tag');
-      }
-      break;
-    case 'record-route':
-      parsed = Grammar.parse(headerValue, 'Record_Route');
+	// If header-field is well-known, parse it.
+	switch (headerName.toLowerCase()) {
+		case 'via':
+		case 'v': {
+			message.addHeader('via', headerValue);
+			if (message.getHeaders('via').length === 1) {
+				parsed = message.parseHeader('Via');
+				if (parsed) {
+					message.via = parsed;
+					message.via_branch = parsed.branch;
+				}
+			} else {
+				parsed = 0;
+			}
+			break;
+		}
+		case 'from':
+		case 'f': {
+			message.setHeader('from', headerValue);
+			parsed = message.parseHeader('from');
+			if (parsed) {
+				message.from = parsed;
+				message.from_tag = parsed.getParam('tag');
+			}
+			break;
+		}
+		case 'to':
+		case 't': {
+			message.setHeader('to', headerValue);
+			parsed = message.parseHeader('to');
+			if (parsed) {
+				message.to = parsed;
+				message.to_tag = parsed.getParam('tag');
+			}
+			break;
+		}
+		case 'record-route': {
+			parsed = Grammar.parse(headerValue, 'Record_Route');

-      if (parsed === -1)
-      {
-        parsed = undefined;
-      }
-      else
-      {
-        for (const header of parsed)
-        {
-          message.addHeader('record-route', headerValue.substring(header.possition, header.offset));
-          message.headers['Record-Route'][message.getHeaders('record-route').length - 1].parsed = header.parsed;
-        }
-      }
-      break;
-    case 'call-id':
-    case 'i':
-      message.setHeader('call-id', headerValue);
-      parsed = message.parseHeader('call-id');
-      if (parsed)
-      {
-        message.call_id = headerValue;
-      }
-      break;
-    case 'contact':
-    case 'm':
-      parsed = Grammar.parse(headerValue, 'Contact');
+			if (parsed === -1) {
+				parsed = undefined;
+			} else {
+				for (const header of parsed) {
+					message.addHeader(
+						'record-route',
+						headerValue.substring(header.possition, header.offset)
+					);
+					message.headers['Record-Route'][
+						message.getHeaders('record-route').length - 1
+					].parsed = header.parsed;
+				}
+			}
+			break;
+		}
+		case 'call-id':
+		case 'i': {
+			message.setHeader('call-id', headerValue);
+			parsed = message.parseHeader('call-id');
+			if (parsed) {
+				message.call_id = headerValue;
+			}
+			break;
+		}
+		case 'contact':
+		case 'm': {
+			parsed = Grammar.parse(headerValue, 'Contact');

-      if (parsed === -1)
-      {
-        parsed = undefined;
-      }
-      else
-      {
-        for (const header of parsed)
-        {
-          message.addHeader('contact', headerValue.substring(header.possition, header.offset));
-          message.headers.Contact[message.getHeaders('contact').length - 1].parsed = header.parsed;
-        }
-      }
-      break;
-    case 'content-length':
-    case 'l':
-      message.setHeader('content-length', headerValue);
-      parsed = message.parseHeader('content-length');
-      break;
-    case 'content-type':
-    case 'c':
-      message.setHeader('content-type', headerValue);
-      parsed = message.parseHeader('content-type');
-      break;
-    case 'cseq':
-      message.setHeader('cseq', headerValue);
-      parsed = message.parseHeader('cseq');
-      if (parsed)
-      {
-        message.cseq = parsed.value;
-      }
-      if (message instanceof SIPMessage.IncomingResponse)
-      {
-        message.method = parsed.method;
-      }
-      break;
-    case 'max-forwards':
-      message.setHeader('max-forwards', headerValue);
-      parsed = message.parseHeader('max-forwards');
-      break;
-    case 'www-authenticate':
-      message.setHeader('www-authenticate', headerValue);
-      parsed = message.parseHeader('www-authenticate');
-      break;
-    case 'proxy-authenticate':
-      message.setHeader('proxy-authenticate', headerValue);
-      parsed = message.parseHeader('proxy-authenticate');
-      break;
-    case 'session-expires':
-    case 'x':
-      message.setHeader('session-expires', headerValue);
-      parsed = message.parseHeader('session-expires');
-      if (parsed)
-      {
-        message.session_expires = parsed.expires;
-        message.session_expires_refresher = parsed.refresher;
-      }
-      break;
-    case 'refer-to':
-    case 'r':
-      message.setHeader('refer-to', headerValue);
-      parsed = message.parseHeader('refer-to');
-      if (parsed)
-      {
-        message.refer_to = parsed;
-      }
-      break;
-    case 'replaces':
-      message.setHeader('replaces', headerValue);
-      parsed = message.parseHeader('replaces');
-      if (parsed)
-      {
-        message.replaces = parsed;
-      }
-      break;
-    case 'event':
-    case 'o':
-      message.setHeader('event', headerValue);
-      parsed = message.parseHeader('event');
-      if (parsed)
-      {
-        message.event = parsed;
-      }
-      break;
-    default:
-      // Do not parse this header.
-      message.addHeader(headerName, headerValue);
-      parsed = 0;
-  }
+			if (parsed === -1) {
+				parsed = undefined;
+			} else {
+				for (const header of parsed) {
+					message.addHeader(
+						'contact',
+						headerValue.substring(header.possition, header.offset)
+					);
+					message.headers.Contact[
+						message.getHeaders('contact').length - 1
+					].parsed = header.parsed;
+				}
+			}
+			break;
+		}
+		case 'content-length':
+		case 'l': {
+			message.setHeader('content-length', headerValue);
+			parsed = message.parseHeader('content-length');
+			break;
+		}
+		case 'content-type':
+		case 'c': {
+			message.setHeader('content-type', headerValue);
+			parsed = message.parseHeader('content-type');
+			break;
+		}
+		case 'cseq': {
+			message.setHeader('cseq', headerValue);
+			parsed = message.parseHeader('cseq');
+			if (parsed) {
+				message.cseq = parsed.value;
+			}
+			if (message instanceof SIPMessage.IncomingResponse) {
+				message.method = parsed.method;
+			}
+			break;
+		}
+		case 'max-forwards': {
+			message.setHeader('max-forwards', headerValue);
+			parsed = message.parseHeader('max-forwards');
+			break;
+		}
+		case 'www-authenticate': {
+			message.setHeader('www-authenticate', headerValue);
+			parsed = message.parseHeader('www-authenticate');
+			break;
+		}
+		case 'proxy-authenticate': {
+			message.setHeader('proxy-authenticate', headerValue);
+			parsed = message.parseHeader('proxy-authenticate');
+			break;
+		}
+		case 'session-expires':
+		case 'x': {
+			message.setHeader('session-expires', headerValue);
+			parsed = message.parseHeader('session-expires');
+			if (parsed) {
+				message.session_expires = parsed.expires;
+				message.session_expires_refresher = parsed.refresher;
+			}
+			break;
+		}
+		case 'refer-to':
+		case 'r': {
+			message.setHeader('refer-to', headerValue);
+			parsed = message.parseHeader('refer-to');
+			if (parsed) {
+				message.refer_to = parsed;
+			}
+			break;
+		}
+		case 'replaces': {
+			message.setHeader('replaces', headerValue);
+			parsed = message.parseHeader('replaces');
+			if (parsed) {
+				message.replaces = parsed;
+			}
+			break;
+		}
+		case 'event':
+		case 'o': {
+			message.setHeader('event', headerValue);
+			parsed = message.parseHeader('event');
+			if (parsed) {
+				message.event = parsed;
+			}
+			break;
+		}
+		default: {
+			// Do not parse this header.
+			message.addHeader(headerName, headerValue);
+			parsed = 0;
+		}
+	}

-  if (parsed === undefined)
-  {
-    return {
-      error : `error parsing header "${headerName}"`
-    };
-  }
-  else
-  {
-    return true;
-  }
+	if (parsed === undefined) {
+		return {
+			error: `error parsing header "${headerName}"`,
+		};
+	} else {
+		return true;
+	}
 }
diff --git a/src/RTCSession.d.ts b/src/RTCSession.d.ts
index fb68910..4e03d92 100644
--- a/src/RTCSession.d.ts
+++ b/src/RTCSession.d.ts
@@ -1,183 +1,190 @@
-import {EventEmitter} from 'events'
-
-import {IncomingRequest, IncomingResponse, OutgoingRequest} from './SIPMessage'
-import {NameAddrHeader} from './NameAddrHeader'
-import {URI} from './URI'
-import {UA} from './UA'
-import {causes, DTMF_TRANSPORT} from './Constants'
+import { EventEmitter } from 'events';
+
+import {
+	IncomingRequest,
+	IncomingResponse,
+	OutgoingRequest,
+} from './SIPMessage';
+import { NameAddrHeader } from './NameAddrHeader';
+import { URI } from './URI';
+import { UA } from './UA';
+import { causes, DTMF_TRANSPORT } from './Constants';

 interface RTCPeerConnectionDeprecated extends RTCPeerConnection {
-  /**
-   * @deprecated
-   * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getRemoteStreams
-   */
-  getRemoteStreams(): MediaStream[];
+	/**
+	 * @deprecated
+	 * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getRemoteStreams
+	 */
+	getRemoteStreams(): MediaStream[];
 }

 export declare enum SessionDirection {
-  INCOMING = 'incoming',
-  OUTGOING = 'outgoing',
+	INCOMING = 'incoming',
+	OUTGOING = 'outgoing',
 }

 export declare enum Originator {
-  LOCAL = 'local',
-  REMOTE = 'remote',
-  SYSTEM = 'system',
+	LOCAL = 'local',
+	REMOTE = 'remote',
+	SYSTEM = 'system',
 }

 // options
 export interface ExtraHeaders {
-  extraHeaders?: string[];
+	extraHeaders?: string[];
 }

 export interface AnswerOptions extends ExtraHeaders {
-  mediaConstraints?: MediaStreamConstraints;
-  mediaStream?: MediaStream;
-  pcConfig?: RTCConfiguration;
-  rtcConstraints?: object;
-  rtcAnswerConstraints?: RTCOfferOptions;
-  rtcOfferConstraints?: RTCOfferOptions;
-  sessionTimersExpires?: number;
+	mediaConstraints?: MediaStreamConstraints;
+	mediaStream?: MediaStream;
+	pcConfig?: RTCConfiguration;
+	rtcConstraints?: object;
+	rtcAnswerConstraints?: RTCOfferOptions;
+	rtcOfferConstraints?: RTCOfferOptions;
+	sessionTimersExpires?: number;
 }

 export interface RejectOptions extends ExtraHeaders {
-  status_code?: number;
-  reason_phrase?: string;
+	status_code?: number;
+	reason_phrase?: string;
 }

 export interface TerminateOptions extends RejectOptions {
-  body?: string;
-  cause?: causes | string;
+	body?: string;
+	cause?: causes | string;
 }

 export interface ReferOptions extends ExtraHeaders {
-  eventHandlers?: any;
-  replaces?: RTCSession;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	eventHandlers?: any;
+	replaces?: RTCSession;
 }

 export interface OnHoldResult {
-  local: boolean;
-  remote: boolean;
+	local: boolean;
+	remote: boolean;
 }

 export interface DTMFOptions extends ExtraHeaders {
-  duration?: number;
-  interToneGap?: number;
-  transportType?: DTMF_TRANSPORT;
+	duration?: number;
+	interToneGap?: number;
+	transportType?: DTMF_TRANSPORT;
 }

 export interface HoldOptions extends ExtraHeaders {
-  useUpdate?: boolean;
+	useUpdate?: boolean;
 }

 export interface RenegotiateOptions extends HoldOptions {
-  rtcOfferConstraints?: RTCOfferOptions;
+	rtcOfferConstraints?: RTCOfferOptions;
 }

 // events
 export interface DTMF extends EventEmitter {
-  tone: string;
-  duration: number;
+	tone: string;
+	duration: number;
 }

 export interface Info extends EventEmitter {
-  contentType: string;
-  body: string;
+	contentType: string;
+	body: string;
 }

 export interface PeerConnectionEvent {
-  peerconnection: RTCPeerConnectionDeprecated;
+	peerconnection: RTCPeerConnectionDeprecated;
 }

 export interface ConnectingEvent {
-  request: IncomingRequest | OutgoingRequest;
+	request: IncomingRequest | OutgoingRequest;
 }

 export interface SendingEvent {
-  request: OutgoingRequest
+	request: OutgoingRequest;
 }

 export interface IncomingEvent {
-  originator: Originator.LOCAL;
+	originator: Originator.LOCAL;
 }

 export interface EndEvent {
-  originator: Originator;
-  message: IncomingRequest | IncomingResponse;
-  cause: string;
+	originator: Originator;
+	message: IncomingRequest | IncomingResponse;
+	cause: string;
 }

 export interface IncomingDTMFEvent {
-  originator: Originator.REMOTE;
-  dtmf: DTMF;
-  request: IncomingRequest;
+	originator: Originator.REMOTE;
+	dtmf: DTMF;
+	request: IncomingRequest;
 }

 export interface OutgoingDTMFEvent {
-  originator: Originator.LOCAL;
-  dtmf: DTMF;
-  request: OutgoingRequest;
+	originator: Originator.LOCAL;
+	dtmf: DTMF;
+	request: OutgoingRequest;
 }

 export interface IncomingInfoEvent {
-  originator: Originator.REMOTE;
-  info: Info;
-  request: IncomingRequest;
+	originator: Originator.REMOTE;
+	info: Info;
+	request: IncomingRequest;
 }

 export interface OutgoingInfoEvent {
-  originator: Originator.LOCAL;
-  info: Info;
-  request: OutgoingRequest;
+	originator: Originator.LOCAL;
+	info: Info;
+	request: OutgoingRequest;
 }

 export interface HoldEvent {
-  originator: Originator
+	originator: Originator;
 }

 export interface ReInviteEvent {
-  request: IncomingRequest;
-  callback?: VoidFunction;
-  reject: (options?: RejectOptions) => void;
+	request: IncomingRequest;
+	callback?: VoidFunction;
+	reject: (options?: RejectOptions) => void;
 }

 export interface ReferEvent {
-  request: IncomingRequest;
-  accept: Function;
-  reject: VoidFunction;
+	request: IncomingRequest;
+	// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
+	accept: Function;
+	reject: VoidFunction;
 }

 export interface SDPEvent {
-  originator: Originator;
-  type: string;
-  sdp: string;
+	originator: Originator;
+	type: string;
+	sdp: string;
 }

 export interface IceCandidateEvent {
-  candidate: RTCIceCandidate;
-  ready: VoidFunction;
+	candidate: RTCIceCandidate;
+	ready: VoidFunction;
 }

 export interface OutgoingEvent {
-  originator: Originator.REMOTE;
-  response: IncomingResponse;
+	originator: Originator.REMOTE;
+	response: IncomingResponse;
 }

 export interface OutgoingAckEvent {
-  originator: Originator.LOCAL;
+	originator: Originator.LOCAL;
 }

 export interface IncomingAckEvent {
-  originator: Originator.REMOTE;
-  ack: IncomingRequest;
+	originator: Originator.REMOTE;
+	ack: IncomingRequest;
 }

 export interface MediaStreamTypes {
-  audio?: boolean;
-  video?: boolean;
+	audio?: boolean;
+	video?: boolean;
 }

 // listener
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export type GenericErrorListener = (error: any) => void;
 export type PeerConnectionListener = (event: PeerConnectionEvent) => void;
 export type ConnectingListener = (event: ConnectingEvent) => void;
@@ -187,7 +194,9 @@ export type OutgoingListener = (event: OutgoingEvent) => void;
 export type IncomingConfirmedListener = (event: IncomingAckEvent) => void;
 export type OutgoingConfirmedListener = (event: OutgoingAckEvent) => void;
 export type CallListener = IncomingListener | OutgoingListener;
-export type ConfirmedListener = IncomingConfirmedListener | OutgoingConfirmedListener;
+export type ConfirmedListener =
+	| IncomingConfirmedListener
+	| OutgoingConfirmedListener;
 export type EndListener = (event: EndEvent) => void;
 export type IncomingDTMFListener = (event: IncomingDTMFEvent) => void;
 export type OutgoingDTMFListener = (event: OutgoingDTMFEvent) => void;
@@ -204,107 +213,112 @@ export type SDPListener = (event: SDPEvent) => void;
 export type IceCandidateListener = (event: IceCandidateEvent) => void;

 export interface RTCSessionEventMap {
-  'peerconnection': PeerConnectionListener;
-  'connecting': ConnectingListener;
-  'sending': SendingListener;
-  'progress': CallListener;
-  'accepted': CallListener;
-  'confirmed': ConfirmedListener;
-  'ended': EndListener;
-  'failed': EndListener;
-  'newDTMF': DTMFListener;
-  'newInfo': InfoListener;
-  'hold': HoldListener;
-  'unhold': HoldListener;
-  'muted': MuteListener;
-  'unmuted': MuteListener;
-  'reinvite': ReInviteListener;
-  'update': UpdateListener;
-  'refer': ReferListener;
-  'replaces': ReferListener;
-  'sdp': SDPListener;
-  'icecandidate': IceCandidateListener;
-  'getusermediafailed': GenericErrorListener;
-  'peerconnection:createofferfailed': GenericErrorListener;
-  'peerconnection:createanswerfailed': GenericErrorListener;
-  'peerconnection:setlocaldescriptionfailed': GenericErrorListener;
-  'peerconnection:setremotedescriptionfailed': GenericErrorListener;
+	peerconnection: PeerConnectionListener;
+	connecting: ConnectingListener;
+	sending: SendingListener;
+	progress: CallListener;
+	accepted: CallListener;
+	confirmed: ConfirmedListener;
+	ended: EndListener;
+	failed: EndListener;
+	newDTMF: DTMFListener;
+	newInfo: InfoListener;
+	hold: HoldListener;
+	unhold: HoldListener;
+	muted: MuteListener;
+	unmuted: MuteListener;
+	reinvite: ReInviteListener;
+	update: UpdateListener;
+	refer: ReferListener;
+	replaces: ReferListener;
+	sdp: SDPListener;
+	icecandidate: IceCandidateListener;
+	getusermediafailed: GenericErrorListener;
+	'peerconnection:createofferfailed': GenericErrorListener;
+	'peerconnection:createanswerfailed': GenericErrorListener;
+	'peerconnection:setlocaldescriptionfailed': GenericErrorListener;
+	'peerconnection:setremotedescriptionfailed': GenericErrorListener;
 }

 declare enum SessionStatus {
-  STATUS_NULL = 0,
-  STATUS_INVITE_SENT = 1,
-  STATUS_1XX_RECEIVED = 2,
-  STATUS_INVITE_RECEIVED = 3,
-  STATUS_WAITING_FOR_ANSWER = 4,
-  STATUS_ANSWERED = 5,
-  STATUS_WAITING_FOR_ACK = 6,
-  STATUS_CANCELED = 7,
-  STATUS_TERMINATED = 8,
-  STATUS_CONFIRMED = 9
+	STATUS_NULL = 0,
+	STATUS_INVITE_SENT = 1,
+	STATUS_1XX_RECEIVED = 2,
+	STATUS_INVITE_RECEIVED = 3,
+	STATUS_WAITING_FOR_ANSWER = 4,
+	STATUS_ANSWERED = 5,
+	STATUS_WAITING_FOR_ACK = 6,
+	STATUS_CANCELED = 7,
+	STATUS_TERMINATED = 8,
+	STATUS_CONFIRMED = 9,
 }

 export class RTCSession extends EventEmitter {
-  constructor (ua: UA);
+	constructor(ua: UA);

-  static get C(): typeof SessionStatus;
+	static get C(): typeof SessionStatus;

-  get C(): typeof SessionStatus;
+	get C(): typeof SessionStatus;

-  get causes(): typeof causes;
+	get causes(): typeof causes;

-  get id(): string;
+	get id(): string;

-  set data(_data: any);
-  get data(): any;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	set data(_data: any);
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	get data(): any;

-  get connection(): RTCPeerConnectionDeprecated;
+	get connection(): RTCPeerConnectionDeprecated;

-  get contact(): string;
+	get contact(): string;

-  get direction(): SessionDirection;
+	get direction(): SessionDirection;

-  get local_identity(): NameAddrHeader;
+	get local_identity(): NameAddrHeader;

-  get remote_identity(): NameAddrHeader;
+	get remote_identity(): NameAddrHeader;

-  get start_time(): Date;
+	get start_time(): Date;

-  get end_time(): Date;
+	get end_time(): Date;

-  get status(): SessionStatus;
+	get status(): SessionStatus;

-  isInProgress(): boolean;
+	isInProgress(): boolean;

-  isEstablished(): boolean;
+	isEstablished(): boolean;

-  isEnded(): boolean;
+	isEnded(): boolean;

-  isReadyToReOffer(): boolean;
+	isReadyToReOffer(): boolean;

-  answer(options?: AnswerOptions): void;
+	answer(options?: AnswerOptions): void;

-  terminate(options?: TerminateOptions): void;
+	terminate(options?: TerminateOptions): void;

-  sendDTMF(tones: string | number, options?: DTMFOptions): void;
+	sendDTMF(tones: string | number, options?: DTMFOptions): void;

-  sendInfo(contentType: string, body?: string, options?: ExtraHeaders): void;
+	sendInfo(contentType: string, body?: string, options?: ExtraHeaders): void;

-  hold(options?: HoldOptions, done?: VoidFunction): boolean;
+	hold(options?: HoldOptions, done?: VoidFunction): boolean;

-  unhold(options?: HoldOptions, done?: VoidFunction): boolean;
+	unhold(options?: HoldOptions, done?: VoidFunction): boolean;

-  renegotiate(options?: RenegotiateOptions, done?: VoidFunction): boolean;
+	renegotiate(options?: RenegotiateOptions, done?: VoidFunction): boolean;

-  isOnHold(): OnHoldResult;
+	isOnHold(): OnHoldResult;

-  mute(options?: MediaStreamTypes): void;
+	mute(options?: MediaStreamTypes): void;

-  unmute(options?: MediaStreamTypes): void;
+	unmute(options?: MediaStreamTypes): void;

-  isMuted(): MediaStreamTypes;
+	isMuted(): MediaStreamTypes;

-  refer(target: string | URI, options?: ReferOptions): void;
+	refer(target: string | URI, options?: ReferOptions): void;

-  on<T extends keyof RTCSessionEventMap>(type: T, listener: RTCSessionEventMap[T]): this;
+	on<T extends keyof RTCSessionEventMap>(
+		type: T,
+		listener: RTCSessionEventMap[T]
+	): this;
 }
diff --git a/src/RTCSession.js b/src/RTCSession.js
index 1b9e6eb..7ac7e76 100644
--- a/src/RTCSession.js
+++ b/src/RTCSession.js
@@ -1,5 +1,5 @@
-// eslint-disable-next-line no-redeclare
 /* globals RTCPeerConnection: false, RTCSessionDescription: false */
+/* eslint-disable no-invalid-this */

 const EventEmitter = require('events').EventEmitter;
 const sdp_transform = require('sdp-transform');
@@ -21,3630 +21,3346 @@ const URI = require('./URI');
 const logger = new Logger('RTCSession');

 const C = {
-  // RTCSession states.
-  STATUS_NULL               : 0,
-  STATUS_INVITE_SENT        : 1,
-  STATUS_1XX_RECEIVED       : 2,
-  STATUS_INVITE_RECEIVED    : 3,
-  STATUS_WAITING_FOR_ANSWER : 4,
-  STATUS_ANSWERED           : 5,
-  STATUS_WAITING_FOR_ACK    : 6,
-  STATUS_CANCELED           : 7,
-  STATUS_TERMINATED         : 8,
-  STATUS_CONFIRMED          : 9
+	// RTCSession states.
+	STATUS_NULL: 0,
+	STATUS_INVITE_SENT: 1,
+	STATUS_1XX_RECEIVED: 2,
+	STATUS_INVITE_RECEIVED: 3,
+	STATUS_WAITING_FOR_ANSWER: 4,
+	STATUS_ANSWERED: 5,
+	STATUS_WAITING_FOR_ACK: 6,
+	STATUS_CANCELED: 7,
+	STATUS_TERMINATED: 8,
+	STATUS_CONFIRMED: 9,
 };

 /**
  * Local variables.
  */
-const holdMediaTypes = [ 'audio', 'video' ];
-
-module.exports = class RTCSession extends EventEmitter
-{
-  /**
-   * Expose C object.
-   */
-  static get C()
-  {
-    return C;
-  }
-
-  constructor(ua)
-  {
-    logger.debug('new');
-
-    super();
-
-    this._id = null;
-    this._ua = ua;
-    this._status = C.STATUS_NULL;
-    this._dialog = null;
-    this._earlyDialogs = {};
-    this._contact = null;
-    this._from_tag = null;
-    this._to_tag = null;
-
-    // The RTCPeerConnection instance (public attribute).
-    this._connection = null;
-
-    // Prevent races on serial PeerConnction operations.
-    this._connectionPromiseQueue = Promise.resolve();
-
-    // Incoming/Outgoing request being currently processed.
-    this._request = null;
-
-    // Cancel state for initial outgoing request.
-    this._is_canceled = false;
-    this._cancel_reason = '';
-
-    // RTCSession confirmation flag.
-    this._is_confirmed = false;
-
-    // Is late SDP being negotiated.
-    this._late_sdp = false;
-
-    // Default rtcOfferConstraints and rtcAnswerConstrainsts (passed in connect() or answer()).
-    this._rtcOfferConstraints = null;
-    this._rtcAnswerConstraints = null;
-
-    // Local MediaStream.
-    this._localMediaStream = null;
-    this._localMediaStreamLocallyGenerated = false;
-
-    // Flag to indicate PeerConnection ready for new actions.
-    this._rtcReady = true;
-
-    // Flag to indicate ICE candidate gathering is finished even if iceGatheringState is not yet 'complete'.
-    this._iceReady = false;
-
-    // SIP Timers.
-    this._timers = {
-      ackTimer          : null,
-      expiresTimer      : null,
-      invite2xxTimer    : null,
-      userNoAnswerTimer : null
-    };
-
-    // Session info.
-    this._direction = null;
-    this._local_identity = null;
-    this._remote_identity = null;
-    this._start_time = null;
-    this._end_time = null;
-    this._tones = null;
-
-    // Mute/Hold state.
-    this._audioMuted = false;
-    this._videoMuted = false;
-    this._localHold = false;
-    this._remoteHold = false;
-
-    // Session Timers (RFC 4028).
-    this._sessionTimers = {
-      enabled        : this._ua.configuration.session_timers,
-      refreshMethod  : this._ua.configuration.session_timers_refresh_method,
-      defaultExpires : JsSIP_C.SESSION_EXPIRES,
-      currentExpires : null,
-      running        : false,
-      refresher      : false,
-      timer          : null // A setTimeout.
-    };
-
-    // Map of ReferSubscriber instances indexed by the REFER's CSeq number.
-    this._referSubscribers = {};
-
-    // Custom session empty object for high level use.
-    this._data = {};
-  }
-
-  /**
-   * User API
-   */
-
-  // Expose RTCSession constants as a property of the RTCSession instance.
-  get C()
-  {
-    return C;
-  }
-
-  // Expose session failed/ended causes as a property of the RTCSession instance.
-  get causes()
-  {
-    return JsSIP_C.causes;
-  }
-
-  get id()
-  {
-    return this._id;
-  }
-
-  get connection()
-  {
-    return this._connection;
-  }
-
-  get contact()
-  {
-    return this._contact;
-  }
-
-  get direction()
-  {
-    return this._direction;
-  }
-
-  get local_identity()
-  {
-    return this._local_identity;
-  }
-
-  get remote_identity()
-  {
-    return this._remote_identity;
-  }
-
-  get start_time()
-  {
-    return this._start_time;
-  }
-
-  get end_time()
-  {
-    return this._end_time;
-  }
-
-  get data()
-  {
-    return this._data;
-  }
-
-  set data(_data)
-  {
-    this._data = _data;
-  }
-
-  get status()
-  {
-    return this._status;
-  }
-
-  isInProgress()
-  {
-    switch (this._status)
-    {
-      case C.STATUS_NULL:
-      case C.STATUS_INVITE_SENT:
-      case C.STATUS_1XX_RECEIVED:
-      case C.STATUS_INVITE_RECEIVED:
-      case C.STATUS_WAITING_FOR_ANSWER:
-        return true;
-      default:
-        return false;
-    }
-  }
-
-  isEstablished()
-  {
-    switch (this._status)
-    {
-      case C.STATUS_ANSWERED:
-      case C.STATUS_WAITING_FOR_ACK:
-      case C.STATUS_CONFIRMED:
-        return true;
-      default:
-        return false;
-    }
-  }
-
-  isEnded()
-  {
-    switch (this._status)
-    {
-      case C.STATUS_CANCELED:
-      case C.STATUS_TERMINATED:
-        return true;
-      default:
-        return false;
-    }
-  }
-
-  isMuted()
-  {
-    return {
-      audio : this._audioMuted,
-      video : this._videoMuted
-    };
-  }
-
-  isOnHold()
-  {
-    return {
-      local  : this._localHold,
-      remote : this._remoteHold
-    };
-  }
-
-  connect(target, options = {}, initCallback)
-  {
-    logger.debug('connect()');
-
-    const originalTarget = target;
-    const eventHandlers = Utils.cloneObject(options.eventHandlers);
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const mediaConstraints = Utils.cloneObject(options.mediaConstraints, {
-      audio : true,
-      video : true
-    });
-    const mediaStream = options.mediaStream || null;
-    const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
-    const rtcConstraints = options.rtcConstraints || null;
-    const rtcOfferConstraints = options.rtcOfferConstraints || null;
-
-    this._rtcOfferConstraints = rtcOfferConstraints;
-    this._rtcAnswerConstraints = options.rtcAnswerConstraints || null;
-
-    this._data = options.data || this._data;
-
-    // Check target.
-    if (target === undefined)
-    {
-      throw new TypeError('Not enough arguments');
-    }
-
-    // Check Session Status.
-    if (this._status !== C.STATUS_NULL)
-    {
-      throw new Exceptions.InvalidStateError(this._status);
-    }
-
-    // Check WebRTC support.
-    if (!window.RTCPeerConnection)
-    {
-      throw new Exceptions.NotSupportedError('WebRTC not supported');
-    }
-
-    // Check target validity.
-    target = this._ua.normalizeTarget(target);
-    if (!target)
-    {
-      throw new TypeError(`Invalid target: ${originalTarget}`);
-    }
-
-    // Session Timers.
-    if (this._sessionTimers.enabled)
-    {
-      if (Utils.isDecimal(options.sessionTimersExpires))
-      {
-        if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES)
-        {
-          this._sessionTimers.defaultExpires = options.sessionTimersExpires;
-        }
-        else
-        {
-          this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
-        }
-      }
-    }
-
-    // Set event handlers.
-    for (const event in eventHandlers)
-    {
-      if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
-      {
-        this.on(event, eventHandlers[event]);
-      }
-    }
-
-    // Session parameter initialization.
-    this._from_tag = Utils.newTag();
-
-    // Set anonymous property.
-    const anonymous = options.anonymous || false;
-
-    const requestParams = { from_tag: this._from_tag };
-
-    this._contact = this._ua.contact.toString({
-      anonymous,
-      outbound : true
-    });
-
-    if (anonymous)
-    {
-      requestParams.from_display_name = 'Anonymous';
-      requestParams.from_uri = new URI('sip', 'anonymous', 'anonymous.invalid');
-
-      extraHeaders.push(`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`);
-      extraHeaders.push('Privacy: id');
-    }
-    else if (options.fromUserName)
-    {
-      requestParams.from_uri = new URI('sip', options.fromUserName, this._ua.configuration.uri.host);
-
-      extraHeaders.push(`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`);
-    }
-
-    if (options.fromDisplayName)
-    {
-      requestParams.from_display_name = options.fromDisplayName;
-    }
-
-    extraHeaders.push(`Contact: ${this._contact}`);
-    extraHeaders.push('Content-Type: application/sdp');
-    if (this._sessionTimers.enabled)
-    {
-      extraHeaders.push(`Session-Expires: ${this._sessionTimers.defaultExpires}${this._ua.configuration.session_timers_force_refresher ? ';refresher=uac' : ''}`);
-    }
-
-    this._request = new SIPMessage.InitialOutgoingInviteRequest(
-      target, this._ua, requestParams, extraHeaders);
-
-    this._id = this._request.call_id + this._from_tag;
-
-    // Create a new RTCPeerConnection instance.
-    this._createRTCConnection(pcConfig, rtcConstraints);
-
-    // Set internal properties.
-    this._direction = 'outgoing';
-    this._local_identity = this._request.from;
-    this._remote_identity = this._request.to;
-
-    // User explicitly provided a newRTCSession callback for this session.
-    if (initCallback)
-    {
-      initCallback(this);
-    }
-
-    this._newRTCSession('local', this._request);
-
-    this._sendInitialRequest(mediaConstraints, rtcOfferConstraints, mediaStream);
-  }
-
-  init_incoming(request, initCallback)
-  {
-    logger.debug('init_incoming()');
-
-    let expires;
-    const contentType = request.hasHeader('Content-Type') ?
-      request.getHeader('Content-Type').toLowerCase() : undefined;
-
-    // Check body and content type.
-    if (request.body && (contentType !== 'application/sdp'))
-    {
-      request.reply(415);
-
-      return;
-    }
-
-    // Session parameter initialization.
-    this._status = C.STATUS_INVITE_RECEIVED;
-    this._from_tag = request.from_tag;
-    this._id = request.call_id + this._from_tag;
-    this._request = request;
-    this._contact = this._ua.contact.toString();
-
-    // Get the Expires header value if exists.
-    if (request.hasHeader('expires'))
-    {
-      expires = request.getHeader('expires') * 1000;
-    }
-
-    /* Set the to_tag before
-     * replying a response code that will create a dialog.
-     */
-    request.to_tag = Utils.newTag();
-
-    // An error on dialog creation will fire 'failed' event.
-    if (!this._createDialog(request, 'UAS', true))
-    {
-      request.reply(500, 'Missing Contact header field');
-
-      return;
-    }
-
-    if (request.body)
-    {
-      this._late_sdp = false;
-    }
-    else
-    {
-      this._late_sdp = true;
-    }
-
-    this._status = C.STATUS_WAITING_FOR_ANSWER;
-
-    // Set userNoAnswerTimer.
-    this._timers.userNoAnswerTimer = setTimeout(() =>
-    {
-      request.reply(408);
-      this._failed('local', null, JsSIP_C.causes.NO_ANSWER);
-    }, this._ua.configuration.no_answer_timeout
-    );
-
-    /* Set expiresTimer
-     * RFC3261 13.3.1
-     */
-    if (expires)
-    {
-      this._timers.expiresTimer = setTimeout(() =>
-      {
-        if (this._status === C.STATUS_WAITING_FOR_ANSWER)
-        {
-          request.reply(487);
-          this._failed('system', null, JsSIP_C.causes.EXPIRES);
-        }
-      }, expires
-      );
-    }
-
-    // Set internal properties.
-    this._direction = 'incoming';
-    this._local_identity = request.to;
-    this._remote_identity = request.from;
-
-    // A init callback was specifically defined.
-    if (initCallback)
-    {
-      initCallback(this);
-    }
-
-    // Fire 'newRTCSession' event.
-    this._newRTCSession('remote', request);
-
-    // The user may have rejected the call in the 'newRTCSession' event.
-    if (this._status === C.STATUS_TERMINATED)
-    {
-      return;
-    }
-
-    // Reply 180.
-    request.reply(180, null, [ `Contact: ${this._contact}` ]);
-
-    // Fire 'progress' event.
-    // TODO: Document that 'response' field in 'progress' event is null for incoming calls.
-    this._progress('local', null);
-  }
-
-  /**
-   * Answer the call.
-   */
-  answer(options = {})
-  {
-    logger.debug('answer()');
-
-    const request = this._request;
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const mediaConstraints = Utils.cloneObject(options.mediaConstraints);
-    const mediaStream = options.mediaStream || null;
-    const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
-    const rtcConstraints = options.rtcConstraints || null;
-    const rtcAnswerConstraints = options.rtcAnswerConstraints || null;
-    const rtcOfferConstraints = Utils.cloneObject(options.rtcOfferConstraints);
-
-    let tracks;
-    let peerHasAudioLine = false;
-    let peerHasVideoLine = false;
-    let peerOffersFullAudio = false;
-    let peerOffersFullVideo = false;
-
-    this._rtcAnswerConstraints = rtcAnswerConstraints;
-    this._rtcOfferConstraints = options.rtcOfferConstraints || null;
-
-    this._data = options.data || this._data;
-
-    // Check Session Direction and Status.
-    if (this._direction !== 'incoming')
-    {
-      throw new Exceptions.NotSupportedError('"answer" not supported for outgoing RTCSession');
-    }
-
-    // Check Session status.
-    if (this._status !== C.STATUS_WAITING_FOR_ANSWER)
-    {
-      throw new Exceptions.InvalidStateError(this._status);
-    }
-
-    // Session Timers.
-    if (this._sessionTimers.enabled)
-    {
-      if (Utils.isDecimal(options.sessionTimersExpires))
-      {
-        if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES)
-        {
-          this._sessionTimers.defaultExpires = options.sessionTimersExpires;
-        }
-        else
-        {
-          this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
-        }
-      }
-    }
-
-    this._status = C.STATUS_ANSWERED;
-
-    // An error on dialog creation will fire 'failed' event.
-    if (!this._createDialog(request, 'UAS'))
-    {
-      request.reply(500, 'Error creating dialog');
-
-      return;
-    }
-
-    clearTimeout(this._timers.userNoAnswerTimer);
-
-    extraHeaders.unshift(`Contact: ${this._contact}`);
-
-    // Determine incoming media from incoming SDP offer (if any).
-    const sdp = request.parseSDP();
-
-    // Make sure sdp.media is an array, not the case if there is only one media.
-    if (!Array.isArray(sdp.media))
-    {
-      sdp.media = [ sdp.media ];
-    }
-
-    // Go through all medias in SDP to find offered capabilities to answer with.
-    for (const m of sdp.media)
-    {
-      if (m.type === 'audio')
-      {
-        peerHasAudioLine = true;
-        if (!m.direction || m.direction === 'sendrecv')
-        {
-          peerOffersFullAudio = true;
-        }
-      }
-      if (m.type === 'video')
-      {
-        peerHasVideoLine = true;
-        if (!m.direction || m.direction === 'sendrecv')
-        {
-          peerOffersFullVideo = true;
-        }
-      }
-    }
-
-    // Remove audio from mediaStream if suggested by mediaConstraints.
-    if (mediaStream && mediaConstraints.audio === false)
-    {
-      tracks = mediaStream.getAudioTracks();
-      for (const track of tracks)
-      {
-        mediaStream.removeTrack(track);
-      }
-    }
-
-    // Remove video from mediaStream if suggested by mediaConstraints.
-    if (mediaStream && mediaConstraints.video === false)
-    {
-      tracks = mediaStream.getVideoTracks();
-      for (const track of tracks)
-      {
-        mediaStream.removeTrack(track);
-      }
-    }
-
-    // Set audio constraints based on incoming stream if not supplied.
-    if (!mediaStream && mediaConstraints.audio === undefined)
-    {
-      mediaConstraints.audio = peerOffersFullAudio;
-    }
-
-    // Set video constraints based on incoming stream if not supplied.
-    if (!mediaStream && mediaConstraints.video === undefined)
-    {
-      mediaConstraints.video = peerOffersFullVideo;
-    }
-
-    // Don't ask for audio if the incoming offer has no audio section.
-    if (!mediaStream && !peerHasAudioLine && !rtcOfferConstraints.offerToReceiveAudio)
-    {
-      mediaConstraints.audio = false;
-    }
-
-    // Don't ask for video if the incoming offer has no video section.
-    if (!mediaStream && !peerHasVideoLine && !rtcOfferConstraints.offerToReceiveVideo)
-    {
-      mediaConstraints.video = false;
-    }
-
-    // Create a new RTCPeerConnection instance.
-    // TODO: This may throw an error, should react.
-    this._createRTCConnection(pcConfig, rtcConstraints);
-
-    Promise.resolve()
-      // Handle local MediaStream.
-      .then(() =>
-      {
-        // A local MediaStream is given, use it.
-        if (mediaStream)
-        {
-          return mediaStream;
-        }
-
-        // Audio and/or video requested, prompt getUserMedia.
-        else if (mediaConstraints.audio || mediaConstraints.video)
-        {
-          this._localMediaStreamLocallyGenerated = true;
-
-          return navigator.mediaDevices.getUserMedia(mediaConstraints)
-            .catch((error) =>
-            {
-              if (this._status === C.STATUS_TERMINATED)
-              {
-                throw new Error('terminated');
-              }
-
-              request.reply(480);
-              this._failed('local', null, JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS);
-
-              logger.warn('emit "getusermediafailed" [error:%o]', error);
-
-              this.emit('getusermediafailed', error);
-
-              throw new Error('getUserMedia() failed');
-            });
-        }
-      })
-      // Attach MediaStream to RTCPeerconnection.
-      .then((stream) =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        this._localMediaStream = stream;
-        if (stream)
-        {
-          stream.getTracks().forEach((track) =>
-          {
-            this._connection.addTrack(track, stream);
-          });
-        }
-      })
-      // Set remote description.
-      .then(() =>
-      {
-        if (this._late_sdp)
-        {
-          return;
-        }
-
-        const e = { originator: 'remote', type: 'offer', sdp: request.body };
-
-        logger.debug('emit "sdp"');
-        this.emit('sdp', e);
-
-        const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
-
-        this._connectionPromiseQueue = this._connectionPromiseQueue
-          .then(() => this._connection.setRemoteDescription(offer))
-          .catch((error) =>
-          {
-            request.reply(488);
-
-            this._failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
-
-            logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
-            this.emit('peerconnection:setremotedescriptionfailed', error);
-
-            throw new Error('peerconnection.setRemoteDescription() failed');
-          });
-
-        return this._connectionPromiseQueue;
-      })
-      // Create local description.
-      .then(() =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        // TODO: Is this event already useful?
-        this._connecting(request);
-
-        if (!this._late_sdp)
-        {
-          return this._createLocalDescription('answer', rtcAnswerConstraints)
-            .catch(() =>
-            {
-              request.reply(500);
-
-              throw new Error('_createLocalDescription() failed');
-            });
-        }
-        else
-        {
-          return this._createLocalDescription('offer', this._rtcOfferConstraints)
-            .catch(() =>
-            {
-              request.reply(500);
-
-              throw new Error('_createLocalDescription() failed');
-            });
-        }
-      })
-      // Send reply.
-      .then((desc) =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        this._handleSessionTimersInIncomingRequest(request, extraHeaders);
-
-        request.reply(200, null, extraHeaders,
-          desc,
-          () =>
-          {
-            this._status = C.STATUS_WAITING_FOR_ACK;
-
-            this._setInvite2xxTimer(request, desc);
-            this._setACKTimer();
-            this._accepted('local');
-          },
-          () =>
-          {
-            this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
-          }
-        );
-      })
-      .catch((error) =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          return;
-        }
-
-        logger.warn(`answer() failed: ${error.message}`);
-
-        this._failed('system', error.message, JsSIP_C.causes.INTERNAL_ERROR);
-      });
-  }
-
-  /**
-   * Terminate the call.
-   */
-  terminate(options = {})
-  {
-    logger.debug('terminate()');
-
-    const cause = options.cause || JsSIP_C.causes.BYE;
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const body = options.body;
-
-    let cancel_reason;
-    let status_code = options.status_code;
-    let reason_phrase = options.reason_phrase;
-
-    // Check Session Status.
-    if (this._status === C.STATUS_TERMINATED)
-    {
-      throw new Exceptions.InvalidStateError(this._status);
-    }
-
-    switch (this._status)
-    {
-      // - UAC -
-      case C.STATUS_NULL:
-      case C.STATUS_INVITE_SENT:
-      case C.STATUS_1XX_RECEIVED:
-        logger.debug('canceling session');
-
-        if (status_code && (status_code < 200 || status_code >= 700))
-        {
-          throw new TypeError(`Invalid status_code: ${status_code}`);
-        }
-        else if (status_code)
-        {
-          reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
-          cancel_reason = `SIP ;cause=${status_code} ;text="${reason_phrase}"`;
-        }
-
-        // Check Session Status.
-        if (this._status === C.STATUS_NULL || this._status === C.STATUS_INVITE_SENT)
-        {
-          this._is_canceled = true;
-          this._cancel_reason = cancel_reason;
-        }
-        else if (this._status === C.STATUS_1XX_RECEIVED)
-        {
-          this._request.cancel(cancel_reason);
-        }
-
-        this._status = C.STATUS_CANCELED;
-
-        this._failed('local', null, JsSIP_C.causes.CANCELED);
-        break;
-
-        // - UAS -
-      case C.STATUS_WAITING_FOR_ANSWER:
-      case C.STATUS_ANSWERED:
-        logger.debug('rejecting session');
-
-        status_code = status_code || 480;
-
-        if (status_code < 300 || status_code >= 700)
-        {
-          throw new TypeError(`Invalid status_code: ${status_code}`);
-        }
-
-        this._request.reply(status_code, reason_phrase, extraHeaders, body);
-        this._failed('local', null, JsSIP_C.causes.REJECTED);
-        break;
-
-      case C.STATUS_WAITING_FOR_ACK:
-      case C.STATUS_CONFIRMED:
-        logger.debug('terminating session');
-
-        reason_phrase = options.reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
-
-        if (status_code && (status_code < 200 || status_code >= 700))
-        {
-          throw new TypeError(`Invalid status_code: ${status_code}`);
-        }
-        else if (status_code)
-        {
-          extraHeaders.push(`Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`);
-        }
-
-        /* RFC 3261 section 15 (Terminating a session):
-          *
-          * "...the callee's UA MUST NOT send a BYE on a confirmed dialog
-          * until it has received an ACK for its 2xx response or until the server
-          * transaction times out."
-          */
-        if (this._status === C.STATUS_WAITING_FOR_ACK &&
-            this._direction === 'incoming' &&
-            this._request.server_transaction.state !== Transactions.C.STATUS_TERMINATED)
-        {
-
-          // Save the dialog for later restoration.
-          const dialog = this._dialog;
-
-          // Send the BYE as soon as the ACK is received...
-          this.receiveRequest = ({ method }) =>
-          {
-            if (method === JsSIP_C.ACK)
-            {
-              this.sendRequest(JsSIP_C.BYE, {
-                extraHeaders,
-                body
-              });
-              dialog.terminate();
-            }
-          };
-
-          // .., or when the INVITE transaction times out
-          this._request.server_transaction.on('stateChanged', () =>
-          {
-            if (this._request.server_transaction.state ===
-                Transactions.C.STATUS_TERMINATED)
-            {
-              this.sendRequest(JsSIP_C.BYE, {
-                extraHeaders,
-                body
-              });
-              dialog.terminate();
-            }
-          });
-
-          this._ended('local', null, cause);
-
-          // Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-).
-          this._dialog = dialog;
-
-          // Restore the dialog into 'ua' so the ACK can reach 'this' session.
-          this._ua.newDialog(dialog);
-        }
-        else
-        {
-          this.sendRequest(JsSIP_C.BYE, {
-            extraHeaders,
-            body
-          });
-
-          this._ended('local', null, cause);
-        }
-    }
-  }
-
-  sendDTMF(tones, options = {})
-  {
-    logger.debug('sendDTMF() | tones: %s', tones);
-
-    let duration = options.duration || null;
-    let interToneGap = options.interToneGap || null;
-    const transportType = options.transportType || JsSIP_C.DTMF_TRANSPORT.INFO;
-
-    if (tones === undefined)
-    {
-      throw new TypeError('Not enough arguments');
-    }
-
-    // Check Session Status.
-    if (
-      this._status !== C.STATUS_CONFIRMED &&
-      this._status !== C.STATUS_WAITING_FOR_ACK &&
-      this._status !== C.STATUS_1XX_RECEIVED
-    )
-    {
-      throw new Exceptions.InvalidStateError(this._status);
-    }
-
-    // Check Transport type.
-    if (
-      transportType !== JsSIP_C.DTMF_TRANSPORT.INFO &&
-      transportType !== JsSIP_C.DTMF_TRANSPORT.RFC2833
-    )
-    {
-      throw new TypeError(`invalid transportType: ${transportType}`);
-    }
-
-    // Convert to string.
-    if (typeof tones === 'number')
-    {
-      tones = tones.toString();
-    }
-
-    // Check tones.
-    if (!tones || typeof tones !== 'string' || !tones.match(/^[0-9A-DR#*,]+$/i))
-    {
-      throw new TypeError(`Invalid tones: ${tones}`);
-    }
-
-    // Check duration.
-    if (duration && !Utils.isDecimal(duration))
-    {
-      throw new TypeError(`Invalid tone duration: ${duration}`);
-    }
-    else if (!duration)
-    {
-      duration = RTCSession_DTMF.C.DEFAULT_DURATION;
-    }
-    else if (duration < RTCSession_DTMF.C.MIN_DURATION)
-    {
-      logger.debug(`"duration" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_DURATION} milliseconds`);
-      duration = RTCSession_DTMF.C.MIN_DURATION;
-    }
-    else if (duration > RTCSession_DTMF.C.MAX_DURATION)
-    {
-      logger.debug(`"duration" value is greater than the maximum allowed, setting it to ${RTCSession_DTMF.C.MAX_DURATION} milliseconds`);
-      duration = RTCSession_DTMF.C.MAX_DURATION;
-    }
-    else
-    {
-      duration = Math.abs(duration);
-    }
-    options.duration = duration;
-
-    // Check interToneGap.
-    if (interToneGap && !Utils.isDecimal(interToneGap))
-    {
-      throw new TypeError(`Invalid interToneGap: ${interToneGap}`);
-    }
-    else if (!interToneGap)
-    {
-      interToneGap = RTCSession_DTMF.C.DEFAULT_INTER_TONE_GAP;
-    }
-    else if (interToneGap < RTCSession_DTMF.C.MIN_INTER_TONE_GAP)
-    {
-      logger.debug(`"interToneGap" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_INTER_TONE_GAP} milliseconds`);
-      interToneGap = RTCSession_DTMF.C.MIN_INTER_TONE_GAP;
-    }
-    else
-    {
-      interToneGap = Math.abs(interToneGap);
-    }
-
-    // RFC2833. Let RTCDTMFSender enqueue the DTMFs.
-    if (transportType === JsSIP_C.DTMF_TRANSPORT.RFC2833)
-    {
-      // Send DTMF in current audio RTP stream.
-      const sender = this._getDTMFRTPSender();
-
-      if (sender)
-      {
-        // Add remaining buffered tones.
-        tones = sender.toneBuffer + tones;
-        // Insert tones.
-        sender.insertDTMF(tones, duration, interToneGap);
-      }
-
-      return;
-    }
-
-    if (this._tones)
-    {
-      // Tones are already queued, just add to the queue.
-      this._tones += tones;
-
-      return;
-    }
-
-    this._tones = tones;
-
-    // Send the first tone.
-    _sendDTMF.call(this);
-
-    function _sendDTMF()
-    {
-      let timeout;
-
-      if (this._status === C.STATUS_TERMINATED || !this._tones)
-      {
-        // Stop sending DTMF.
-        this._tones = null;
-
-        return;
-      }
-
-      // Retrieve the next tone.
-      const tone = this._tones[0];
-
-      // Remove the tone from this._tones.
-      this._tones = this._tones.substring(1);
-
-      if (tone === ',')
-      {
-        timeout = 2000;
-      }
-      else
-      {
-        // Send DTMF via SIP INFO messages.
-        const dtmf = new RTCSession_DTMF(this);
-
-        options.eventHandlers = {
-          onFailed : () => { this._tones = null; }
-        };
-        dtmf.send(tone, options);
-        timeout = duration + interToneGap;
-      }
-
-      // Set timeout for the next tone.
-      setTimeout(_sendDTMF.bind(this), timeout);
-    }
-  }
-
-  sendInfo(contentType, body, options = {})
-  {
-    logger.debug('sendInfo()');
-
-    // Check Session Status.
-    if (
-      this._status !== C.STATUS_CONFIRMED &&
-      this._status !== C.STATUS_WAITING_FOR_ACK &&
-      this._status !== C.STATUS_1XX_RECEIVED
-    )
-    {
-      throw new Exceptions.InvalidStateError(this._status);
-    }
-
-    const info = new RTCSession_Info(this);
-
-    info.send(contentType, body, options);
-  }
-
-  /**
-   * Mute
-   */
-  mute(options = { audio: true, video: false })
-  {
-    logger.debug('mute()');
-
-    let audioMuted = false, videoMuted = false;
-
-    if (this._audioMuted === false && options.audio)
-    {
-      audioMuted = true;
-      this._audioMuted = true;
-      this._toggleMuteAudio(true);
-    }
-
-    if (this._videoMuted === false && options.video)
-    {
-      videoMuted = true;
-      this._videoMuted = true;
-      this._toggleMuteVideo(true);
-    }
-
-    if (audioMuted === true || videoMuted === true)
-    {
-      this._onmute({
-        audio : audioMuted,
-        video : videoMuted
-      });
-    }
-  }
-
-  /**
-   * Unmute
-   */
-  unmute(options = { audio: true, video: true })
-  {
-    logger.debug('unmute()');
-
-    let audioUnMuted = false, videoUnMuted = false;
-
-    if (this._audioMuted === true && options.audio)
-    {
-      audioUnMuted = true;
-      this._audioMuted = false;
-
-      if (this._localHold === false)
-      {
-        this._toggleMuteAudio(false);
-      }
-    }
-
-    if (this._videoMuted === true && options.video)
-    {
-      videoUnMuted = true;
-      this._videoMuted = false;
-
-      if (this._localHold === false)
-      {
-        this._toggleMuteVideo(false);
-      }
-    }
-
-    if (audioUnMuted === true || videoUnMuted === true)
-    {
-      this._onunmute({
-        audio : audioUnMuted,
-        video : videoUnMuted
-      });
-    }
-  }
-
-  /**
-   * Hold
-   */
-  hold(options = {}, done)
-  {
-    logger.debug('hold()');
-
-    if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
-    {
-      return false;
-    }
-
-    if (this._localHold === true)
-    {
-      return false;
-    }
-
-    if (!this.isReadyToReOffer())
-    {
-      return false;
-    }
-
-    this._localHold = true;
-    this._onhold('local');
-
-    const eventHandlers = {
-      succeeded : () =>
-      {
-        if (done) { done(); }
-      },
-      failed : () =>
-      {
-        this.terminate({
-          cause         : JsSIP_C.causes.WEBRTC_ERROR,
-          status_code   : 500,
-          reason_phrase : 'Hold Failed'
-        });
-      }
-    };
-
-    if (options.useUpdate)
-    {
-      this._sendUpdate({
-        sdpOffer     : true,
-        eventHandlers,
-        extraHeaders : options.extraHeaders
-      });
-    }
-    else
-    {
-      this._sendReinvite({
-        eventHandlers,
-        extraHeaders : options.extraHeaders
-      });
-    }
-
-    return true;
-  }
-
-  unhold(options = {}, done)
-  {
-    logger.debug('unhold()');
-
-    if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
-    {
-      return false;
-    }
-
-    if (this._localHold === false)
-    {
-      return false;
-    }
-
-    if (!this.isReadyToReOffer())
-    {
-      return false;
-    }
-
-    this._localHold = false;
-    this._onunhold('local');
-
-    const eventHandlers = {
-      succeeded : () =>
-      {
-        if (done) { done(); }
-      },
-      failed : () =>
-      {
-        this.terminate({
-          cause         : JsSIP_C.causes.WEBRTC_ERROR,
-          status_code   : 500,
-          reason_phrase : 'Unhold Failed'
-        });
-      }
-    };
-
-    if (options.useUpdate)
-    {
-      this._sendUpdate({
-        sdpOffer     : true,
-        eventHandlers,
-        extraHeaders : options.extraHeaders
-      });
-    }
-    else
-    {
-      this._sendReinvite({
-        eventHandlers,
-        extraHeaders : options.extraHeaders
-      });
-    }
-
-    return true;
-  }
-
-  renegotiate(options = {}, done)
-  {
-    logger.debug('renegotiate()');
-
-    const rtcOfferConstraints = options.rtcOfferConstraints || null;
-
-    if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
-    {
-      return false;
-    }
-
-    if (!this.isReadyToReOffer())
-    {
-      return false;
-    }
-
-    const eventHandlers = {
-      succeeded : () =>
-      {
-        if (done) { done(); }
-      },
-      failed : () =>
-      {
-        this.terminate({
-          cause         : JsSIP_C.causes.WEBRTC_ERROR,
-          status_code   : 500,
-          reason_phrase : 'Media Renegotiation Failed'
-        });
-      }
-    };
-
-    this._setLocalMediaStatus();
-
-    if (options.useUpdate)
-    {
-      this._sendUpdate({
-        sdpOffer     : true,
-        eventHandlers,
-        rtcOfferConstraints,
-        extraHeaders : options.extraHeaders
-      });
-    }
-    else
-    {
-      this._sendReinvite({
-        eventHandlers,
-        rtcOfferConstraints,
-        extraHeaders : options.extraHeaders
-      });
-    }
-
-    return true;
-  }
-
-  /**
-   * Refer
-   */
-  refer(target, options)
-  {
-    logger.debug('refer()');
-
-    const originalTarget = target;
-
-    if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
-    {
-      return false;
-    }
-
-    // Check target validity.
-    target = this._ua.normalizeTarget(target);
-    if (!target)
-    {
-      throw new TypeError(`Invalid target: ${originalTarget}`);
-    }
-
-    const referSubscriber = new RTCSession_ReferSubscriber(this);
-
-    referSubscriber.sendRefer(target, options);
-
-    // Store in the map.
-    const id = referSubscriber.id;
-
-    this._referSubscribers[id] = referSubscriber;
-
-    // Listen for ending events so we can remove it from the map.
-    referSubscriber.on('requestFailed', () =>
-    {
-      delete this._referSubscribers[id];
-    });
-    referSubscriber.on('accepted', () =>
-    {
-      delete this._referSubscribers[id];
-    });
-    referSubscriber.on('failed', () =>
-    {
-      delete this._referSubscribers[id];
-    });
-
-    return referSubscriber;
-  }
-
-  /**
-   * Send a generic in-dialog Request
-   */
-  sendRequest(method, options)
-  {
-    logger.debug('sendRequest()');
-
-    if (this._dialog)
-    {
-      return this._dialog.sendRequest(method, options);
-    }
-    else
-    {
-      const dialogsArray = Object.values(this._earlyDialogs);
-
-      if (dialogsArray.length > 0)
-      {
-        return dialogsArray[0].sendRequest(method, options);
-      }
-
-      logger.warn('sendRequest() | no valid early dialog found');
-
-      return;
-    }
-  }
-
-  /**
-   * In dialog Request Reception
-   */
-  receiveRequest(request)
-  {
-    logger.debug('receiveRequest()');
-
-    if (request.method === JsSIP_C.CANCEL)
-    {
-      /* RFC3261 15 States that a UAS may have accepted an invitation while a CANCEL
-      * was in progress and that the UAC MAY continue with the session established by
-      * any 2xx response, or MAY terminate with BYE. JsSIP does continue with the
-      * established session. So the CANCEL is processed only if the session is not yet
-      * established.
-      */
-
-      /*
-      * Terminate the whole session in case the user didn't accept (or yet send the answer)
-      * nor reject the request opening the session.
-      */
-      if (this._status === C.STATUS_WAITING_FOR_ANSWER ||
-          this._status === C.STATUS_ANSWERED)
-      {
-        this._status = C.STATUS_CANCELED;
-        this._request.reply(487);
-        this._failed('remote', request, JsSIP_C.causes.CANCELED);
-      }
-    }
-    else
-    {
-      // Requests arriving here are in-dialog requests.
-      switch (request.method)
-      {
-        case JsSIP_C.ACK:
-          if (this._status !== C.STATUS_WAITING_FOR_ACK)
-          {
-            return;
-          }
-
-          // Update signaling status.
-          this._status = C.STATUS_CONFIRMED;
-
-          clearTimeout(this._timers.ackTimer);
-          clearTimeout(this._timers.invite2xxTimer);
-
-          if (this._late_sdp)
-          {
-            if (!request.body)
-            {
-              this.terminate({
-                cause       : JsSIP_C.causes.MISSING_SDP,
-                status_code : 400
-              });
-              break;
-            }
-
-            const e = { originator: 'remote', type: 'answer', sdp: request.body };
-
-            logger.debug('emit "sdp"');
-            this.emit('sdp', e);
-
-            const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
-            this._connectionPromiseQueue = this._connectionPromiseQueue
-              .then(() => this._connection.setRemoteDescription(answer))
-              .then(() =>
-              {
-                if (!this._is_confirmed)
-                {
-                  this._confirmed('remote', request);
-                }
-              })
-              .catch((error) =>
-              {
-                this.terminate({
-                  cause       : JsSIP_C.causes.BAD_MEDIA_DESCRIPTION,
-                  status_code : 488
-                });
-
-                logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-                this.emit('peerconnection:setremotedescriptionfailed', error);
-              });
-          }
-          else if (!this._is_confirmed)
-          {
-            this._confirmed('remote', request);
-          }
-
-          break;
-        case JsSIP_C.BYE:
-          if (this._status === C.STATUS_CONFIRMED ||
-              this._status === C.STATUS_WAITING_FOR_ACK)
-          {
-            request.reply(200);
-            this._ended('remote', request, JsSIP_C.causes.BYE);
-          }
-          else if (this._status === C.STATUS_INVITE_RECEIVED ||
-                   this._status === C.STATUS_WAITING_FOR_ANSWER)
-          {
-            request.reply(200);
-            this._request.reply(487, 'BYE Received');
-            this._ended('remote', request, JsSIP_C.causes.BYE);
-          }
-          else
-          {
-            request.reply(403, 'Wrong Status');
-          }
-          break;
-        case JsSIP_C.INVITE:
-          if (this._status === C.STATUS_CONFIRMED)
-          {
-            if (request.hasHeader('replaces'))
-            {
-              this._receiveReplaces(request);
-            }
-            else
-            {
-              this._receiveReinvite(request);
-            }
-          }
-          else
-          {
-            request.reply(403, 'Wrong Status');
-          }
-          break;
-        case JsSIP_C.INFO:
-          if (this._status === C.STATUS_1XX_RECEIVED ||
-              this._status === C.STATUS_WAITING_FOR_ANSWER ||
-              this._status === C.STATUS_ANSWERED ||
-              this._status === C.STATUS_WAITING_FOR_ACK ||
-              this._status === C.STATUS_CONFIRMED)
-          {
-            const contentType = request.hasHeader('Content-Type') ?
-              request.getHeader('Content-Type').toLowerCase() : undefined;
-
-            if (contentType && (contentType.match(/^application\/dtmf-relay/i)))
-            {
-              new RTCSession_DTMF(this).init_incoming(request);
-            }
-            else if (contentType !== undefined)
-            {
-              new RTCSession_Info(this).init_incoming(request);
-            }
-            else
-            {
-              request.reply(415);
-            }
-          }
-          else
-          {
-            request.reply(403, 'Wrong Status');
-          }
-          break;
-        case JsSIP_C.UPDATE:
-          if (this._status === C.STATUS_CONFIRMED)
-          {
-            this._receiveUpdate(request);
-          }
-          else
-          {
-            request.reply(403, 'Wrong Status');
-          }
-          break;
-        case JsSIP_C.REFER:
-          if (this._status === C.STATUS_CONFIRMED)
-          {
-            this._receiveRefer(request);
-          }
-          else
-          {
-            request.reply(403, 'Wrong Status');
-          }
-          break;
-        case JsSIP_C.NOTIFY:
-          if (this._status === C.STATUS_CONFIRMED)
-          {
-            this._receiveNotify(request);
-          }
-          else
-          {
-            request.reply(403, 'Wrong Status');
-          }
-          break;
-        default:
-          request.reply(501);
-      }
-    }
-  }
-
-  /**
-   * Session Callbacks
-   */
-
-  onTransportError()
-  {
-    logger.warn('onTransportError()');
-
-    if (this._status !== C.STATUS_TERMINATED)
-    {
-      this.terminate({
-        status_code   : 500,
-        reason_phrase : JsSIP_C.causes.CONNECTION_ERROR,
-        cause         : JsSIP_C.causes.CONNECTION_ERROR
-      });
-    }
-  }
-
-  onRequestTimeout()
-  {
-    logger.warn('onRequestTimeout()');
-
-    if (this._status !== C.STATUS_TERMINATED)
-    {
-      this.terminate({
-        status_code   : 408,
-        reason_phrase : JsSIP_C.causes.REQUEST_TIMEOUT,
-        cause         : JsSIP_C.causes.REQUEST_TIMEOUT
-      });
-    }
-  }
-
-  onDialogError()
-  {
-    logger.warn('onDialogError()');
-
-    if (this._status !== C.STATUS_TERMINATED)
-    {
-      this.terminate({
-        status_code   : 500,
-        reason_phrase : JsSIP_C.causes.DIALOG_ERROR,
-        cause         : JsSIP_C.causes.DIALOG_ERROR
-      });
-    }
-  }
-
-  // Called from DTMF handler.
-  newDTMF(data)
-  {
-    logger.debug('newDTMF()');
-
-    this.emit('newDTMF', data);
-  }
-
-  // Called from Info handler.
-  newInfo(data)
-  {
-    logger.debug('newInfo()');
-
-    this.emit('newInfo', data);
-  }
-
-  /**
-   * Check if RTCSession is ready for an outgoing re-INVITE or UPDATE with SDP.
-   */
-  isReadyToReOffer()
-  {
-    if (!this._rtcReady)
-    {
-      logger.debug('isReadyToReOffer() | internal WebRTC status not ready');
-
-      return false;
-    }
-
-    // No established yet.
-    if (!this._dialog)
-    {
-      logger.debug('isReadyToReOffer() | session not established yet');
-
-      return false;
-    }
-
-    // Another INVITE transaction is in progress.
-    if (this._dialog.uac_pending_reply === true ||
-        this._dialog.uas_pending_reply === true)
-    {
-      logger.debug('isReadyToReOffer() | there is another INVITE/UPDATE transaction in progress');
-
-      return false;
-    }
-
-    return true;
-  }
-
-  _close()
-  {
-    logger.debug('close()');
-
-    // Close local MediaStream if it was not given by the user.
-    if (this._localMediaStream && this._localMediaStreamLocallyGenerated)
-    {
-      logger.debug('close() | closing local MediaStream');
-
-      Utils.closeMediaStream(this._localMediaStream);
-    }
-
-    if (this._status === C.STATUS_TERMINATED)
-    {
-      return;
-    }
-
-    this._status = C.STATUS_TERMINATED;
-
-    // Terminate RTC.
-    if (this._connection)
-    {
-      try
-      {
-        this._connection.close();
-      }
-      catch (error)
-      {
-        logger.warn('close() | error closing the RTCPeerConnection: %o', error);
-      }
-    }
-
-    // Terminate signaling.
-
-    // Clear SIP timers.
-    for (const timer in this._timers)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._timers, timer))
-      {
-        clearTimeout(this._timers[timer]);
-      }
-    }
-
-    // Clear Session Timers.
-    clearTimeout(this._sessionTimers.timer);
-
-    // Terminate confirmed dialog.
-    if (this._dialog)
-    {
-      this._dialog.terminate();
-      delete this._dialog;
-    }
-
-    // Terminate early dialogs.
-    for (const dialog in this._earlyDialogs)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._earlyDialogs, dialog))
-      {
-        this._earlyDialogs[dialog].terminate();
-        delete this._earlyDialogs[dialog];
-      }
-    }
-
-    // Terminate REFER subscribers.
-    for (const subscriber in this._referSubscribers)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._referSubscribers, subscriber))
-      {
-        delete this._referSubscribers[subscriber];
-      }
-    }
-
-    this._ua.destroyRTCSession(this);
-  }
-
-  /**
-   * Private API.
-   */
-
-  /**
-   * RFC3261 13.3.1.4
-   * Response retransmissions cannot be accomplished by transaction layer
-   *  since it is destroyed when receiving the first 2xx answer
-   */
-  _setInvite2xxTimer(request, body)
-  {
-    let timeout = Timers.T1;
-
-    function invite2xxRetransmission()
-    {
-      if (this._status !== C.STATUS_WAITING_FOR_ACK)
-      {
-        return;
-      }
-
-      request.reply(200, null, [ `Contact: ${this._contact}` ], body);
-
-      if (timeout < Timers.T2)
-      {
-        timeout = timeout * 2;
-        if (timeout > Timers.T2)
-        {
-          timeout = Timers.T2;
-        }
-      }
-
-      this._timers.invite2xxTimer = setTimeout(
-        invite2xxRetransmission.bind(this), timeout);
-    }
-
-    this._timers.invite2xxTimer = setTimeout(
-      invite2xxRetransmission.bind(this), timeout);
-  }
-
-
-  /**
-   * RFC3261 14.2
-   * If a UAS generates a 2xx response and never receives an ACK,
-   *  it SHOULD generate a BYE to terminate the dialog.
-   */
-  _setACKTimer()
-  {
-    this._timers.ackTimer = setTimeout(() =>
-    {
-      if (this._status === C.STATUS_WAITING_FOR_ACK)
-      {
-        logger.debug('no ACK received, terminating the session');
-
-        clearTimeout(this._timers.invite2xxTimer);
-        this.sendRequest(JsSIP_C.BYE);
-        this._ended('remote', null, JsSIP_C.causes.NO_ACK);
-      }
-    }, Timers.TIMER_H);
-  }
-
-
-  _createRTCConnection(pcConfig, rtcConstraints)
-  {
-    this._connection = new RTCPeerConnection(pcConfig, rtcConstraints);
-
-    this._connection.addEventListener('iceconnectionstatechange', () =>
-    {
-      const state = this._connection.iceConnectionState;
-
-      // TODO: Do more with different states.
-      if (state === 'failed')
-      {
-        this.terminate({
-          cause         : JsSIP_C.causes.RTP_TIMEOUT,
-          status_code   : 408,
-          reason_phrase : JsSIP_C.causes.RTP_TIMEOUT
-        });
-      }
-    });
-
-    logger.debug('emit "peerconnection"');
-
-    this.emit('peerconnection', {
-      peerconnection : this._connection
-    });
-  }
-
-  _createLocalDescription(type, constraints)
-  {
-    logger.debug('createLocalDescription()');
-
-    if (type !== 'offer' && type !== 'answer')
-      throw new Error(`createLocalDescription() | invalid type "${type}"`);
-
-    const connection = this._connection;
-
-    this._rtcReady = false;
-
-    return Promise.resolve()
-      // Create Offer or Answer.
-      .then(() =>
-      {
-        if (type === 'offer')
-        {
-          return connection.createOffer(constraints)
-            .catch((error) =>
-            {
-              logger.warn('emit "peerconnection:createofferfailed" [error:%o]', error);
-
-              this.emit('peerconnection:createofferfailed', error);
-
-              return Promise.reject(error);
-            });
-        }
-        else
-        {
-          return connection.createAnswer(constraints)
-            .catch((error) =>
-            {
-              logger.warn('emit "peerconnection:createanswerfailed" [error:%o]', error);
-
-              this.emit('peerconnection:createanswerfailed', error);
-
-              return Promise.reject(error);
-            });
-        }
-      })
-      // Set local description.
-      .then((desc) =>
-      {
-        return connection.setLocalDescription(desc)
-          .catch((error) =>
-          {
-            this._rtcReady = true;
-
-            logger.warn('emit "peerconnection:setlocaldescriptionfailed" [error:%o]', error);
-
-            this.emit('peerconnection:setlocaldescriptionfailed', error);
-
-            return Promise.reject(error);
-          });
-      })
-      .then(() =>
-      {
-        // Resolve right away if 'pc.iceGatheringState' is 'complete'.
-        /**
-         * Resolve right away if:
-         * - 'connection.iceGatheringState' is 'complete' and no 'iceRestart' constraint is set.
-         * - 'connection.iceGatheringState' is 'gathering' and 'iceReady' is true.
-         */
-        const iceRestart = constraints && constraints.iceRestart;
-
-        if ((connection.iceGatheringState === 'complete' && !iceRestart) ||
-          (connection.iceGatheringState === 'gathering' && this._iceReady))
-        {
-          this._rtcReady = true;
-
-          const e = { originator: 'local', type: type, sdp: connection.localDescription.sdp };
-
-          logger.debug('emit "sdp"');
-
-          this.emit('sdp', e);
-
-          return Promise.resolve(e.sdp);
-        }
-
-        // Add 'pc.onicencandidate' event handler to resolve on last candidate.
-        return new Promise((resolve) =>
-        {
-          let finished = false;
-          let iceCandidateListener;
-          let iceGatheringStateListener;
-
-          this._iceReady = false;
-
-          const ready = () =>
-          {
-            if (finished)
-            {
-              return;
-            }
-
-            connection.removeEventListener('icecandidate', iceCandidateListener);
-            connection.removeEventListener('icegatheringstatechange', iceGatheringStateListener);
-
-            finished = true;
-            this._rtcReady = true;
-
-            // connection.iceGatheringState will still indicate 'gathering' and thus be blocking.
-            this._iceReady = true;
-
-            const e = { originator: 'local', type: type, sdp: connection.localDescription.sdp };
-
-            logger.debug('emit "sdp"');
-
-            this.emit('sdp', e);
-
-            resolve(e.sdp);
-          };
-
-          connection.addEventListener('icecandidate', iceCandidateListener = (event) =>
-          {
-            const candidate = event.candidate;
-
-            if (candidate)
-            {
-              this.emit('icecandidate', {
-                candidate,
-                ready
-              });
-            }
-            else
-            {
-              ready();
-            }
-          });
-
-          connection.addEventListener('icegatheringstatechange', iceGatheringStateListener = () =>
-          {
-            if (connection.iceGatheringState === 'complete')
-            {
-              ready();
-            }
-          });
-        });
-      });
-  }
-
-  /**
-   * Dialog Management
-   */
-  _createDialog(message, type, early)
-  {
-    const local_tag = (type === 'UAS') ? message.to_tag : message.from_tag;
-    const remote_tag = (type === 'UAS') ? message.from_tag : message.to_tag;
-    const id = message.call_id + local_tag + remote_tag;
-
-    let early_dialog = this._earlyDialogs[id];
-
-    // Early Dialog.
-    if (early)
-    {
-      if (early_dialog)
-      {
-        return true;
-      }
-      else
-      {
-        early_dialog = new Dialog(this, message, type, Dialog.C.STATUS_EARLY);
-
-        // Dialog has been successfully created.
-        if (early_dialog.error)
-        {
-          logger.debug(early_dialog.error);
-          this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
-
-          return false;
-        }
-        else
-        {
-          this._earlyDialogs[id] = early_dialog;
-
-          return true;
-        }
-      }
-    }
-
-    // Confirmed Dialog.
-    else
-    {
-      this._from_tag = message.from_tag;
-      this._to_tag = message.to_tag;
-
-      // In case the dialog is in _early_ state, update it.
-      if (early_dialog)
-      {
-        early_dialog.update(message, type);
-        this._dialog = early_dialog;
-        delete this._earlyDialogs[id];
-
-        return true;
-      }
-
-      // Otherwise, create a _confirmed_ dialog.
-      const dialog = new Dialog(this, message, type);
-
-      if (dialog.error)
-      {
-        logger.debug(dialog.error);
-        this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
-
-        return false;
-      }
-      else
-      {
-        this._dialog = dialog;
-
-        return true;
-      }
-    }
-  }
-
-  /**
-   * In dialog INVITE Reception
-   */
-
-  _receiveReinvite(request)
-  {
-    logger.debug('receiveReinvite()');
-
-    const contentType = request.hasHeader('Content-Type') ?
-      request.getHeader('Content-Type').toLowerCase() : undefined;
-    const data = {
-      request,
-      callback : undefined,
-      reject   : reject.bind(this)
-    };
-
-    let rejected = false;
-
-    function reject(options = {})
-    {
-      rejected = true;
-
-      const status_code = options.status_code || 403;
-      const reason_phrase = options.reason_phrase || '';
-      const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
-      if (this._status !== C.STATUS_CONFIRMED)
-      {
-        return false;
-      }
-
-      if (status_code < 300 || status_code >= 700)
-      {
-        throw new TypeError(`Invalid status_code: ${status_code}`);
-      }
-
-      request.reply(status_code, reason_phrase, extraHeaders);
-    }
-
-    // Emit 'reinvite'.
-    this.emit('reinvite', data);
-
-    if (rejected)
-    {
-      return;
-    }
-
-    this._late_sdp = false;
-
-    // Request without SDP.
-    if (!request.body)
-    {
-      this._late_sdp = true;
-      if (this._remoteHold)
-      {
-        this._remoteHold = false;
-        this._onunhold('remote');
-      }
-      this._connectionPromiseQueue = this._connectionPromiseQueue
-        .then(() => this._createLocalDescription('offer', this._rtcOfferConstraints))
-        .then((sdp) =>
-        {
-          sendAnswer.call(this, sdp);
-        })
-        .catch(() =>
-        {
-          request.reply(500);
-        });
-
-      return;
-    }
-
-    // Request with SDP.
-    if (contentType !== 'application/sdp')
-    {
-      logger.debug('invalid Content-Type');
-      request.reply(415);
-
-      return;
-    }
-
-    this._processInDialogSdpOffer(request)
-      // Send answer.
-      .then((desc) =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          return;
-        }
-
-        sendAnswer.call(this, desc);
-      })
-      .catch((error) =>
-      {
-        logger.warn(error);
-      });
-
-    function sendAnswer(desc)
-    {
-      const extraHeaders = [ `Contact: ${this._contact}` ];
-
-      this._handleSessionTimersInIncomingRequest(request, extraHeaders);
-
-      if (this._late_sdp)
-      {
-        desc = this._mangleOffer(desc);
-      }
-
-      request.reply(200, null, extraHeaders, desc,
-        () =>
-        {
-          this._status = C.STATUS_WAITING_FOR_ACK;
-          this._setInvite2xxTimer(request, desc);
-          this._setACKTimer();
-        }
-      );
-
-      // If callback is given execute it.
-      if (typeof data.callback === 'function')
-      {
-        data.callback();
-      }
-    }
-  }
-
-  /**
-   * In dialog UPDATE Reception
-   */
-  _receiveUpdate(request)
-  {
-    logger.debug('receiveUpdate()');
-
-    const contentType = request.hasHeader('Content-Type') ?
-      request.getHeader('Content-Type').toLowerCase() : undefined;
-    const data = {
-      request,
-      callback : undefined,
-      reject   : reject.bind(this)
-    };
-
-    let rejected = false;
-
-    function reject(options = {})
-    {
-      rejected = true;
-
-      const status_code = options.status_code || 403;
-      const reason_phrase = options.reason_phrase || '';
-      const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
-      if (this._status !== C.STATUS_CONFIRMED)
-      {
-        return false;
-      }
-
-      if (status_code < 300 || status_code >= 700)
-      {
-        throw new TypeError(`Invalid status_code: ${status_code}`);
-      }
-
-      request.reply(status_code, reason_phrase, extraHeaders);
-    }
-
-    // Emit 'update'.
-    this.emit('update', data);
-
-    if (rejected)
-    {
-      return;
-    }
-
-    if (!request.body)
-    {
-      sendAnswer.call(this, null);
-
-      return;
-    }
-
-    if (contentType !== 'application/sdp')
-    {
-      logger.debug('invalid Content-Type');
-
-      request.reply(415);
-
-      return;
-    }
-
-    this._processInDialogSdpOffer(request)
-      // Send answer.
-      .then((desc) =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          return;
-        }
-
-        sendAnswer.call(this, desc);
-      })
-      .catch((error) =>
-      {
-        logger.warn(error);
-      });
-
-    function sendAnswer(desc)
-    {
-      const extraHeaders = [ `Contact: ${this._contact}` ];
-
-      this._handleSessionTimersInIncomingRequest(request, extraHeaders);
-
-      request.reply(200, null, extraHeaders, desc);
-
-      // If callback is given execute it.
-      if (typeof data.callback === 'function')
-      {
-        data.callback();
-      }
-    }
-  }
-
-  _processInDialogSdpOffer(request)
-  {
-    logger.debug('_processInDialogSdpOffer()');
-
-    const sdp = request.parseSDP();
-
-    let hold = false;
-
-    for (const m of sdp.media)
-    {
-      if (holdMediaTypes.indexOf(m.type) === -1)
-      {
-        continue;
-      }
-
-      const direction = m.direction || sdp.direction || 'sendrecv';
-
-      if (direction === 'sendonly' || direction === 'inactive')
-      {
-        hold = true;
-      }
-      // If at least one of the streams is active don't emit 'hold'.
-      else
-      {
-        hold = false;
-        break;
-      }
-    }
-
-    const e = { originator: 'remote', type: 'offer', sdp: request.body };
-
-    logger.debug('emit "sdp"');
-    this.emit('sdp', e);
-
-    const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
-
-    this._connectionPromiseQueue = this._connectionPromiseQueue
-      // Set remote description.
-      .then(() =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        return this._connection.setRemoteDescription(offer)
-          .catch((error) =>
-          {
-            request.reply(488);
-            logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
-            this.emit('peerconnection:setremotedescriptionfailed', error);
-
-            throw error;
-          });
-      })
-      .then(() =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        if (this._remoteHold === true && hold === false)
-        {
-          this._remoteHold = false;
-          this._onunhold('remote');
-        }
-        else if (this._remoteHold === false && hold === true)
-        {
-          this._remoteHold = true;
-          this._onhold('remote');
-        }
-      })
-      // Create local description.
-      .then(() =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        return this._createLocalDescription('answer', this._rtcAnswerConstraints)
-          .catch((error) =>
-          {
-            request.reply(500);
-            logger.warn('emit "peerconnection:createtelocaldescriptionfailed" [error:%o]', error);
-
-            throw error;
-          });
-      })
-      .catch((error) =>
-      {
-        logger.warn('_processInDialogSdpOffer() failed [error: %o]', error);
-      });
-
-    return this._connectionPromiseQueue;
-  }
-
-  /**
-   * In dialog Refer Reception
-   */
-  _receiveRefer(request)
-  {
-    logger.debug('receiveRefer()');
-
-    if (!request.refer_to)
-    {
-      logger.debug('no Refer-To header field present in REFER');
-      request.reply(400);
-
-      return;
-    }
-
-    if (request.refer_to.uri.scheme !== JsSIP_C.SIP)
-    {
-      logger.debug('Refer-To header field points to a non-SIP URI scheme');
-      request.reply(416);
-
-      return;
-    }
-
-    // Reply before the transaction timer expires.
-    request.reply(202);
-
-    const notifier = new RTCSession_ReferNotifier(this, request.cseq);
-
-    logger.debug('emit "refer"');
-
-    // Emit 'refer'.
-    this.emit('refer', {
-      request,
-      accept : (initCallback, options) =>
-      {
-        accept.call(this, initCallback, options);
-      },
-      reject : () =>
-      {
-        reject.call(this);
-      }
-    });
-
-    function accept(initCallback, options = {})
-    {
-      initCallback = (typeof initCallback === 'function')? initCallback : null;
-
-      if (this._status !== C.STATUS_WAITING_FOR_ACK &&
-          this._status !== C.STATUS_CONFIRMED)
-      {
-        return false;
-      }
-
-      const session = new RTCSession(this._ua);
-
-      session.on('progress', ({ response }) =>
-      {
-        notifier.notify(response.status_code, response.reason_phrase);
-      });
-
-      session.on('accepted', ({ response }) =>
-      {
-        notifier.notify(response.status_code, response.reason_phrase);
-      });
-
-      session.on('_failed', ({ message, cause }) =>
-      {
-        if (message)
-        {
-          notifier.notify(message.status_code, message.reason_phrase);
-        }
-        else
-        {
-          notifier.notify(487, cause);
-        }
-      });
-
-      // Consider the Replaces header present in the Refer-To URI.
-      if (request.refer_to.uri.hasHeader('replaces'))
-      {
-        const replaces = decodeURIComponent(request.refer_to.uri.getHeader('replaces'));
-
-        options.extraHeaders = Utils.cloneArray(options.extraHeaders);
-        options.extraHeaders.push(`Replaces: ${replaces}`);
-      }
-
-      session.connect(request.refer_to.uri.toAor(), options, initCallback);
-    }
-
-    function reject()
-    {
-      notifier.notify(603);
-    }
-  }
-
-  /**
-   * In dialog Notify Reception
-   */
-  _receiveNotify(request)
-  {
-    logger.debug('receiveNotify()');
-
-    if (!request.event)
-    {
-      request.reply(400);
-    }
-
-    switch (request.event.event)
-    {
-      case 'refer': {
-        let id;
-        let referSubscriber;
-
-        if (request.event.params && request.event.params.id)
-        {
-          id = request.event.params.id;
-          referSubscriber = this._referSubscribers[id];
-        }
-        else if (Object.keys(this._referSubscribers).length === 1)
-        {
-          referSubscriber = this._referSubscribers[
-            Object.keys(this._referSubscribers)[0]];
-        }
-        else
-        {
-          request.reply(400, 'Missing event id parameter');
-
-          return;
-        }
-
-        if (!referSubscriber)
-        {
-          request.reply(481, 'Subscription does not exist');
-
-          return;
-        }
-
-        referSubscriber.receiveNotify(request);
-        request.reply(200);
-
-        break;
-      }
-
-      default: {
-        request.reply(489);
-      }
-    }
-  }
-
-  /**
-   * INVITE with Replaces Reception
-   */
-  _receiveReplaces(request)
-  {
-    logger.debug('receiveReplaces()');
-
-    function accept(initCallback)
-    {
-      if (this._status !== C.STATUS_WAITING_FOR_ACK &&
-          this._status !== C.STATUS_CONFIRMED)
-      {
-        return false;
-      }
-
-      const session = new RTCSession(this._ua);
-
-      // Terminate the current session when the new one is confirmed.
-      session.on('confirmed', () =>
-      {
-        this.terminate();
-      });
-
-      session.init_incoming(request, initCallback);
-    }
-
-    function reject()
-    {
-      logger.debug('Replaced INVITE rejected by the user');
-      request.reply(486);
-    }
-
-    // Emit 'replace'.
-    this.emit('replaces', {
-      request,
-      accept : (initCallback) => { accept.call(this, initCallback); },
-      reject : () => { reject.call(this); }
-    });
-  }
-
-  /**
-   * Initial Request Sender
-   */
-  _sendInitialRequest(mediaConstraints, rtcOfferConstraints, mediaStream)
-  {
-    const request_sender = new RequestSender(this._ua, this._request, {
-      onRequestTimeout : () =>
-      {
-        this.onRequestTimeout();
-      },
-      onTransportError : () =>
-      {
-        this.onTransportError();
-      },
-      // Update the request on authentication.
-      onAuthenticated : (request) =>
-      {
-        this._request = request;
-      },
-      onReceiveResponse : (response) =>
-      {
-        this._receiveInviteResponse(response);
-      }
-    });
-
-    // This Promise is resolved within the next iteration, so the app has now
-    // a chance to set events such as 'peerconnection' and 'connecting'.
-    Promise.resolve()
-      // Get a stream if required.
-      .then(() =>
-      {
-        // A stream is given, let the app set events such as 'peerconnection' and 'connecting'.
-        if (mediaStream)
-        {
-          return mediaStream;
-        }
-        // Request for user media access.
-        else if (mediaConstraints.audio || mediaConstraints.video)
-        {
-          this._localMediaStreamLocallyGenerated = true;
-
-          return navigator.mediaDevices.getUserMedia(mediaConstraints)
-            .catch((error) =>
-            {
-              if (this._status === C.STATUS_TERMINATED)
-              {
-                throw new Error('terminated');
-              }
-
-              this._failed('local', null, JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS);
-
-              logger.warn('emit "getusermediafailed" [error:%o]', error);
-
-              this.emit('getusermediafailed', error);
-
-              throw error;
-            });
-        }
-      })
-      .then((stream) =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        this._localMediaStream = stream;
-
-        if (stream)
-        {
-          stream.getTracks().forEach((track) =>
-          {
-            this._connection.addTrack(track, stream);
-          });
-        }
-
-        // TODO: should this be triggered here?
-        this._connecting(this._request);
-
-        return this._createLocalDescription('offer', rtcOfferConstraints)
-          .catch((error) =>
-          {
-            this._failed('local', null, JsSIP_C.causes.WEBRTC_ERROR);
-
-            throw error;
-          });
-      })
-      .then((desc) =>
-      {
-        if (this._is_canceled || this._status === C.STATUS_TERMINATED)
-        {
-          throw new Error('terminated');
-        }
-
-        this._request.body = desc;
-        this._status = C.STATUS_INVITE_SENT;
-
-        logger.debug('emit "sending" [request:%o]', this._request);
-
-        // Emit 'sending' so the app can mangle the body before the request is sent.
-        this.emit('sending', {
-          request : this._request
-        });
-
-        request_sender.send();
-      })
-      .catch((error) =>
-      {
-        if (this._status === C.STATUS_TERMINATED)
-        {
-          return;
-        }
-
-        logger.warn(error);
-      });
-  }
-
-  /**
-   * Get DTMF RTCRtpSender.
-   */
-  _getDTMFRTPSender()
-  {
-    const sender = this._connection.getSenders().find((rtpSender) =>
-    {
-      return rtpSender.track && rtpSender.track.kind === 'audio';
-    });
-
-    if (!(sender && sender.dtmf))
-    {
-      logger.warn('sendDTMF() | no local audio track to send DTMF with');
-
-      return;
-    }
-
-    return sender.dtmf;
-  }
-
-  /**
-   * Reception of Response for Initial INVITE
-   */
-  _receiveInviteResponse(response)
-  {
-    logger.debug('receiveInviteResponse()');
-
-    // Handle 2XX retransmissions and responses from forked requests.
-    if (this._dialog && (response.status_code >=200 && response.status_code <=299))
-    {
-
-      /*
-       * If it is a retransmission from the endpoint that established
-       * the dialog, send an ACK
-       */
-      if (this._dialog.id.call_id === response.call_id &&
-          this._dialog.id.local_tag === response.from_tag &&
-          this._dialog.id.remote_tag === response.to_tag)
-      {
-        this.sendRequest(JsSIP_C.ACK);
-
-        return;
-      }
-
-      // If not, send an ACK  and terminate.
-      else
-      {
-        const dialog = new Dialog(this, response, 'UAC');
-
-        if (dialog.error !== undefined)
-        {
-          logger.debug(dialog.error);
-
-          return;
-        }
-
-        this.sendRequest(JsSIP_C.ACK);
-        this.sendRequest(JsSIP_C.BYE);
-
-        return;
-      }
-
-    }
-
-    // Proceed to cancellation if the user requested.
-    if (this._is_canceled)
-    {
-      if (response.status_code >= 100 && response.status_code < 200)
-      {
-        this._request.cancel(this._cancel_reason);
-      }
-      else if (response.status_code >= 200 && response.status_code < 299)
-      {
-        this._acceptAndTerminate(response);
-      }
-
-      return;
-    }
-
-    if (this._status !== C.STATUS_INVITE_SENT && this._status !== C.STATUS_1XX_RECEIVED)
-    {
-      return;
-    }
-
-    switch (true)
-    {
-      case /^100$/.test(response.status_code):
-        this._status = C.STATUS_1XX_RECEIVED;
-        break;
-
-      case /^1[0-9]{2}$/.test(response.status_code):
-      {
-        // Do nothing with 1xx responses without To tag.
-        if (!response.to_tag)
-        {
-          logger.debug('1xx response received without to tag');
-          break;
-        }
-
-        // Create Early Dialog if 1XX comes with contact.
-        if (response.hasHeader('contact'))
-        {
-          // An error on dialog creation will fire 'failed' event.
-          if (!this._createDialog(response, 'UAC', true))
-          {
-            break;
-          }
-        }
-
-        this._status = C.STATUS_1XX_RECEIVED;
-
-        if (!response.body)
-        {
-          this._progress('remote', response);
-          break;
-        }
-
-        const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
-        logger.debug('emit "sdp"');
-        this.emit('sdp', e);
-
-        const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
-        this._connectionPromiseQueue = this._connectionPromiseQueue
-          .then(() => this._connection.setRemoteDescription(answer))
-          .then(() => this._progress('remote', response))
-          .catch((error) =>
-          {
-            logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
-            this.emit('peerconnection:setremotedescriptionfailed', error);
-          });
-        break;
-      }
-
-      case /^2[0-9]{2}$/.test(response.status_code):
-      {
-        this._status = C.STATUS_CONFIRMED;
-
-        if (!response.body)
-        {
-          this._acceptAndTerminate(response, 400, JsSIP_C.causes.MISSING_SDP);
-          this._failed('remote', response, JsSIP_C.causes.BAD_MEDIA_DESCRIPTION);
-          break;
-        }
-
-        // An error on dialog creation will fire 'failed' event.
-        if (!this._createDialog(response, 'UAC'))
-        {
-          break;
-        }
-
-        const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
-        logger.debug('emit "sdp"');
-        this.emit('sdp', e);
-
-        const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
-        this._connectionPromiseQueue = this._connectionPromiseQueue
-          .then(() =>
-          {
-            // Be ready for 200 with SDP after a 180/183 with SDP.
-            // We created a SDP 'answer' for it, so check the current signaling state.
-            if (this._connection.signalingState === 'stable')
-            {
-              return this._connection.createOffer(this._rtcOfferConstraints)
-                .then((offer) => this._connection.setLocalDescription(offer))
-                .catch((error) =>
-                {
-                  this._acceptAndTerminate(response, 500, error.toString());
-                  this._failed('local', response, JsSIP_C.causes.WEBRTC_ERROR);
-                });
-            }
-          })
-          .then(() =>
-          {
-            this._connection.setRemoteDescription(answer)
-              .then(() =>
-              {
-                // Handle Session Timers.
-                this._handleSessionTimersInIncomingResponse(response);
-
-                this._accepted('remote', response);
-                this.sendRequest(JsSIP_C.ACK);
-                this._confirmed('local', null);
-              })
-              .catch((error) =>
-              {
-                this._acceptAndTerminate(response, 488, 'Not Acceptable Here');
-                this._failed('remote', response, JsSIP_C.causes.BAD_MEDIA_DESCRIPTION);
-
-                logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
-                this.emit('peerconnection:setremotedescriptionfailed', error);
-              });
-          });
-        break;
-      }
-
-      default:
-      {
-        const cause = Utils.sipErrorCause(response.status_code);
-
-        this._failed('remote', response, cause);
-      }
-    }
-  }
-
-  /**
-   * Send Re-INVITE
-   */
-  _sendReinvite(options = {})
-  {
-    logger.debug('sendReinvite()');
-
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const eventHandlers = Utils.cloneObject(options.eventHandlers);
-    const rtcOfferConstraints = options.rtcOfferConstraints ||
-      this._rtcOfferConstraints || null;
-
-    let succeeded = false;
-
-    extraHeaders.push(`Contact: ${this._contact}`);
-    extraHeaders.push('Content-Type: application/sdp');
-
-    // Session Timers.
-    if (this._sessionTimers.running)
-    {
-      extraHeaders.push(`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`);
-    }
-
-    this._connectionPromiseQueue = this._connectionPromiseQueue
-      .then(() => this._createLocalDescription('offer', rtcOfferConstraints))
-      .then((sdp) =>
-      {
-        sdp = this._mangleOffer(sdp);
-
-        const e = { originator: 'local', type: 'offer', sdp };
-
-        logger.debug('emit "sdp"');
-        this.emit('sdp', e);
-
-        this.sendRequest(JsSIP_C.INVITE, {
-          extraHeaders,
-          body          : sdp,
-          eventHandlers : {
-            onSuccessResponse : (response) =>
-            {
-              onSucceeded.call(this, response);
-              succeeded = true;
-            },
-            onErrorResponse : (response) =>
-            {
-              onFailed.call(this, response);
-            },
-            onTransportError : () =>
-            {
-              this.onTransportError(); // Do nothing because session ends.
-            },
-            onRequestTimeout : () =>
-            {
-              this.onRequestTimeout(); // Do nothing because session ends.
-            },
-            onDialogError : () =>
-            {
-              this.onDialogError(); // Do nothing because session ends.
-            }
-          }
-        });
-      })
-      .catch(() =>
-      {
-        onFailed();
-      });
-
-    function onSucceeded(response)
-    {
-      if (this._status === C.STATUS_TERMINATED)
-      {
-        return;
-      }
-
-      this.sendRequest(JsSIP_C.ACK);
-
-      // If it is a 2XX retransmission exit now.
-      if (succeeded) { return; }
-
-      // Handle Session Timers.
-      this._handleSessionTimersInIncomingResponse(response);
-
-      // Must have SDP answer.
-      if (!response.body)
-      {
-        onFailed.call(this);
-
-        return;
-      }
-      else if (!response.hasHeader('Content-Type') || response.getHeader('Content-Type').toLowerCase() !== 'application/sdp')
-      {
-        onFailed.call(this);
-
-        return;
-      }
-
-      const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
-      logger.debug('emit "sdp"');
-      this.emit('sdp', e);
-
-      const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
-      this._connectionPromiseQueue = this._connectionPromiseQueue
-        .then(() => this._connection.setRemoteDescription(answer))
-        .then(() =>
-        {
-          if (eventHandlers.succeeded)
-          {
-            eventHandlers.succeeded(response);
-          }
-        })
-        .catch((error) =>
-        {
-          onFailed.call(this);
-
-          logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
-          this.emit('peerconnection:setremotedescriptionfailed', error);
-        });
-    }
-
-    function onFailed(response)
-    {
-      if (eventHandlers.failed)
-      {
-        eventHandlers.failed(response);
-      }
-    }
-  }
-
-  /**
-   * Send UPDATE
-   */
-  _sendUpdate(options = {})
-  {
-    logger.debug('sendUpdate()');
-
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const eventHandlers = Utils.cloneObject(options.eventHandlers);
-    const rtcOfferConstraints = options.rtcOfferConstraints ||
-      this._rtcOfferConstraints || null;
-    const sdpOffer = options.sdpOffer || false;
-
-    let succeeded = false;
-
-    extraHeaders.push(`Contact: ${this._contact}`);
-
-    // Session Timers.
-    if (this._sessionTimers.running)
-    {
-      extraHeaders.push(`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`);
-    }
-
-    if (sdpOffer)
-    {
-      extraHeaders.push('Content-Type: application/sdp');
-
-      this._connectionPromiseQueue = this._connectionPromiseQueue
-        .then(() => this._createLocalDescription('offer', rtcOfferConstraints))
-        .then((sdp) =>
-        {
-          sdp = this._mangleOffer(sdp);
-
-          const e = { originator: 'local', type: 'offer', sdp };
-
-          logger.debug('emit "sdp"');
-          this.emit('sdp', e);
-
-          this.sendRequest(JsSIP_C.UPDATE, {
-            extraHeaders,
-            body          : sdp,
-            eventHandlers : {
-              onSuccessResponse : (response) =>
-              {
-                onSucceeded.call(this, response);
-                succeeded = true;
-              },
-              onErrorResponse : (response) =>
-              {
-                onFailed.call(this, response);
-              },
-              onTransportError : () =>
-              {
-                this.onTransportError(); // Do nothing because session ends.
-              },
-              onRequestTimeout : () =>
-              {
-                this.onRequestTimeout(); // Do nothing because session ends.
-              },
-              onDialogError : () =>
-              {
-                this.onDialogError(); // Do nothing because session ends.
-              }
-            }
-          });
-        })
-        .catch(() =>
-        {
-          onFailed.call(this);
-        });
-    }
-
-    // No SDP.
-    else
-    {
-      this.sendRequest(JsSIP_C.UPDATE, {
-        extraHeaders,
-        eventHandlers : {
-          onSuccessResponse : (response) =>
-          {
-            onSucceeded.call(this, response);
-          },
-          onErrorResponse : (response) =>
-          {
-            onFailed.call(this, response);
-          },
-          onTransportError : () =>
-          {
-            this.onTransportError(); // Do nothing because session ends.
-          },
-          onRequestTimeout : () =>
-          {
-            this.onRequestTimeout(); // Do nothing because session ends.
-          },
-          onDialogError : () =>
-          {
-            this.onDialogError(); // Do nothing because session ends.
-          }
-        }
-      });
-    }
-
-    function onSucceeded(response)
-    {
-      if (this._status === C.STATUS_TERMINATED)
-      {
-        return;
-      }
-
-      // If it is a 2XX retransmission exit now.
-      if (succeeded) { return; }
-
-      // Handle Session Timers.
-      this._handleSessionTimersInIncomingResponse(response);
-
-      // Must have SDP answer.
-      if (sdpOffer)
-      {
-        if (!response.body)
-        {
-          onFailed.call(this);
-
-          return;
-        }
-        else if (!response.hasHeader('Content-Type') || response.getHeader('Content-Type').toLowerCase() !== 'application/sdp')
-        {
-          onFailed.call(this);
-
-          return;
-        }
-
-        const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
-        logger.debug('emit "sdp"');
-        this.emit('sdp', e);
-
-        const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
-        this._connectionPromiseQueue = this._connectionPromiseQueue
-          .then(() => this._connection.setRemoteDescription(answer))
-          .then(() =>
-          {
-            if (eventHandlers.succeeded)
-            {
-              eventHandlers.succeeded(response);
-            }
-          })
-          .catch((error) =>
-          {
-            onFailed.call(this);
-
-            logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
-            this.emit('peerconnection:setremotedescriptionfailed', error);
-          });
-      }
-      // No SDP answer.
-      else if (eventHandlers.succeeded)
-      {
-        eventHandlers.succeeded(response);
-      }
-    }
-
-    function onFailed(response)
-    {
-      if (eventHandlers.failed) { eventHandlers.failed(response); }
-    }
-  }
-
-  _acceptAndTerminate(response, status_code, reason_phrase)
-  {
-    logger.debug('acceptAndTerminate()');
-
-    const extraHeaders = [];
-
-    if (status_code)
-    {
-      reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
-      extraHeaders.push(`Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`);
-    }
-
-    // An error on dialog creation will fire 'failed' event.
-    if (this._dialog || this._createDialog(response, 'UAC'))
-    {
-      this.sendRequest(JsSIP_C.ACK);
-      this.sendRequest(JsSIP_C.BYE, {
-        extraHeaders
-      });
-    }
-
-    // Update session status.
-    this._status = C.STATUS_TERMINATED;
-  }
-
-  /**
-   * Correctly set the SDP direction attributes if the call is on local hold
-   */
-  _mangleOffer(sdp)
-  {
-
-    if (!this._localHold && !this._remoteHold)
-    {
-      return sdp;
-    }
-
-    sdp = sdp_transform.parse(sdp);
-
-    // Local hold.
-    if (this._localHold && !this._remoteHold)
-    {
-      logger.debug('mangleOffer() | me on hold, mangling offer');
-      for (const m of sdp.media)
-      {
-        if (holdMediaTypes.indexOf(m.type) === -1)
-        {
-          continue;
-        }
-        if (!m.direction)
-        {
-          m.direction = 'sendonly';
-        }
-        else if (m.direction === 'sendrecv')
-        {
-          m.direction = 'sendonly';
-        }
-        else if (m.direction === 'recvonly')
-        {
-          m.direction = 'inactive';
-        }
-      }
-    }
-    // Local and remote hold.
-    else if (this._localHold && this._remoteHold)
-    {
-      logger.debug('mangleOffer() | both on hold, mangling offer');
-      for (const m of sdp.media)
-      {
-        if (holdMediaTypes.indexOf(m.type) === -1)
-        {
-          continue;
-        }
-        m.direction = 'inactive';
-      }
-    }
-    // Remote hold.
-    else if (this._remoteHold)
-    {
-      logger.debug('mangleOffer() | remote on hold, mangling offer');
-      for (const m of sdp.media)
-      {
-        if (holdMediaTypes.indexOf(m.type) === -1)
-        {
-          continue;
-        }
-        if (!m.direction)
-        {
-          m.direction = 'recvonly';
-        }
-        else if (m.direction === 'sendrecv')
-        {
-          m.direction = 'recvonly';
-        }
-        else if (m.direction === 'recvonly')
-        {
-          m.direction = 'inactive';
-        }
-      }
-    }
-
-    return sdp_transform.write(sdp);
-  }
-
-  _setLocalMediaStatus()
-  {
-    let enableAudio = true, enableVideo = true;
-
-    if (this._localHold || this._remoteHold)
-    {
-      enableAudio = false;
-      enableVideo = false;
-    }
-
-    if (this._audioMuted)
-    {
-      enableAudio = false;
-    }
-
-    if (this._videoMuted)
-    {
-      enableVideo = false;
-    }
-
-    this._toggleMuteAudio(!enableAudio);
-    this._toggleMuteVideo(!enableVideo);
-  }
-
-  /**
-   * Handle SessionTimers for an incoming INVITE or UPDATE.
-   * @param  {IncomingRequest} request
-   * @param  {Array} responseExtraHeaders  Extra headers for the 200 response.
-   */
-  _handleSessionTimersInIncomingRequest(request, responseExtraHeaders)
-  {
-    if (!this._sessionTimers.enabled) { return; }
-
-    let session_expires_refresher;
-
-    if (request.session_expires && request.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES)
-    {
-      this._sessionTimers.currentExpires = request.session_expires;
-      session_expires_refresher = request.session_expires_refresher || 'uas';
-    }
-    else
-    {
-      this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
-      session_expires_refresher = 'uas';
-    }
-
-    responseExtraHeaders.push(`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${session_expires_refresher}`);
-
-    this._sessionTimers.refresher = (session_expires_refresher === 'uas');
-    this._runSessionTimer();
-  }
-
-  /**
-   * Handle SessionTimers for an incoming response to INVITE or UPDATE.
-   * @param  {IncomingResponse} response
-   */
-  _handleSessionTimersInIncomingResponse(response)
-  {
-    if (!this._sessionTimers.enabled) { return; }
-
-    let session_expires_refresher;
-
-    if (response.session_expires &&
-        response.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES)
-    {
-      this._sessionTimers.currentExpires = response.session_expires;
-      session_expires_refresher = response.session_expires_refresher || 'uac';
-    }
-    else
-    {
-      this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
-      session_expires_refresher = 'uac';
-    }
-
-    this._sessionTimers.refresher = (session_expires_refresher === 'uac');
-    this._runSessionTimer();
-  }
-
-  _runSessionTimer()
-  {
-    const expires = this._sessionTimers.currentExpires;
-
-    this._sessionTimers.running = true;
-
-    clearTimeout(this._sessionTimers.timer);
-
-    // I'm the refresher.
-    if (this._sessionTimers.refresher)
-    {
-      this._sessionTimers.timer = setTimeout(() =>
-      {
-        if (this._status === C.STATUS_TERMINATED) { return; }
-
-        if (!this.isReadyToReOffer()) { return; }
-
-        logger.debug('runSessionTimer() | sending session refresh request');
-
-        if (this._sessionTimers.refreshMethod === JsSIP_C.UPDATE)
-        {
-          this._sendUpdate();
-        }
-        else
-        {
-          this._sendReinvite();
-        }
-      }, expires * 500); // Half the given interval (as the RFC states).
-    }
-
-    // I'm not the refresher.
-    else
-    {
-      this._sessionTimers.timer = setTimeout(() =>
-      {
-        if (this._status === C.STATUS_TERMINATED) { return; }
-
-        logger.warn('runSessionTimer() | timer expired, terminating the session');
-
-        this.terminate({
-          cause         : JsSIP_C.causes.REQUEST_TIMEOUT,
-          status_code   : 408,
-          reason_phrase : 'Session Timer Expired'
-        });
-      }, expires * 1100);
-    }
-  }
-
-  _toggleMuteAudio(mute)
-  {
-    const senders = this._connection.getSenders().filter((sender) =>
-    {
-      return sender.track && sender.track.kind === 'audio';
-    });
-
-    for (const sender of senders)
-    {
-      sender.track.enabled = !mute;
-    }
-  }
-
-  _toggleMuteVideo(mute)
-  {
-    const senders = this._connection.getSenders().filter((sender) =>
-    {
-      return sender.track && sender.track.kind === 'video';
-    });
-
-    for (const sender of senders)
-    {
-      sender.track.enabled = !mute;
-    }
-  }
-
-  _newRTCSession(originator, request)
-  {
-    logger.debug('newRTCSession()');
-
-    this._ua.newRTCSession(this, {
-      originator,
-      session : this,
-      request
-    });
-  }
-
-  _connecting(request)
-  {
-    logger.debug('session connecting');
-
-    logger.debug('emit "connecting"');
-
-    this.emit('connecting', {
-      request
-    });
-  }
-
-  _progress(originator, response)
-  {
-    logger.debug('session progress');
-
-    logger.debug('emit "progress"');
-
-    this.emit('progress', {
-      originator,
-      response : response || null
-    });
-  }
-
-  _accepted(originator, message)
-  {
-    logger.debug('session accepted');
-
-    this._start_time = new Date();
-
-    logger.debug('emit "accepted"');
-
-    this.emit('accepted', {
-      originator,
-      response : message || null
-    });
-  }
-
-  _confirmed(originator, ack)
-  {
-    logger.debug('session confirmed');
+const holdMediaTypes = ['audio', 'video'];
+
+module.exports = class RTCSession extends EventEmitter {
+	/**
+	 * Expose C object.
+	 */
+	static get C() {
+		return C;
+	}
+
+	constructor(ua) {
+		logger.debug('new');
+
+		super();
+
+		this._id = null;
+		this._ua = ua;
+		this._status = C.STATUS_NULL;
+		this._dialog = null;
+		this._earlyDialogs = {};
+		this._contact = null;
+		this._from_tag = null;
+		this._to_tag = null;
+
+		// The RTCPeerConnection instance (public attribute).
+		this._connection = null;
+
+		// Prevent races on serial PeerConnction operations.
+		this._connectionPromiseQueue = Promise.resolve();
+
+		// Incoming/Outgoing request being currently processed.
+		this._request = null;
+
+		// Cancel state for initial outgoing request.
+		this._is_canceled = false;
+		this._cancel_reason = '';
+
+		// RTCSession confirmation flag.
+		this._is_confirmed = false;
+
+		// Is late SDP being negotiated.
+		this._late_sdp = false;
+
+		// Default rtcOfferConstraints and rtcAnswerConstrainsts (passed in connect() or answer()).
+		this._rtcOfferConstraints = null;
+		this._rtcAnswerConstraints = null;
+
+		// Local MediaStream.
+		this._localMediaStream = null;
+		this._localMediaStreamLocallyGenerated = false;
+
+		// Flag to indicate PeerConnection ready for new actions.
+		this._rtcReady = true;
+
+		// Flag to indicate ICE candidate gathering is finished even if iceGatheringState is not yet 'complete'.
+		this._iceReady = false;
+
+		// SIP Timers.
+		this._timers = {
+			ackTimer: null,
+			expiresTimer: null,
+			invite2xxTimer: null,
+			userNoAnswerTimer: null,
+		};
+
+		// Session info.
+		this._direction = null;
+		this._local_identity = null;
+		this._remote_identity = null;
+		this._start_time = null;
+		this._end_time = null;
+		this._tones = null;
+
+		// Mute/Hold state.
+		this._audioMuted = false;
+		this._videoMuted = false;
+		this._localHold = false;
+		this._remoteHold = false;
+
+		// Session Timers (RFC 4028).
+		this._sessionTimers = {
+			enabled: this._ua.configuration.session_timers,
+			refreshMethod: this._ua.configuration.session_timers_refresh_method,
+			defaultExpires: JsSIP_C.SESSION_EXPIRES,
+			currentExpires: null,
+			running: false,
+			refresher: false,
+			timer: null, // A setTimeout.
+		};
+
+		// Map of ReferSubscriber instances indexed by the REFER's CSeq number.
+		this._referSubscribers = {};
+
+		// Custom session empty object for high level use.
+		this._data = {};
+	}
+
+	/**
+	 * User API
+	 */
+
+	// Expose RTCSession constants as a property of the RTCSession instance.
+	get C() {
+		return C;
+	}
+
+	// Expose session failed/ended causes as a property of the RTCSession instance.
+	get causes() {
+		return JsSIP_C.causes;
+	}
+
+	get id() {
+		return this._id;
+	}
+
+	get connection() {
+		return this._connection;
+	}
+
+	get contact() {
+		return this._contact;
+	}
+
+	get direction() {
+		return this._direction;
+	}
+
+	get local_identity() {
+		return this._local_identity;
+	}
+
+	get remote_identity() {
+		return this._remote_identity;
+	}
+
+	get start_time() {
+		return this._start_time;
+	}
+
+	get end_time() {
+		return this._end_time;
+	}
+
+	get data() {
+		return this._data;
+	}
+
+	set data(_data) {
+		this._data = _data;
+	}
+
+	get status() {
+		return this._status;
+	}
+
+	isInProgress() {
+		switch (this._status) {
+			case C.STATUS_NULL:
+			case C.STATUS_INVITE_SENT:
+			case C.STATUS_1XX_RECEIVED:
+			case C.STATUS_INVITE_RECEIVED:
+			case C.STATUS_WAITING_FOR_ANSWER: {
+				return true;
+			}
+			default: {
+				return false;
+			}
+		}
+	}
+
+	isEstablished() {
+		switch (this._status) {
+			case C.STATUS_ANSWERED:
+			case C.STATUS_WAITING_FOR_ACK:
+			case C.STATUS_CONFIRMED: {
+				return true;
+			}
+			default: {
+				return false;
+			}
+		}
+	}
+
+	isEnded() {
+		switch (this._status) {
+			case C.STATUS_CANCELED:
+			case C.STATUS_TERMINATED: {
+				return true;
+			}
+			default: {
+				return false;
+			}
+		}
+	}
+
+	isMuted() {
+		return {
+			audio: this._audioMuted,
+			video: this._videoMuted,
+		};
+	}
+
+	isOnHold() {
+		return {
+			local: this._localHold,
+			remote: this._remoteHold,
+		};
+	}
+
+	connect(target, options = {}, initCallback) {
+		logger.debug('connect()');
+
+		const originalTarget = target;
+		const eventHandlers = Utils.cloneObject(options.eventHandlers);
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const mediaConstraints = Utils.cloneObject(options.mediaConstraints, {
+			audio: true,
+			video: true,
+		});
+		const mediaStream = options.mediaStream || null;
+		const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
+		const rtcConstraints = options.rtcConstraints || null;
+		const rtcOfferConstraints = options.rtcOfferConstraints || null;
+
+		this._rtcOfferConstraints = rtcOfferConstraints;
+		this._rtcAnswerConstraints = options.rtcAnswerConstraints || null;
+
+		this._data = options.data || this._data;
+
+		// Check target.
+		if (target === undefined) {
+			throw new TypeError('Not enough arguments');
+		}
+
+		// Check Session Status.
+		if (this._status !== C.STATUS_NULL) {
+			throw new Exceptions.InvalidStateError(this._status);
+		}
+
+		// Check WebRTC support.
+		// eslint-disable-next-line no-undef
+		if (!window.RTCPeerConnection) {
+			throw new Exceptions.NotSupportedError('WebRTC not supported');
+		}
+
+		// Check target validity.
+		target = this._ua.normalizeTarget(target);
+		if (!target) {
+			throw new TypeError(`Invalid target: ${originalTarget}`);
+		}
+
+		// Session Timers.
+		if (this._sessionTimers.enabled) {
+			if (Utils.isDecimal(options.sessionTimersExpires)) {
+				if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES) {
+					this._sessionTimers.defaultExpires = options.sessionTimersExpires;
+				} else {
+					this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
+				}
+			}
+		}
+
+		// Set event handlers.
+		for (const event in eventHandlers) {
+			if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+				this.on(event, eventHandlers[event]);
+			}
+		}
+
+		// Session parameter initialization.
+		this._from_tag = Utils.newTag();
+
+		// Set anonymous property.
+		const anonymous = options.anonymous || false;
+
+		const requestParams = { from_tag: this._from_tag };
+
+		this._contact = this._ua.contact.toString({
+			anonymous,
+			outbound: true,
+		});
+
+		if (anonymous) {
+			requestParams.from_display_name = 'Anonymous';
+			requestParams.from_uri = new URI('sip', 'anonymous', 'anonymous.invalid');
+
+			extraHeaders.push(
+				`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`
+			);
+			extraHeaders.push('Privacy: id');
+		} else if (options.fromUserName) {
+			requestParams.from_uri = new URI(
+				'sip',
+				options.fromUserName,
+				this._ua.configuration.uri.host
+			);
+
+			extraHeaders.push(
+				`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`
+			);
+		}
+
+		if (options.fromDisplayName) {
+			requestParams.from_display_name = options.fromDisplayName;
+		}
+
+		extraHeaders.push(`Contact: ${this._contact}`);
+		extraHeaders.push('Content-Type: application/sdp');
+		if (this._sessionTimers.enabled) {
+			extraHeaders.push(
+				`Session-Expires: ${this._sessionTimers.defaultExpires}${this._ua.configuration.session_timers_force_refresher ? ';refresher=uac' : ''}`
+			);
+		}
+
+		this._request = new SIPMessage.InitialOutgoingInviteRequest(
+			target,
+			this._ua,
+			requestParams,
+			extraHeaders
+		);
+
+		this._id = this._request.call_id + this._from_tag;
+
+		// Create a new RTCPeerConnection instance.
+		this._createRTCConnection(pcConfig, rtcConstraints);
+
+		// Set internal properties.
+		this._direction = 'outgoing';
+		this._local_identity = this._request.from;
+		this._remote_identity = this._request.to;
+
+		// User explicitly provided a newRTCSession callback for this session.
+		if (initCallback) {
+			initCallback(this);
+		}
+
+		this._newRTCSession('local', this._request);
+
+		this._sendInitialRequest(
+			mediaConstraints,
+			rtcOfferConstraints,
+			mediaStream
+		);
+	}
+
+	init_incoming(request, initCallback) {
+		logger.debug('init_incoming()');
+
+		let expires;
+		const contentType = request.hasHeader('Content-Type')
+			? request.getHeader('Content-Type').toLowerCase()
+			: undefined;
+
+		// Check body and content type.
+		if (request.body && contentType !== 'application/sdp') {
+			request.reply(415);
+
+			return;
+		}
+
+		// Session parameter initialization.
+		this._status = C.STATUS_INVITE_RECEIVED;
+		this._from_tag = request.from_tag;
+		this._id = request.call_id + this._from_tag;
+		this._request = request;
+		this._contact = this._ua.contact.toString();
+
+		// Get the Expires header value if exists.
+		if (request.hasHeader('expires')) {
+			expires = request.getHeader('expires') * 1000;
+		}
+
+		/* Set the to_tag before
+		 * replying a response code that will create a dialog.
+		 */
+		request.to_tag = Utils.newTag();
+
+		// An error on dialog creation will fire 'failed' event.
+		if (!this._createDialog(request, 'UAS', true)) {
+			request.reply(500, 'Missing Contact header field');
+
+			return;
+		}
+
+		if (request.body) {
+			this._late_sdp = false;
+		} else {
+			this._late_sdp = true;
+		}
+
+		this._status = C.STATUS_WAITING_FOR_ANSWER;
+
+		// Set userNoAnswerTimer.
+		this._timers.userNoAnswerTimer = setTimeout(() => {
+			request.reply(408);
+			this._failed('local', null, JsSIP_C.causes.NO_ANSWER);
+		}, this._ua.configuration.no_answer_timeout);
+
+		/* Set expiresTimer
+		 * RFC3261 13.3.1
+		 */
+		if (expires) {
+			this._timers.expiresTimer = setTimeout(() => {
+				if (this._status === C.STATUS_WAITING_FOR_ANSWER) {
+					request.reply(487);
+					this._failed('system', null, JsSIP_C.causes.EXPIRES);
+				}
+			}, expires);
+		}
+
+		// Set internal properties.
+		this._direction = 'incoming';
+		this._local_identity = request.to;
+		this._remote_identity = request.from;
+
+		// A init callback was specifically defined.
+		if (initCallback) {
+			initCallback(this);
+		}
+
+		// Fire 'newRTCSession' event.
+		this._newRTCSession('remote', request);
+
+		// The user may have rejected the call in the 'newRTCSession' event.
+		if (this._status === C.STATUS_TERMINATED) {
+			return;
+		}
+
+		// Reply 180.
+		request.reply(180, null, [`Contact: ${this._contact}`]);
+
+		// Fire 'progress' event.
+		// TODO: Document that 'response' field in 'progress' event is null for incoming calls.
+		this._progress('local', null);
+	}
+
+	/**
+	 * Answer the call.
+	 */
+	answer(options = {}) {
+		logger.debug('answer()');
+
+		const request = this._request;
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const mediaConstraints = Utils.cloneObject(options.mediaConstraints);
+		const mediaStream = options.mediaStream || null;
+		const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
+		const rtcConstraints = options.rtcConstraints || null;
+		const rtcAnswerConstraints = options.rtcAnswerConstraints || null;
+		const rtcOfferConstraints = Utils.cloneObject(options.rtcOfferConstraints);
+
+		let tracks;
+		let peerHasAudioLine = false;
+		let peerHasVideoLine = false;
+		let peerOffersFullAudio = false;
+		let peerOffersFullVideo = false;
+
+		this._rtcAnswerConstraints = rtcAnswerConstraints;
+		this._rtcOfferConstraints = options.rtcOfferConstraints || null;
+
+		this._data = options.data || this._data;
+
+		// Check Session Direction and Status.
+		if (this._direction !== 'incoming') {
+			throw new Exceptions.NotSupportedError(
+				'"answer" not supported for outgoing RTCSession'
+			);
+		}
+
+		// Check Session status.
+		if (this._status !== C.STATUS_WAITING_FOR_ANSWER) {
+			throw new Exceptions.InvalidStateError(this._status);
+		}
+
+		// Session Timers.
+		if (this._sessionTimers.enabled) {
+			if (Utils.isDecimal(options.sessionTimersExpires)) {
+				if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES) {
+					this._sessionTimers.defaultExpires = options.sessionTimersExpires;
+				} else {
+					this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
+				}
+			}
+		}
+
+		this._status = C.STATUS_ANSWERED;
+
+		// An error on dialog creation will fire 'failed' event.
+		if (!this._createDialog(request, 'UAS')) {
+			request.reply(500, 'Error creating dialog');
+
+			return;
+		}
+
+		clearTimeout(this._timers.userNoAnswerTimer);
+
+		extraHeaders.unshift(`Contact: ${this._contact}`);
+
+		// Determine incoming media from incoming SDP offer (if any).
+		const sdp = request.parseSDP();
+
+		// Make sure sdp.media is an array, not the case if there is only one media.
+		if (!Array.isArray(sdp.media)) {
+			sdp.media = [sdp.media];
+		}
+
+		// Go through all medias in SDP to find offered capabilities to answer with.
+		for (const m of sdp.media) {
+			if (m.type === 'audio') {
+				peerHasAudioLine = true;
+				if (!m.direction || m.direction === 'sendrecv') {
+					peerOffersFullAudio = true;
+				}
+			}
+			if (m.type === 'video') {
+				peerHasVideoLine = true;
+				if (!m.direction || m.direction === 'sendrecv') {
+					peerOffersFullVideo = true;
+				}
+			}
+		}
+
+		// Remove audio from mediaStream if suggested by mediaConstraints.
+		if (mediaStream && mediaConstraints.audio === false) {
+			tracks = mediaStream.getAudioTracks();
+			for (const track of tracks) {
+				mediaStream.removeTrack(track);
+			}
+		}
+
+		// Remove video from mediaStream if suggested by mediaConstraints.
+		if (mediaStream && mediaConstraints.video === false) {
+			tracks = mediaStream.getVideoTracks();
+			for (const track of tracks) {
+				mediaStream.removeTrack(track);
+			}
+		}
+
+		// Set audio constraints based on incoming stream if not supplied.
+		if (!mediaStream && mediaConstraints.audio === undefined) {
+			mediaConstraints.audio = peerOffersFullAudio;
+		}
+
+		// Set video constraints based on incoming stream if not supplied.
+		if (!mediaStream && mediaConstraints.video === undefined) {
+			mediaConstraints.video = peerOffersFullVideo;
+		}
+
+		// Don't ask for audio if the incoming offer has no audio section.
+		if (
+			!mediaStream &&
+			!peerHasAudioLine &&
+			!rtcOfferConstraints.offerToReceiveAudio
+		) {
+			mediaConstraints.audio = false;
+		}
+
+		// Don't ask for video if the incoming offer has no video section.
+		if (
+			!mediaStream &&
+			!peerHasVideoLine &&
+			!rtcOfferConstraints.offerToReceiveVideo
+		) {
+			mediaConstraints.video = false;
+		}
+
+		// Create a new RTCPeerConnection instance.
+		// TODO: This may throw an error, should react.
+		this._createRTCConnection(pcConfig, rtcConstraints);
+
+		Promise.resolve()
+			// Handle local MediaStream.
+			.then(() => {
+				// A local MediaStream is given, use it.
+				if (mediaStream) {
+					return mediaStream;
+				}
+
+				// Audio and/or video requested, prompt getUserMedia.
+				else if (mediaConstraints.audio || mediaConstraints.video) {
+					this._localMediaStreamLocallyGenerated = true;
+
+					return navigator.mediaDevices
+						.getUserMedia(mediaConstraints)
+						.catch(error => {
+							if (this._status === C.STATUS_TERMINATED) {
+								throw new Error('terminated');
+							}
+
+							request.reply(480);
+							this._failed(
+								'local',
+								null,
+								JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS
+							);
+
+							logger.warn('emit "getusermediafailed" [error:%o]', error);
+
+							this.emit('getusermediafailed', error);
+
+							throw new Error('getUserMedia() failed');
+						});
+				}
+			})
+			// Attach MediaStream to RTCPeerconnection.
+			.then(stream => {
+				if (this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				this._localMediaStream = stream;
+				if (stream) {
+					stream.getTracks().forEach(track => {
+						this._connection.addTrack(track, stream);
+					});
+				}
+			})
+			// Set remote description.
+			.then(() => {
+				if (this._late_sdp) {
+					return;
+				}
+
+				const e = { originator: 'remote', type: 'offer', sdp: request.body };
+
+				logger.debug('emit "sdp"');
+				this.emit('sdp', e);
+
+				const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
+
+				this._connectionPromiseQueue = this._connectionPromiseQueue
+					.then(() => this._connection.setRemoteDescription(offer))
+					.catch(error => {
+						request.reply(488);
+
+						this._failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
+
+						logger.warn(
+							'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+							error
+						);
+
+						this.emit('peerconnection:setremotedescriptionfailed', error);
+
+						throw new Error('peerconnection.setRemoteDescription() failed');
+					});
+
+				return this._connectionPromiseQueue;
+			})
+			// Create local description.
+			.then(() => {
+				if (this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				// TODO: Is this event already useful?
+				this._connecting(request);
+
+				if (!this._late_sdp) {
+					return this._createLocalDescription(
+						'answer',
+						rtcAnswerConstraints
+					).catch(() => {
+						request.reply(500);
+
+						throw new Error('_createLocalDescription() failed');
+					});
+				} else {
+					return this._createLocalDescription(
+						'offer',
+						this._rtcOfferConstraints
+					).catch(() => {
+						request.reply(500);
+
+						throw new Error('_createLocalDescription() failed');
+					});
+				}
+			})
+			// Send reply.
+			.then(desc => {
+				if (this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				this._handleSessionTimersInIncomingRequest(request, extraHeaders);
+
+				request.reply(
+					200,
+					null,
+					extraHeaders,
+					desc,
+					() => {
+						this._status = C.STATUS_WAITING_FOR_ACK;
+
+						this._setInvite2xxTimer(request, desc);
+						this._setACKTimer();
+						this._accepted('local');
+					},
+					() => {
+						this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
+					}
+				);
+			})
+			.catch(error => {
+				if (this._status === C.STATUS_TERMINATED) {
+					return;
+				}
+
+				logger.warn(`answer() failed: ${error.message}`);
+
+				this._failed('system', error.message, JsSIP_C.causes.INTERNAL_ERROR);
+			});
+	}
+
+	/**
+	 * Terminate the call.
+	 */
+	terminate(options = {}) {
+		logger.debug('terminate()');
+
+		const cause = options.cause || JsSIP_C.causes.BYE;
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const body = options.body;
+
+		let cancel_reason;
+		let status_code = options.status_code;
+		let reason_phrase = options.reason_phrase;
+
+		// Check Session Status.
+		if (this._status === C.STATUS_TERMINATED) {
+			throw new Exceptions.InvalidStateError(this._status);
+		}
+
+		switch (this._status) {
+			// - UAC -
+			case C.STATUS_NULL:
+			case C.STATUS_INVITE_SENT:
+			case C.STATUS_1XX_RECEIVED: {
+				logger.debug('canceling session');
+
+				if (status_code && (status_code < 200 || status_code >= 700)) {
+					throw new TypeError(`Invalid status_code: ${status_code}`);
+				} else if (status_code) {
+					reason_phrase =
+						reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
+					cancel_reason = `SIP ;cause=${status_code} ;text="${reason_phrase}"`;
+				}
+
+				// Check Session Status.
+				if (
+					this._status === C.STATUS_NULL ||
+					this._status === C.STATUS_INVITE_SENT
+				) {
+					this._is_canceled = true;
+					this._cancel_reason = cancel_reason;
+				} else if (this._status === C.STATUS_1XX_RECEIVED) {
+					this._request.cancel(cancel_reason);
+				}
+
+				this._status = C.STATUS_CANCELED;
+
+				this._failed('local', null, JsSIP_C.causes.CANCELED);
+				break;
+			}
+
+			// - UAS -
+			case C.STATUS_WAITING_FOR_ANSWER:
+			case C.STATUS_ANSWERED: {
+				logger.debug('rejecting session');
+
+				status_code = status_code || 480;
+
+				if (status_code < 300 || status_code >= 700) {
+					throw new TypeError(`Invalid status_code: ${status_code}`);
+				}
+
+				this._request.reply(status_code, reason_phrase, extraHeaders, body);
+				this._failed('local', null, JsSIP_C.causes.REJECTED);
+				break;
+			}
+
+			case C.STATUS_WAITING_FOR_ACK:
+			case C.STATUS_CONFIRMED: {
+				logger.debug('terminating session');
+
+				reason_phrase =
+					options.reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
+
+				if (status_code && (status_code < 200 || status_code >= 700)) {
+					throw new TypeError(`Invalid status_code: ${status_code}`);
+				} else if (status_code) {
+					extraHeaders.push(
+						`Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`
+					);
+				}
+
+				/* RFC 3261 section 15 (Terminating a session):
+				 *
+				 * "...the callee's UA MUST NOT send a BYE on a confirmed dialog
+				 * until it has received an ACK for its 2xx response or until the server
+				 * transaction times out."
+				 */
+				if (
+					this._status === C.STATUS_WAITING_FOR_ACK &&
+					this._direction === 'incoming' &&
+					this._request.server_transaction.state !==
+						Transactions.C.STATUS_TERMINATED
+				) {
+					// Save the dialog for later restoration.
+					const dialog = this._dialog;
+
+					// Send the BYE as soon as the ACK is received...
+					this.receiveRequest = ({ method }) => {
+						if (method === JsSIP_C.ACK) {
+							this.sendRequest(JsSIP_C.BYE, {
+								extraHeaders,
+								body,
+							});
+							dialog.terminate();
+						}
+					};
+
+					// .., or when the INVITE transaction times out
+					this._request.server_transaction.on('stateChanged', () => {
+						if (
+							this._request.server_transaction.state ===
+							Transactions.C.STATUS_TERMINATED
+						) {
+							this.sendRequest(JsSIP_C.BYE, {
+								extraHeaders,
+								body,
+							});
+							dialog.terminate();
+						}
+					});
+
+					this._ended('local', null, cause);
+
+					// Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-).
+					this._dialog = dialog;
+
+					// Restore the dialog into 'ua' so the ACK can reach 'this' session.
+					this._ua.newDialog(dialog);
+				} else {
+					this.sendRequest(JsSIP_C.BYE, {
+						extraHeaders,
+						body,
+					});
+
+					this._ended('local', null, cause);
+				}
+			}
+		}
+	}
+
+	sendDTMF(tones, options = {}) {
+		logger.debug('sendDTMF() | tones: %s', tones);
+
+		let duration = options.duration || null;
+		let interToneGap = options.interToneGap || null;
+		const transportType = options.transportType || JsSIP_C.DTMF_TRANSPORT.INFO;
+
+		if (tones === undefined) {
+			throw new TypeError('Not enough arguments');
+		}
+
+		// Check Session Status.
+		if (
+			this._status !== C.STATUS_CONFIRMED &&
+			this._status !== C.STATUS_WAITING_FOR_ACK &&
+			this._status !== C.STATUS_1XX_RECEIVED
+		) {
+			throw new Exceptions.InvalidStateError(this._status);
+		}
+
+		// Check Transport type.
+		if (
+			transportType !== JsSIP_C.DTMF_TRANSPORT.INFO &&
+			transportType !== JsSIP_C.DTMF_TRANSPORT.RFC2833
+		) {
+			throw new TypeError(`invalid transportType: ${transportType}`);
+		}
+
+		// Convert to string.
+		if (typeof tones === 'number') {
+			tones = tones.toString();
+		}
+
+		// Check tones.
+		if (
+			!tones ||
+			typeof tones !== 'string' ||
+			!tones.match(/^[0-9A-DR#*,]+$/i)
+		) {
+			throw new TypeError(`Invalid tones: ${tones}`);
+		}
+
+		// Check duration.
+		if (duration && !Utils.isDecimal(duration)) {
+			throw new TypeError(`Invalid tone duration: ${duration}`);
+		} else if (!duration) {
+			duration = RTCSession_DTMF.C.DEFAULT_DURATION;
+		} else if (duration < RTCSession_DTMF.C.MIN_DURATION) {
+			logger.debug(
+				`"duration" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_DURATION} milliseconds`
+			);
+			duration = RTCSession_DTMF.C.MIN_DURATION;
+		} else if (duration > RTCSession_DTMF.C.MAX_DURATION) {
+			logger.debug(
+				`"duration" value is greater than the maximum allowed, setting it to ${RTCSession_DTMF.C.MAX_DURATION} milliseconds`
+			);
+			duration = RTCSession_DTMF.C.MAX_DURATION;
+		} else {
+			duration = Math.abs(duration);
+		}
+		options.duration = duration;
+
+		// Check interToneGap.
+		if (interToneGap && !Utils.isDecimal(interToneGap)) {
+			throw new TypeError(`Invalid interToneGap: ${interToneGap}`);
+		} else if (!interToneGap) {
+			interToneGap = RTCSession_DTMF.C.DEFAULT_INTER_TONE_GAP;
+		} else if (interToneGap < RTCSession_DTMF.C.MIN_INTER_TONE_GAP) {
+			logger.debug(
+				`"interToneGap" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_INTER_TONE_GAP} milliseconds`
+			);
+			interToneGap = RTCSession_DTMF.C.MIN_INTER_TONE_GAP;
+		} else {
+			interToneGap = Math.abs(interToneGap);
+		}
+
+		// RFC2833. Let RTCDTMFSender enqueue the DTMFs.
+		if (transportType === JsSIP_C.DTMF_TRANSPORT.RFC2833) {
+			// Send DTMF in current audio RTP stream.
+			const sender = this._getDTMFRTPSender();
+
+			if (sender) {
+				// Add remaining buffered tones.
+				tones = sender.toneBuffer + tones;
+				// Insert tones.
+				sender.insertDTMF(tones, duration, interToneGap);
+			}
+
+			return;
+		}
+
+		if (this._tones) {
+			// Tones are already queued, just add to the queue.
+			this._tones += tones;
+
+			return;
+		}
+
+		this._tones = tones;
+
+		// Send the first tone.
+		_sendDTMF.call(this);
+
+		function _sendDTMF() {
+			let timeout;
+
+			if (this._status === C.STATUS_TERMINATED || !this._tones) {
+				// Stop sending DTMF.
+				this._tones = null;
+
+				return;
+			}
+
+			// Retrieve the next tone.
+			const tone = this._tones[0];
+
+			// Remove the tone from this._tones.
+			this._tones = this._tones.substring(1);
+
+			if (tone === ',') {
+				timeout = 2000;
+			} else {
+				// Send DTMF via SIP INFO messages.
+				const dtmf = new RTCSession_DTMF(this);
+
+				options.eventHandlers = {
+					onFailed: () => {
+						this._tones = null;
+					},
+				};
+				dtmf.send(tone, options);
+				timeout = duration + interToneGap;
+			}
+
+			// Set timeout for the next tone.
+			setTimeout(_sendDTMF.bind(this), timeout);
+		}
+	}
+
+	sendInfo(contentType, body, options = {}) {
+		logger.debug('sendInfo()');
+
+		// Check Session Status.
+		if (
+			this._status !== C.STATUS_CONFIRMED &&
+			this._status !== C.STATUS_WAITING_FOR_ACK &&
+			this._status !== C.STATUS_1XX_RECEIVED
+		) {
+			throw new Exceptions.InvalidStateError(this._status);
+		}
+
+		const info = new RTCSession_Info(this);
+
+		info.send(contentType, body, options);
+	}
+
+	/**
+	 * Mute
+	 */
+	mute(options = { audio: true, video: false }) {
+		logger.debug('mute()');
+
+		let audioMuted = false,
+			videoMuted = false;
+
+		if (this._audioMuted === false && options.audio) {
+			audioMuted = true;
+			this._audioMuted = true;
+			this._toggleMuteAudio(true);
+		}
+
+		if (this._videoMuted === false && options.video) {
+			videoMuted = true;
+			this._videoMuted = true;
+			this._toggleMuteVideo(true);
+		}
+
+		if (audioMuted === true || videoMuted === true) {
+			this._onmute({
+				audio: audioMuted,
+				video: videoMuted,
+			});
+		}
+	}
+
+	/**
+	 * Unmute
+	 */
+	unmute(options = { audio: true, video: true }) {
+		logger.debug('unmute()');
+
+		let audioUnMuted = false,
+			videoUnMuted = false;
+
+		if (this._audioMuted === true && options.audio) {
+			audioUnMuted = true;
+			this._audioMuted = false;
+
+			if (this._localHold === false) {
+				this._toggleMuteAudio(false);
+			}
+		}
+
+		if (this._videoMuted === true && options.video) {
+			videoUnMuted = true;
+			this._videoMuted = false;
+
+			if (this._localHold === false) {
+				this._toggleMuteVideo(false);
+			}
+		}
+
+		if (audioUnMuted === true || videoUnMuted === true) {
+			this._onunmute({
+				audio: audioUnMuted,
+				video: videoUnMuted,
+			});
+		}
+	}
+
+	/**
+	 * Hold
+	 */
+	hold(options = {}, done) {
+		logger.debug('hold()');
+
+		if (
+			this._status !== C.STATUS_WAITING_FOR_ACK &&
+			this._status !== C.STATUS_CONFIRMED
+		) {
+			return false;
+		}
+
+		if (this._localHold === true) {
+			return false;
+		}
+
+		if (!this.isReadyToReOffer()) {
+			return false;
+		}
+
+		this._localHold = true;
+		this._onhold('local');
+
+		const eventHandlers = {
+			succeeded: () => {
+				if (done) {
+					done();
+				}
+			},
+			failed: () => {
+				this.terminate({
+					cause: JsSIP_C.causes.WEBRTC_ERROR,
+					status_code: 500,
+					reason_phrase: 'Hold Failed',
+				});
+			},
+		};
+
+		if (options.useUpdate) {
+			this._sendUpdate({
+				sdpOffer: true,
+				eventHandlers,
+				extraHeaders: options.extraHeaders,
+			});
+		} else {
+			this._sendReinvite({
+				eventHandlers,
+				extraHeaders: options.extraHeaders,
+			});
+		}
+
+		return true;
+	}
+
+	unhold(options = {}, done) {
+		logger.debug('unhold()');
+
+		if (
+			this._status !== C.STATUS_WAITING_FOR_ACK &&
+			this._status !== C.STATUS_CONFIRMED
+		) {
+			return false;
+		}
+
+		if (this._localHold === false) {
+			return false;
+		}
+
+		if (!this.isReadyToReOffer()) {
+			return false;
+		}
+
+		this._localHold = false;
+		this._onunhold('local');
+
+		const eventHandlers = {
+			succeeded: () => {
+				if (done) {
+					done();
+				}
+			},
+			failed: () => {
+				this.terminate({
+					cause: JsSIP_C.causes.WEBRTC_ERROR,
+					status_code: 500,
+					reason_phrase: 'Unhold Failed',
+				});
+			},
+		};
+
+		if (options.useUpdate) {
+			this._sendUpdate({
+				sdpOffer: true,
+				eventHandlers,
+				extraHeaders: options.extraHeaders,
+			});
+		} else {
+			this._sendReinvite({
+				eventHandlers,
+				extraHeaders: options.extraHeaders,
+			});
+		}
+
+		return true;
+	}
+
+	renegotiate(options = {}, done) {
+		logger.debug('renegotiate()');
+
+		const rtcOfferConstraints = options.rtcOfferConstraints || null;
+
+		if (
+			this._status !== C.STATUS_WAITING_FOR_ACK &&
+			this._status !== C.STATUS_CONFIRMED
+		) {
+			return false;
+		}
+
+		if (!this.isReadyToReOffer()) {
+			return false;
+		}
+
+		const eventHandlers = {
+			succeeded: () => {
+				if (done) {
+					done();
+				}
+			},
+			failed: () => {
+				this.terminate({
+					cause: JsSIP_C.causes.WEBRTC_ERROR,
+					status_code: 500,
+					reason_phrase: 'Media Renegotiation Failed',
+				});
+			},
+		};
+
+		this._setLocalMediaStatus();
+
+		if (options.useUpdate) {
+			this._sendUpdate({
+				sdpOffer: true,
+				eventHandlers,
+				rtcOfferConstraints,
+				extraHeaders: options.extraHeaders,
+			});
+		} else {
+			this._sendReinvite({
+				eventHandlers,
+				rtcOfferConstraints,
+				extraHeaders: options.extraHeaders,
+			});
+		}
+
+		return true;
+	}
+
+	/**
+	 * Refer
+	 */
+	refer(target, options) {
+		logger.debug('refer()');
+
+		const originalTarget = target;
+
+		if (
+			this._status !== C.STATUS_WAITING_FOR_ACK &&
+			this._status !== C.STATUS_CONFIRMED
+		) {
+			return false;
+		}
+
+		// Check target validity.
+		target = this._ua.normalizeTarget(target);
+		if (!target) {
+			throw new TypeError(`Invalid target: ${originalTarget}`);
+		}
+
+		const referSubscriber = new RTCSession_ReferSubscriber(this);
+
+		referSubscriber.sendRefer(target, options);
+
+		// Store in the map.
+		const id = referSubscriber.id;
+
+		this._referSubscribers[id] = referSubscriber;
+
+		// Listen for ending events so we can remove it from the map.
+		referSubscriber.on('requestFailed', () => {
+			delete this._referSubscribers[id];
+		});
+		referSubscriber.on('accepted', () => {
+			delete this._referSubscribers[id];
+		});
+		referSubscriber.on('failed', () => {
+			delete this._referSubscribers[id];
+		});
+
+		return referSubscriber;
+	}
+
+	/**
+	 * Send a generic in-dialog Request
+	 */
+	sendRequest(method, options) {
+		logger.debug('sendRequest()');
+
+		if (this._dialog) {
+			return this._dialog.sendRequest(method, options);
+		} else {
+			const dialogsArray = Object.values(this._earlyDialogs);
+
+			if (dialogsArray.length > 0) {
+				return dialogsArray[0].sendRequest(method, options);
+			}
+
+			logger.warn('sendRequest() | no valid early dialog found');
+
+			return;
+		}
+	}
+
+	/**
+	 * In dialog Request Reception
+	 */
+	receiveRequest(request) {
+		logger.debug('receiveRequest()');
+
+		if (request.method === JsSIP_C.CANCEL) {
+			/* RFC3261 15 States that a UAS may have accepted an invitation while a CANCEL
+			 * was in progress and that the UAC MAY continue with the session established by
+			 * any 2xx response, or MAY terminate with BYE. JsSIP does continue with the
+			 * established session. So the CANCEL is processed only if the session is not yet
+			 * established.
+			 */
+
+			/*
+			 * Terminate the whole session in case the user didn't accept (or yet send the answer)
+			 * nor reject the request opening the session.
+			 */
+			if (
+				this._status === C.STATUS_WAITING_FOR_ANSWER ||
+				this._status === C.STATUS_ANSWERED
+			) {
+				this._status = C.STATUS_CANCELED;
+				this._request.reply(487);
+				this._failed('remote', request, JsSIP_C.causes.CANCELED);
+			}
+		} else {
+			// Requests arriving here are in-dialog requests.
+			switch (request.method) {
+				case JsSIP_C.ACK: {
+					if (this._status !== C.STATUS_WAITING_FOR_ACK) {
+						return;
+					}
+
+					// Update signaling status.
+					this._status = C.STATUS_CONFIRMED;
+
+					clearTimeout(this._timers.ackTimer);
+					clearTimeout(this._timers.invite2xxTimer);
+
+					if (this._late_sdp) {
+						if (!request.body) {
+							this.terminate({
+								cause: JsSIP_C.causes.MISSING_SDP,
+								status_code: 400,
+							});
+							break;
+						}
+
+						const e = {
+							originator: 'remote',
+							type: 'answer',
+							sdp: request.body,
+						};
+
+						logger.debug('emit "sdp"');
+						this.emit('sdp', e);
+
+						const answer = new RTCSessionDescription({
+							type: 'answer',
+							sdp: e.sdp,
+						});
+
+						this._connectionPromiseQueue = this._connectionPromiseQueue
+							.then(() => this._connection.setRemoteDescription(answer))
+							.then(() => {
+								if (!this._is_confirmed) {
+									this._confirmed('remote', request);
+								}
+							})
+							.catch(error => {
+								this.terminate({
+									cause: JsSIP_C.causes.BAD_MEDIA_DESCRIPTION,
+									status_code: 488,
+								});
+
+								logger.warn(
+									'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+									error
+								);
+								this.emit('peerconnection:setremotedescriptionfailed', error);
+							});
+					} else if (!this._is_confirmed) {
+						this._confirmed('remote', request);
+					}
+
+					break;
+				}
+				case JsSIP_C.BYE: {
+					if (
+						this._status === C.STATUS_CONFIRMED ||
+						this._status === C.STATUS_WAITING_FOR_ACK
+					) {
+						request.reply(200);
+						this._ended('remote', request, JsSIP_C.causes.BYE);
+					} else if (
+						this._status === C.STATUS_INVITE_RECEIVED ||
+						this._status === C.STATUS_WAITING_FOR_ANSWER
+					) {
+						request.reply(200);
+						this._request.reply(487, 'BYE Received');
+						this._ended('remote', request, JsSIP_C.causes.BYE);
+					} else {
+						request.reply(403, 'Wrong Status');
+					}
+					break;
+				}
+				case JsSIP_C.INVITE: {
+					if (this._status === C.STATUS_CONFIRMED) {
+						if (request.hasHeader('replaces')) {
+							this._receiveReplaces(request);
+						} else {
+							this._receiveReinvite(request);
+						}
+					} else {
+						request.reply(403, 'Wrong Status');
+					}
+					break;
+				}
+				case JsSIP_C.INFO: {
+					if (
+						this._status === C.STATUS_1XX_RECEIVED ||
+						this._status === C.STATUS_WAITING_FOR_ANSWER ||
+						this._status === C.STATUS_ANSWERED ||
+						this._status === C.STATUS_WAITING_FOR_ACK ||
+						this._status === C.STATUS_CONFIRMED
+					) {
+						const contentType = request.hasHeader('Content-Type')
+							? request.getHeader('Content-Type').toLowerCase()
+							: undefined;
+
+						if (contentType && contentType.match(/^application\/dtmf-relay/i)) {
+							new RTCSession_DTMF(this).init_incoming(request);
+						} else if (contentType !== undefined) {
+							new RTCSession_Info(this).init_incoming(request);
+						} else {
+							request.reply(415);
+						}
+					} else {
+						request.reply(403, 'Wrong Status');
+					}
+					break;
+				}
+				case JsSIP_C.UPDATE: {
+					if (this._status === C.STATUS_CONFIRMED) {
+						this._receiveUpdate(request);
+					} else {
+						request.reply(403, 'Wrong Status');
+					}
+					break;
+				}
+				case JsSIP_C.REFER: {
+					if (this._status === C.STATUS_CONFIRMED) {
+						this._receiveRefer(request);
+					} else {
+						request.reply(403, 'Wrong Status');
+					}
+					break;
+				}
+				case JsSIP_C.NOTIFY: {
+					if (this._status === C.STATUS_CONFIRMED) {
+						this._receiveNotify(request);
+					} else {
+						request.reply(403, 'Wrong Status');
+					}
+					break;
+				}
+				default: {
+					request.reply(501);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Session Callbacks
+	 */
+
+	onTransportError() {
+		logger.warn('onTransportError()');
+
+		if (this._status !== C.STATUS_TERMINATED) {
+			this.terminate({
+				status_code: 500,
+				reason_phrase: JsSIP_C.causes.CONNECTION_ERROR,
+				cause: JsSIP_C.causes.CONNECTION_ERROR,
+			});
+		}
+	}
+
+	onRequestTimeout() {
+		logger.warn('onRequestTimeout()');
+
+		if (this._status !== C.STATUS_TERMINATED) {
+			this.terminate({
+				status_code: 408,
+				reason_phrase: JsSIP_C.causes.REQUEST_TIMEOUT,
+				cause: JsSIP_C.causes.REQUEST_TIMEOUT,
+			});
+		}
+	}
+
+	onDialogError() {
+		logger.warn('onDialogError()');
+
+		if (this._status !== C.STATUS_TERMINATED) {
+			this.terminate({
+				status_code: 500,
+				reason_phrase: JsSIP_C.causes.DIALOG_ERROR,
+				cause: JsSIP_C.causes.DIALOG_ERROR,
+			});
+		}
+	}
+
+	// Called from DTMF handler.
+	newDTMF(data) {
+		logger.debug('newDTMF()');
+
+		this.emit('newDTMF', data);
+	}
+
+	// Called from Info handler.
+	newInfo(data) {
+		logger.debug('newInfo()');
+
+		this.emit('newInfo', data);
+	}
+
+	/**
+	 * Check if RTCSession is ready for an outgoing re-INVITE or UPDATE with SDP.
+	 */
+	isReadyToReOffer() {
+		if (!this._rtcReady) {
+			logger.debug('isReadyToReOffer() | internal WebRTC status not ready');
+
+			return false;
+		}
+
+		// No established yet.
+		if (!this._dialog) {
+			logger.debug('isReadyToReOffer() | session not established yet');
+
+			return false;
+		}
+
+		// Another INVITE transaction is in progress.
+		if (
+			this._dialog.uac_pending_reply === true ||
+			this._dialog.uas_pending_reply === true
+		) {
+			logger.debug(
+				'isReadyToReOffer() | there is another INVITE/UPDATE transaction in progress'
+			);
+
+			return false;
+		}
+
+		return true;
+	}
+
+	_close() {
+		logger.debug('close()');
+
+		// Close local MediaStream if it was not given by the user.
+		if (this._localMediaStream && this._localMediaStreamLocallyGenerated) {
+			logger.debug('close() | closing local MediaStream');
+
+			Utils.closeMediaStream(this._localMediaStream);
+		}
+
+		if (this._status === C.STATUS_TERMINATED) {
+			return;
+		}
+
+		this._status = C.STATUS_TERMINATED;
+
+		// Terminate RTC.
+		if (this._connection) {
+			try {
+				this._connection.close();
+			} catch (error) {
+				logger.warn('close() | error closing the RTCPeerConnection: %o', error);
+			}
+		}
+
+		// Terminate signaling.
+
+		// Clear SIP timers.
+		for (const timer in this._timers) {
+			if (Object.prototype.hasOwnProperty.call(this._timers, timer)) {
+				clearTimeout(this._timers[timer]);
+			}
+		}
+
+		// Clear Session Timers.
+		clearTimeout(this._sessionTimers.timer);
+
+		// Terminate confirmed dialog.
+		if (this._dialog) {
+			this._dialog.terminate();
+			delete this._dialog;
+		}
+
+		// Terminate early dialogs.
+		for (const dialog in this._earlyDialogs) {
+			if (Object.prototype.hasOwnProperty.call(this._earlyDialogs, dialog)) {
+				this._earlyDialogs[dialog].terminate();
+				delete this._earlyDialogs[dialog];
+			}
+		}
+
+		// Terminate REFER subscribers.
+		for (const subscriber in this._referSubscribers) {
+			if (
+				Object.prototype.hasOwnProperty.call(this._referSubscribers, subscriber)
+			) {
+				delete this._referSubscribers[subscriber];
+			}
+		}
+
+		this._ua.destroyRTCSession(this);
+	}
+
+	/**
+	 * Private API.
+	 */
+
+	/**
+	 * RFC3261 13.3.1.4
+	 * Response retransmissions cannot be accomplished by transaction layer
+	 *  since it is destroyed when receiving the first 2xx answer
+	 */
+	_setInvite2xxTimer(request, body) {
+		let timeout = Timers.T1;
+
+		function invite2xxRetransmission() {
+			if (this._status !== C.STATUS_WAITING_FOR_ACK) {
+				return;
+			}
+
+			request.reply(200, null, [`Contact: ${this._contact}`], body);
+
+			if (timeout < Timers.T2) {
+				timeout = timeout * 2;
+				if (timeout > Timers.T2) {
+					timeout = Timers.T2;
+				}
+			}
+
+			this._timers.invite2xxTimer = setTimeout(
+				invite2xxRetransmission.bind(this),
+				timeout
+			);
+		}
+
+		this._timers.invite2xxTimer = setTimeout(
+			invite2xxRetransmission.bind(this),
+			timeout
+		);
+	}
+
+	/**
+	 * RFC3261 14.2
+	 * If a UAS generates a 2xx response and never receives an ACK,
+	 *  it SHOULD generate a BYE to terminate the dialog.
+	 */
+	_setACKTimer() {
+		this._timers.ackTimer = setTimeout(() => {
+			if (this._status === C.STATUS_WAITING_FOR_ACK) {
+				logger.debug('no ACK received, terminating the session');
+
+				clearTimeout(this._timers.invite2xxTimer);
+				this.sendRequest(JsSIP_C.BYE);
+				this._ended('remote', null, JsSIP_C.causes.NO_ACK);
+			}
+		}, Timers.TIMER_H);
+	}
+
+	_createRTCConnection(pcConfig, rtcConstraints) {
+		this._connection = new RTCPeerConnection(pcConfig, rtcConstraints);
+
+		this._connection.addEventListener('iceconnectionstatechange', () => {
+			const state = this._connection.iceConnectionState;
+
+			// TODO: Do more with different states.
+			if (state === 'failed') {
+				this.terminate({
+					cause: JsSIP_C.causes.RTP_TIMEOUT,
+					status_code: 408,
+					reason_phrase: JsSIP_C.causes.RTP_TIMEOUT,
+				});
+			}
+		});
+
+		logger.debug('emit "peerconnection"');
+
+		this.emit('peerconnection', {
+			peerconnection: this._connection,
+		});
+	}
+
+	_createLocalDescription(type, constraints) {
+		logger.debug('createLocalDescription()');
+
+		if (type !== 'offer' && type !== 'answer') {
+			throw new Error(`createLocalDescription() | invalid type "${type}"`);
+		}
+
+		const connection = this._connection;
+
+		this._rtcReady = false;
+
+		return (
+			Promise.resolve()
+				// Create Offer or Answer.
+				.then(() => {
+					if (type === 'offer') {
+						return connection.createOffer(constraints).catch(error => {
+							logger.warn(
+								'emit "peerconnection:createofferfailed" [error:%o]',
+								error
+							);
+
+							this.emit('peerconnection:createofferfailed', error);
+
+							return Promise.reject(error);
+						});
+					} else {
+						return connection.createAnswer(constraints).catch(error => {
+							logger.warn(
+								'emit "peerconnection:createanswerfailed" [error:%o]',
+								error
+							);
+
+							this.emit('peerconnection:createanswerfailed', error);
+
+							return Promise.reject(error);
+						});
+					}
+				})
+				// Set local description.
+				.then(desc => {
+					return connection.setLocalDescription(desc).catch(error => {
+						this._rtcReady = true;
+
+						logger.warn(
+							'emit "peerconnection:setlocaldescriptionfailed" [error:%o]',
+							error
+						);
+
+						this.emit('peerconnection:setlocaldescriptionfailed', error);
+
+						return Promise.reject(error);
+					});
+				})
+				.then(() => {
+					// Resolve right away if 'pc.iceGatheringState' is 'complete'.
+					/**
+					 * Resolve right away if:
+					 * - 'connection.iceGatheringState' is 'complete' and no 'iceRestart' constraint is set.
+					 * - 'connection.iceGatheringState' is 'gathering' and 'iceReady' is true.
+					 */
+					const iceRestart = constraints && constraints.iceRestart;
+
+					if (
+						(connection.iceGatheringState === 'complete' && !iceRestart) ||
+						(connection.iceGatheringState === 'gathering' && this._iceReady)
+					) {
+						this._rtcReady = true;
+
+						const e = {
+							originator: 'local',
+							type: type,
+							sdp: connection.localDescription.sdp,
+						};
+
+						logger.debug('emit "sdp"');
+
+						this.emit('sdp', e);
+
+						return Promise.resolve(e.sdp);
+					}
+
+					// Add 'pc.onicencandidate' event handler to resolve on last candidate.
+					return new Promise(resolve => {
+						let finished = false;
+						let iceCandidateListener;
+						let iceGatheringStateListener;
+
+						this._iceReady = false;
+
+						const ready = () => {
+							if (finished) {
+								return;
+							}
+
+							connection.removeEventListener(
+								'icecandidate',
+								iceCandidateListener
+							);
+							connection.removeEventListener(
+								'icegatheringstatechange',
+								iceGatheringStateListener
+							);
+
+							finished = true;
+							this._rtcReady = true;
+
+							// connection.iceGatheringState will still indicate 'gathering' and thus be blocking.
+							this._iceReady = true;
+
+							const e = {
+								originator: 'local',
+								type: type,
+								sdp: connection.localDescription.sdp,
+							};
+
+							logger.debug('emit "sdp"');
+
+							this.emit('sdp', e);
+
+							resolve(e.sdp);
+						};
+
+						connection.addEventListener(
+							'icecandidate',
+							(iceCandidateListener = event => {
+								const candidate = event.candidate;
+
+								if (candidate) {
+									this.emit('icecandidate', {
+										candidate,
+										ready,
+									});
+								} else {
+									ready();
+								}
+							})
+						);
+
+						connection.addEventListener(
+							'icegatheringstatechange',
+							(iceGatheringStateListener = () => {
+								if (connection.iceGatheringState === 'complete') {
+									ready();
+								}
+							})
+						);
+					});
+				})
+		);
+	}
+
+	/**
+	 * Dialog Management
+	 */
+	_createDialog(message, type, early) {
+		const local_tag = type === 'UAS' ? message.to_tag : message.from_tag;
+		const remote_tag = type === 'UAS' ? message.from_tag : message.to_tag;
+		const id = message.call_id + local_tag + remote_tag;
+
+		let early_dialog = this._earlyDialogs[id];
+
+		// Early Dialog.
+		if (early) {
+			if (early_dialog) {
+				return true;
+			} else {
+				early_dialog = new Dialog(this, message, type, Dialog.C.STATUS_EARLY);
+
+				// Dialog has been successfully created.
+				if (early_dialog.error) {
+					logger.debug(early_dialog.error);
+					this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
+
+					return false;
+				} else {
+					this._earlyDialogs[id] = early_dialog;
+
+					return true;
+				}
+			}
+		}
+
+		// Confirmed Dialog.
+		else {
+			this._from_tag = message.from_tag;
+			this._to_tag = message.to_tag;
+
+			// In case the dialog is in _early_ state, update it.
+			if (early_dialog) {
+				early_dialog.update(message, type);
+				this._dialog = early_dialog;
+				delete this._earlyDialogs[id];
+
+				return true;
+			}
+
+			// Otherwise, create a _confirmed_ dialog.
+			const dialog = new Dialog(this, message, type);
+
+			if (dialog.error) {
+				logger.debug(dialog.error);
+				this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
+
+				return false;
+			} else {
+				this._dialog = dialog;
+
+				return true;
+			}
+		}
+	}
+
+	/**
+	 * In dialog INVITE Reception
+	 */
+
+	_receiveReinvite(request) {
+		logger.debug('receiveReinvite()');
+
+		const contentType = request.hasHeader('Content-Type')
+			? request.getHeader('Content-Type').toLowerCase()
+			: undefined;
+		const data = {
+			request,
+			callback: undefined,
+			reject: reject.bind(this),
+		};
+
+		let rejected = false;
+
+		function reject(options = {}) {
+			rejected = true;
+
+			const status_code = options.status_code || 403;
+			const reason_phrase = options.reason_phrase || '';
+			const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+			if (this._status !== C.STATUS_CONFIRMED) {
+				return false;
+			}
+
+			if (status_code < 300 || status_code >= 700) {
+				throw new TypeError(`Invalid status_code: ${status_code}`);
+			}
+
+			request.reply(status_code, reason_phrase, extraHeaders);
+		}
+
+		// Emit 'reinvite'.
+		this.emit('reinvite', data);
+
+		if (rejected) {
+			return;
+		}
+
+		this._late_sdp = false;
+
+		// Request without SDP.
+		if (!request.body) {
+			this._late_sdp = true;
+			if (this._remoteHold) {
+				this._remoteHold = false;
+				this._onunhold('remote');
+			}
+			this._connectionPromiseQueue = this._connectionPromiseQueue
+				.then(() =>
+					this._createLocalDescription('offer', this._rtcOfferConstraints)
+				)
+				.then(sdp => {
+					sendAnswer.call(this, sdp);
+				})
+				.catch(() => {
+					request.reply(500);
+				});
+
+			return;
+		}
+
+		// Request with SDP.
+		if (contentType !== 'application/sdp') {
+			logger.debug('invalid Content-Type');
+			request.reply(415);
+
+			return;
+		}
+
+		this._processInDialogSdpOffer(request)
+			// Send answer.
+			.then(desc => {
+				if (this._status === C.STATUS_TERMINATED) {
+					return;
+				}
+
+				sendAnswer.call(this, desc);
+			})
+			.catch(error => {
+				logger.warn(error);
+			});
+
+		function sendAnswer(desc) {
+			const extraHeaders = [`Contact: ${this._contact}`];
+
+			this._handleSessionTimersInIncomingRequest(request, extraHeaders);
+
+			if (this._late_sdp) {
+				desc = this._mangleOffer(desc);
+			}
+
+			request.reply(200, null, extraHeaders, desc, () => {
+				this._status = C.STATUS_WAITING_FOR_ACK;
+				this._setInvite2xxTimer(request, desc);
+				this._setACKTimer();
+			});
+
+			// If callback is given execute it.
+			if (typeof data.callback === 'function') {
+				data.callback();
+			}
+		}
+	}
+
+	/**
+	 * In dialog UPDATE Reception
+	 */
+	_receiveUpdate(request) {
+		logger.debug('receiveUpdate()');
+
+		const contentType = request.hasHeader('Content-Type')
+			? request.getHeader('Content-Type').toLowerCase()
+			: undefined;
+		const data = {
+			request,
+			callback: undefined,
+			reject: reject.bind(this),
+		};
+
+		let rejected = false;
+
+		function reject(options = {}) {
+			rejected = true;
+
+			const status_code = options.status_code || 403;
+			const reason_phrase = options.reason_phrase || '';
+			const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+			if (this._status !== C.STATUS_CONFIRMED) {
+				return false;
+			}
+
+			if (status_code < 300 || status_code >= 700) {
+				throw new TypeError(`Invalid status_code: ${status_code}`);
+			}
+
+			request.reply(status_code, reason_phrase, extraHeaders);
+		}
+
+		// Emit 'update'.
+		this.emit('update', data);
+
+		if (rejected) {
+			return;
+		}
+
+		if (!request.body) {
+			sendAnswer.call(this, null);
+
+			return;
+		}
+
+		if (contentType !== 'application/sdp') {
+			logger.debug('invalid Content-Type');
+
+			request.reply(415);
+
+			return;
+		}
+
+		this._processInDialogSdpOffer(request)
+			// Send answer.
+			.then(desc => {
+				if (this._status === C.STATUS_TERMINATED) {
+					return;
+				}
+
+				sendAnswer.call(this, desc);
+			})
+			.catch(error => {
+				logger.warn(error);
+			});
+
+		function sendAnswer(desc) {
+			const extraHeaders = [`Contact: ${this._contact}`];
+
+			this._handleSessionTimersInIncomingRequest(request, extraHeaders);
+
+			request.reply(200, null, extraHeaders, desc);
+
+			// If callback is given execute it.
+			if (typeof data.callback === 'function') {
+				data.callback();
+			}
+		}
+	}
+
+	_processInDialogSdpOffer(request) {
+		logger.debug('_processInDialogSdpOffer()');
+
+		const sdp = request.parseSDP();
+
+		let hold = false;
+
+		for (const m of sdp.media) {
+			if (holdMediaTypes.indexOf(m.type) === -1) {
+				continue;
+			}
+
+			const direction = m.direction || sdp.direction || 'sendrecv';
+
+			if (direction === 'sendonly' || direction === 'inactive') {
+				hold = true;
+			}
+			// If at least one of the streams is active don't emit 'hold'.
+			else {
+				hold = false;
+				break;
+			}
+		}
+
+		const e = { originator: 'remote', type: 'offer', sdp: request.body };
+
+		logger.debug('emit "sdp"');
+		this.emit('sdp', e);
+
+		const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
+
+		this._connectionPromiseQueue = this._connectionPromiseQueue
+			// Set remote description.
+			.then(() => {
+				if (this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				return this._connection.setRemoteDescription(offer).catch(error => {
+					request.reply(488);
+					logger.warn(
+						'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+						error
+					);
+
+					this.emit('peerconnection:setremotedescriptionfailed', error);
+
+					throw error;
+				});
+			})
+			.then(() => {
+				if (this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				if (this._remoteHold === true && hold === false) {
+					this._remoteHold = false;
+					this._onunhold('remote');
+				} else if (this._remoteHold === false && hold === true) {
+					this._remoteHold = true;
+					this._onhold('remote');
+				}
+			})
+			// Create local description.
+			.then(() => {
+				if (this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				return this._createLocalDescription(
+					'answer',
+					this._rtcAnswerConstraints
+				).catch(error => {
+					request.reply(500);
+					logger.warn(
+						'emit "peerconnection:createtelocaldescriptionfailed" [error:%o]',
+						error
+					);
+
+					throw error;
+				});
+			})
+			.catch(error => {
+				logger.warn('_processInDialogSdpOffer() failed [error: %o]', error);
+			});
+
+		return this._connectionPromiseQueue;
+	}
+
+	/**
+	 * In dialog Refer Reception
+	 */
+	_receiveRefer(request) {
+		logger.debug('receiveRefer()');
+
+		if (!request.refer_to) {
+			logger.debug('no Refer-To header field present in REFER');
+			request.reply(400);
+
+			return;
+		}
+
+		if (request.refer_to.uri.scheme !== JsSIP_C.SIP) {
+			logger.debug('Refer-To header field points to a non-SIP URI scheme');
+			request.reply(416);
+
+			return;
+		}
+
+		// Reply before the transaction timer expires.
+		request.reply(202);
+
+		const notifier = new RTCSession_ReferNotifier(this, request.cseq);
+
+		logger.debug('emit "refer"');
+
+		// Emit 'refer'.
+		this.emit('refer', {
+			request,
+			accept: (initCallback, options) => {
+				accept.call(this, initCallback, options);
+			},
+			reject: () => {
+				reject.call(this);
+			},
+		});
+
+		function accept(initCallback, options = {}) {
+			initCallback = typeof initCallback === 'function' ? initCallback : null;
+
+			if (
+				this._status !== C.STATUS_WAITING_FOR_ACK &&
+				this._status !== C.STATUS_CONFIRMED
+			) {
+				return false;
+			}
+
+			const session = new RTCSession(this._ua);
+
+			session.on('progress', ({ response }) => {
+				notifier.notify(response.status_code, response.reason_phrase);
+			});
+
+			session.on('accepted', ({ response }) => {
+				notifier.notify(response.status_code, response.reason_phrase);
+			});
+
+			session.on('_failed', ({ message, cause }) => {
+				if (message) {
+					notifier.notify(message.status_code, message.reason_phrase);
+				} else {
+					notifier.notify(487, cause);
+				}
+			});
+
+			// Consider the Replaces header present in the Refer-To URI.
+			if (request.refer_to.uri.hasHeader('replaces')) {
+				const replaces = decodeURIComponent(
+					request.refer_to.uri.getHeader('replaces')
+				);
+
+				options.extraHeaders = Utils.cloneArray(options.extraHeaders);
+				options.extraHeaders.push(`Replaces: ${replaces}`);
+			}
+
+			session.connect(request.refer_to.uri.toAor(), options, initCallback);
+		}
+
+		function reject() {
+			notifier.notify(603);
+		}
+	}
+
+	/**
+	 * In dialog Notify Reception
+	 */
+	_receiveNotify(request) {
+		logger.debug('receiveNotify()');
+
+		if (!request.event) {
+			request.reply(400);
+		}
+
+		switch (request.event.event) {
+			case 'refer': {
+				let id;
+				let referSubscriber;
+
+				if (request.event.params && request.event.params.id) {
+					id = request.event.params.id;
+					referSubscriber = this._referSubscribers[id];
+				} else if (Object.keys(this._referSubscribers).length === 1) {
+					referSubscriber =
+						this._referSubscribers[Object.keys(this._referSubscribers)[0]];
+				} else {
+					request.reply(400, 'Missing event id parameter');
+
+					return;
+				}
+
+				if (!referSubscriber) {
+					request.reply(481, 'Subscription does not exist');
+
+					return;
+				}
+
+				referSubscriber.receiveNotify(request);
+				request.reply(200);
+
+				break;
+			}
+
+			default: {
+				request.reply(489);
+			}
+		}
+	}
+
+	/**
+	 * INVITE with Replaces Reception
+	 */
+	_receiveReplaces(request) {
+		logger.debug('receiveReplaces()');
+
+		function accept(initCallback) {
+			if (
+				this._status !== C.STATUS_WAITING_FOR_ACK &&
+				this._status !== C.STATUS_CONFIRMED
+			) {
+				return false;
+			}
+
+			const session = new RTCSession(this._ua);
+
+			// Terminate the current session when the new one is confirmed.
+			session.on('confirmed', () => {
+				this.terminate();
+			});
+
+			session.init_incoming(request, initCallback);
+		}
+
+		function reject() {
+			logger.debug('Replaced INVITE rejected by the user');
+			request.reply(486);
+		}
+
+		// Emit 'replace'.
+		this.emit('replaces', {
+			request,
+			accept: initCallback => {
+				accept.call(this, initCallback);
+			},
+			reject: () => {
+				reject.call(this);
+			},
+		});
+	}
+
+	/**
+	 * Initial Request Sender
+	 */
+	_sendInitialRequest(mediaConstraints, rtcOfferConstraints, mediaStream) {
+		const request_sender = new RequestSender(this._ua, this._request, {
+			onRequestTimeout: () => {
+				this.onRequestTimeout();
+			},
+			onTransportError: () => {
+				this.onTransportError();
+			},
+			// Update the request on authentication.
+			onAuthenticated: request => {
+				this._request = request;
+			},
+			onReceiveResponse: response => {
+				this._receiveInviteResponse(response);
+			},
+		});
+
+		// This Promise is resolved within the next iteration, so the app has now
+		// a chance to set events such as 'peerconnection' and 'connecting'.
+		Promise.resolve()
+			// Get a stream if required.
+			.then(() => {
+				// A stream is given, let the app set events such as 'peerconnection' and 'connecting'.
+				if (mediaStream) {
+					return mediaStream;
+				}
+				// Request for user media access.
+				else if (mediaConstraints.audio || mediaConstraints.video) {
+					this._localMediaStreamLocallyGenerated = true;
+
+					return navigator.mediaDevices
+						.getUserMedia(mediaConstraints)
+						.catch(error => {
+							if (this._status === C.STATUS_TERMINATED) {
+								throw new Error('terminated');
+							}
+
+							this._failed(
+								'local',
+								null,
+								JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS
+							);
+
+							logger.warn('emit "getusermediafailed" [error:%o]', error);
+
+							this.emit('getusermediafailed', error);
+
+							throw error;
+						});
+				}
+			})
+			.then(stream => {
+				if (this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				this._localMediaStream = stream;
+
+				if (stream) {
+					stream.getTracks().forEach(track => {
+						this._connection.addTrack(track, stream);
+					});
+				}
+
+				// TODO: should this be triggered here?
+				this._connecting(this._request);
+
+				return this._createLocalDescription('offer', rtcOfferConstraints).catch(
+					error => {
+						this._failed('local', null, JsSIP_C.causes.WEBRTC_ERROR);
+
+						throw error;
+					}
+				);
+			})
+			.then(desc => {
+				if (this._is_canceled || this._status === C.STATUS_TERMINATED) {
+					throw new Error('terminated');
+				}
+
+				this._request.body = desc;
+				this._status = C.STATUS_INVITE_SENT;
+
+				logger.debug('emit "sending" [request:%o]', this._request);
+
+				// Emit 'sending' so the app can mangle the body before the request is sent.
+				this.emit('sending', {
+					request: this._request,
+				});
+
+				request_sender.send();
+			})
+			.catch(error => {
+				if (this._status === C.STATUS_TERMINATED) {
+					return;
+				}
+
+				logger.warn(error);
+			});
+	}
+
+	/**
+	 * Get DTMF RTCRtpSender.
+	 */
+	_getDTMFRTPSender() {
+		const sender = this._connection.getSenders().find(rtpSender => {
+			return rtpSender.track && rtpSender.track.kind === 'audio';
+		});
+
+		if (!(sender && sender.dtmf)) {
+			logger.warn('sendDTMF() | no local audio track to send DTMF with');
+
+			return;
+		}
+
+		return sender.dtmf;
+	}
+
+	/**
+	 * Reception of Response for Initial INVITE
+	 */
+	_receiveInviteResponse(response) {
+		logger.debug('receiveInviteResponse()');
+
+		// Handle 2XX retransmissions and responses from forked requests.
+		if (
+			this._dialog &&
+			response.status_code >= 200 &&
+			response.status_code <= 299
+		) {
+			/*
+			 * If it is a retransmission from the endpoint that established
+			 * the dialog, send an ACK
+			 */
+			if (
+				this._dialog.id.call_id === response.call_id &&
+				this._dialog.id.local_tag === response.from_tag &&
+				this._dialog.id.remote_tag === response.to_tag
+			) {
+				this.sendRequest(JsSIP_C.ACK);
+
+				return;
+			}
+
+			// If not, send an ACK  and terminate.
+			else {
+				const dialog = new Dialog(this, response, 'UAC');
+
+				if (dialog.error !== undefined) {
+					logger.debug(dialog.error);
+
+					return;
+				}
+
+				this.sendRequest(JsSIP_C.ACK);
+				this.sendRequest(JsSIP_C.BYE);
+
+				return;
+			}
+		}
+
+		// Proceed to cancellation if the user requested.
+		if (this._is_canceled) {
+			if (response.status_code >= 100 && response.status_code < 200) {
+				this._request.cancel(this._cancel_reason);
+			} else if (response.status_code >= 200 && response.status_code < 299) {
+				this._acceptAndTerminate(response);
+			}
+
+			return;
+		}
+
+		if (
+			this._status !== C.STATUS_INVITE_SENT &&
+			this._status !== C.STATUS_1XX_RECEIVED
+		) {
+			return;
+		}
+
+		switch (true) {
+			case /^100$/.test(response.status_code): {
+				this._status = C.STATUS_1XX_RECEIVED;
+				break;
+			}
+			case /^1[0-9]{2}$/.test(response.status_code): {
+				// Do nothing with 1xx responses without To tag.
+				if (!response.to_tag) {
+					logger.debug('1xx response received without to tag');
+					break;
+				}
+
+				// Create Early Dialog if 1XX comes with contact.
+				if (response.hasHeader('contact')) {
+					// An error on dialog creation will fire 'failed' event.
+					if (!this._createDialog(response, 'UAC', true)) {
+						break;
+					}
+				}
+
+				this._status = C.STATUS_1XX_RECEIVED;
+
+				if (!response.body) {
+					this._progress('remote', response);
+					break;
+				}
+
+				const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+				logger.debug('emit "sdp"');
+				this.emit('sdp', e);
+
+				const answer = new RTCSessionDescription({
+					type: 'answer',
+					sdp: e.sdp,
+				});
+
+				this._connectionPromiseQueue = this._connectionPromiseQueue
+					.then(() => this._connection.setRemoteDescription(answer))
+					.then(() => this._progress('remote', response))
+					.catch(error => {
+						logger.warn(
+							'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+							error
+						);
+
+						this.emit('peerconnection:setremotedescriptionfailed', error);
+					});
+				break;
+			}
+
+			case /^2[0-9]{2}$/.test(response.status_code): {
+				this._status = C.STATUS_CONFIRMED;
+
+				if (!response.body) {
+					this._acceptAndTerminate(response, 400, JsSIP_C.causes.MISSING_SDP);
+					this._failed(
+						'remote',
+						response,
+						JsSIP_C.causes.BAD_MEDIA_DESCRIPTION
+					);
+					break;
+				}
+
+				// An error on dialog creation will fire 'failed' event.
+				if (!this._createDialog(response, 'UAC')) {
+					break;
+				}
+
+				const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+				logger.debug('emit "sdp"');
+				this.emit('sdp', e);
+
+				const answer = new RTCSessionDescription({
+					type: 'answer',
+					sdp: e.sdp,
+				});
+
+				this._connectionPromiseQueue = this._connectionPromiseQueue
+					.then(() => {
+						// Be ready for 200 with SDP after a 180/183 with SDP.
+						// We created a SDP 'answer' for it, so check the current signaling state.
+						if (this._connection.signalingState === 'stable') {
+							return this._connection
+								.createOffer(this._rtcOfferConstraints)
+								.then(offer => this._connection.setLocalDescription(offer))
+								.catch(error => {
+									this._acceptAndTerminate(response, 500, error.toString());
+									this._failed('local', response, JsSIP_C.causes.WEBRTC_ERROR);
+								});
+						}
+					})
+					.then(() => {
+						this._connection
+							.setRemoteDescription(answer)
+							.then(() => {
+								// Handle Session Timers.
+								this._handleSessionTimersInIncomingResponse(response);
+
+								this._accepted('remote', response);
+								this.sendRequest(JsSIP_C.ACK);
+								this._confirmed('local', null);
+							})
+							.catch(error => {
+								this._acceptAndTerminate(response, 488, 'Not Acceptable Here');
+								this._failed(
+									'remote',
+									response,
+									JsSIP_C.causes.BAD_MEDIA_DESCRIPTION
+								);
+
+								logger.warn(
+									'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+									error
+								);
+
+								this.emit('peerconnection:setremotedescriptionfailed', error);
+							});
+					});
+				break;
+			}
+
+			default: {
+				const cause = Utils.sipErrorCause(response.status_code);
+
+				this._failed('remote', response, cause);
+			}
+		}
+	}
+
+	/**
+	 * Send Re-INVITE
+	 */
+	_sendReinvite(options = {}) {
+		logger.debug('sendReinvite()');
+
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const eventHandlers = Utils.cloneObject(options.eventHandlers);
+		const rtcOfferConstraints =
+			options.rtcOfferConstraints || this._rtcOfferConstraints || null;
+
+		let succeeded = false;
+
+		extraHeaders.push(`Contact: ${this._contact}`);
+		extraHeaders.push('Content-Type: application/sdp');
+
+		// Session Timers.
+		if (this._sessionTimers.running) {
+			extraHeaders.push(
+				`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`
+			);
+		}
+
+		this._connectionPromiseQueue = this._connectionPromiseQueue
+			.then(() => this._createLocalDescription('offer', rtcOfferConstraints))
+			.then(sdp => {
+				sdp = this._mangleOffer(sdp);
+
+				const e = { originator: 'local', type: 'offer', sdp };
+
+				logger.debug('emit "sdp"');
+				this.emit('sdp', e);
+
+				this.sendRequest(JsSIP_C.INVITE, {
+					extraHeaders,
+					body: sdp,
+					eventHandlers: {
+						onSuccessResponse: response => {
+							onSucceeded.call(this, response);
+							succeeded = true;
+						},
+						onErrorResponse: response => {
+							onFailed.call(this, response);
+						},
+						onTransportError: () => {
+							this.onTransportError(); // Do nothing because session ends.
+						},
+						onRequestTimeout: () => {
+							this.onRequestTimeout(); // Do nothing because session ends.
+						},
+						onDialogError: () => {
+							this.onDialogError(); // Do nothing because session ends.
+						},
+					},
+				});
+			})
+			.catch(() => {
+				onFailed();
+			});
+
+		function onSucceeded(response) {
+			if (this._status === C.STATUS_TERMINATED) {
+				return;
+			}
+
+			this.sendRequest(JsSIP_C.ACK);
+
+			// If it is a 2XX retransmission exit now.
+			if (succeeded) {
+				return;
+			}
+
+			// Handle Session Timers.
+			this._handleSessionTimersInIncomingResponse(response);
+
+			// Must have SDP answer.
+			if (!response.body) {
+				onFailed.call(this);
+
+				return;
+			} else if (
+				!response.hasHeader('Content-Type') ||
+				response.getHeader('Content-Type').toLowerCase() !== 'application/sdp'
+			) {
+				onFailed.call(this);
+
+				return;
+			}
+
+			const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+			logger.debug('emit "sdp"');
+			this.emit('sdp', e);
+
+			const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
+
+			this._connectionPromiseQueue = this._connectionPromiseQueue
+				.then(() => this._connection.setRemoteDescription(answer))
+				.then(() => {
+					if (eventHandlers.succeeded) {
+						eventHandlers.succeeded(response);
+					}
+				})
+				.catch(error => {
+					onFailed.call(this);
+
+					logger.warn(
+						'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+						error
+					);
+
+					this.emit('peerconnection:setremotedescriptionfailed', error);
+				});
+		}
+
+		function onFailed(response) {
+			if (eventHandlers.failed) {
+				eventHandlers.failed(response);
+			}
+		}
+	}
+
+	/**
+	 * Send UPDATE
+	 */
+	_sendUpdate(options = {}) {
+		logger.debug('sendUpdate()');
+
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const eventHandlers = Utils.cloneObject(options.eventHandlers);
+		const rtcOfferConstraints =
+			options.rtcOfferConstraints || this._rtcOfferConstraints || null;
+		const sdpOffer = options.sdpOffer || false;
+
+		let succeeded = false;
+
+		extraHeaders.push(`Contact: ${this._contact}`);
+
+		// Session Timers.
+		if (this._sessionTimers.running) {
+			extraHeaders.push(
+				`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`
+			);
+		}
+
+		if (sdpOffer) {
+			extraHeaders.push('Content-Type: application/sdp');
+
+			this._connectionPromiseQueue = this._connectionPromiseQueue
+				.then(() => this._createLocalDescription('offer', rtcOfferConstraints))
+				.then(sdp => {
+					sdp = this._mangleOffer(sdp);
+
+					const e = { originator: 'local', type: 'offer', sdp };
+
+					logger.debug('emit "sdp"');
+					this.emit('sdp', e);
+
+					this.sendRequest(JsSIP_C.UPDATE, {
+						extraHeaders,
+						body: sdp,
+						eventHandlers: {
+							onSuccessResponse: response => {
+								onSucceeded.call(this, response);
+								succeeded = true;
+							},
+							onErrorResponse: response => {
+								onFailed.call(this, response);
+							},
+							onTransportError: () => {
+								this.onTransportError(); // Do nothing because session ends.
+							},
+							onRequestTimeout: () => {
+								this.onRequestTimeout(); // Do nothing because session ends.
+							},
+							onDialogError: () => {
+								this.onDialogError(); // Do nothing because session ends.
+							},
+						},
+					});
+				})
+				.catch(() => {
+					onFailed.call(this);
+				});
+		}
+
+		// No SDP.
+		else {
+			this.sendRequest(JsSIP_C.UPDATE, {
+				extraHeaders,
+				eventHandlers: {
+					onSuccessResponse: response => {
+						onSucceeded.call(this, response);
+					},
+					onErrorResponse: response => {
+						onFailed.call(this, response);
+					},
+					onTransportError: () => {
+						this.onTransportError(); // Do nothing because session ends.
+					},
+					onRequestTimeout: () => {
+						this.onRequestTimeout(); // Do nothing because session ends.
+					},
+					onDialogError: () => {
+						this.onDialogError(); // Do nothing because session ends.
+					},
+				},
+			});
+		}
+
+		function onSucceeded(response) {
+			if (this._status === C.STATUS_TERMINATED) {
+				return;
+			}
+
+			// If it is a 2XX retransmission exit now.
+			if (succeeded) {
+				return;
+			}
+
+			// Handle Session Timers.
+			this._handleSessionTimersInIncomingResponse(response);
+
+			// Must have SDP answer.
+			if (sdpOffer) {
+				if (!response.body) {
+					onFailed.call(this);
+
+					return;
+				} else if (
+					!response.hasHeader('Content-Type') ||
+					response.getHeader('Content-Type').toLowerCase() !== 'application/sdp'
+				) {
+					onFailed.call(this);
+
+					return;
+				}
+
+				const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+				logger.debug('emit "sdp"');
+				this.emit('sdp', e);
+
+				const answer = new RTCSessionDescription({
+					type: 'answer',
+					sdp: e.sdp,
+				});
+
+				this._connectionPromiseQueue = this._connectionPromiseQueue
+					.then(() => this._connection.setRemoteDescription(answer))
+					.then(() => {
+						if (eventHandlers.succeeded) {
+							eventHandlers.succeeded(response);
+						}
+					})
+					.catch(error => {
+						onFailed.call(this);
+
+						logger.warn(
+							'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+							error
+						);
+
+						this.emit('peerconnection:setremotedescriptionfailed', error);
+					});
+			}
+			// No SDP answer.
+			else if (eventHandlers.succeeded) {
+				eventHandlers.succeeded(response);
+			}
+		}
+
+		function onFailed(response) {
+			if (eventHandlers.failed) {
+				eventHandlers.failed(response);
+			}
+		}
+	}
+
+	_acceptAndTerminate(response, status_code, reason_phrase) {
+		logger.debug('acceptAndTerminate()');
+
+		const extraHeaders = [];
+
+		if (status_code) {
+			reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
+			extraHeaders.push(
+				`Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`
+			);
+		}
+
+		// An error on dialog creation will fire 'failed' event.
+		if (this._dialog || this._createDialog(response, 'UAC')) {
+			this.sendRequest(JsSIP_C.ACK);
+			this.sendRequest(JsSIP_C.BYE, {
+				extraHeaders,
+			});
+		}
+
+		// Update session status.
+		this._status = C.STATUS_TERMINATED;
+	}
+
+	/**
+	 * Correctly set the SDP direction attributes if the call is on local hold
+	 */
+	_mangleOffer(sdp) {
+		if (!this._localHold && !this._remoteHold) {
+			return sdp;
+		}
+
+		sdp = sdp_transform.parse(sdp);
+
+		// Local hold.
+		if (this._localHold && !this._remoteHold) {
+			logger.debug('mangleOffer() | me on hold, mangling offer');
+			for (const m of sdp.media) {
+				if (holdMediaTypes.indexOf(m.type) === -1) {
+					continue;
+				}
+				if (!m.direction) {
+					m.direction = 'sendonly';
+				} else if (m.direction === 'sendrecv') {
+					m.direction = 'sendonly';
+				} else if (m.direction === 'recvonly') {
+					m.direction = 'inactive';
+				}
+			}
+		}
+		// Local and remote hold.
+		else if (this._localHold && this._remoteHold) {
+			logger.debug('mangleOffer() | both on hold, mangling offer');
+			for (const m of sdp.media) {
+				if (holdMediaTypes.indexOf(m.type) === -1) {
+					continue;
+				}
+				m.direction = 'inactive';
+			}
+		}
+		// Remote hold.
+		else if (this._remoteHold) {
+			logger.debug('mangleOffer() | remote on hold, mangling offer');
+			for (const m of sdp.media) {
+				if (holdMediaTypes.indexOf(m.type) === -1) {
+					continue;
+				}
+				if (!m.direction) {
+					m.direction = 'recvonly';
+				} else if (m.direction === 'sendrecv') {
+					m.direction = 'recvonly';
+				} else if (m.direction === 'recvonly') {
+					m.direction = 'inactive';
+				}
+			}
+		}
+
+		return sdp_transform.write(sdp);
+	}
+
+	_setLocalMediaStatus() {
+		let enableAudio = true,
+			enableVideo = true;
+
+		if (this._localHold || this._remoteHold) {
+			enableAudio = false;
+			enableVideo = false;
+		}
+
+		if (this._audioMuted) {
+			enableAudio = false;
+		}
+
+		if (this._videoMuted) {
+			enableVideo = false;
+		}
+
+		this._toggleMuteAudio(!enableAudio);
+		this._toggleMuteVideo(!enableVideo);
+	}
+
+	/**
+	 * Handle SessionTimers for an incoming INVITE or UPDATE.
+	 * @param  {IncomingRequest} request
+	 * @param  {Array} responseExtraHeaders  Extra headers for the 200 response.
+	 */
+	_handleSessionTimersInIncomingRequest(request, responseExtraHeaders) {
+		if (!this._sessionTimers.enabled) {
+			return;
+		}
+
+		let session_expires_refresher;
+
+		if (
+			request.session_expires &&
+			request.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES
+		) {
+			this._sessionTimers.currentExpires = request.session_expires;
+			session_expires_refresher = request.session_expires_refresher || 'uas';
+		} else {
+			this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
+			session_expires_refresher = 'uas';
+		}
+
+		responseExtraHeaders.push(
+			`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${session_expires_refresher}`
+		);
+
+		this._sessionTimers.refresher = session_expires_refresher === 'uas';
+		this._runSessionTimer();
+	}
+
+	/**
+	 * Handle SessionTimers for an incoming response to INVITE or UPDATE.
+	 * @param  {IncomingResponse} response
+	 */
+	_handleSessionTimersInIncomingResponse(response) {
+		if (!this._sessionTimers.enabled) {
+			return;
+		}
+
+		let session_expires_refresher;
+
+		if (
+			response.session_expires &&
+			response.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES
+		) {
+			this._sessionTimers.currentExpires = response.session_expires;
+			session_expires_refresher = response.session_expires_refresher || 'uac';
+		} else {
+			this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
+			session_expires_refresher = 'uac';
+		}
+
+		this._sessionTimers.refresher = session_expires_refresher === 'uac';
+		this._runSessionTimer();
+	}
+
+	_runSessionTimer() {
+		const expires = this._sessionTimers.currentExpires;
+
+		this._sessionTimers.running = true;
+
+		clearTimeout(this._sessionTimers.timer);
+
+		// I'm the refresher.
+		if (this._sessionTimers.refresher) {
+			this._sessionTimers.timer = setTimeout(() => {
+				if (this._status === C.STATUS_TERMINATED) {
+					return;
+				}
+
+				if (!this.isReadyToReOffer()) {
+					return;
+				}
+
+				logger.debug('runSessionTimer() | sending session refresh request');
+
+				if (this._sessionTimers.refreshMethod === JsSIP_C.UPDATE) {
+					this._sendUpdate();
+				} else {
+					this._sendReinvite();
+				}
+			}, expires * 500); // Half the given interval (as the RFC states).
+		}
+
+		// I'm not the refresher.
+		else {
+			this._sessionTimers.timer = setTimeout(() => {
+				if (this._status === C.STATUS_TERMINATED) {
+					return;
+				}
+
+				logger.warn(
+					'runSessionTimer() | timer expired, terminating the session'
+				);
+
+				this.terminate({
+					cause: JsSIP_C.causes.REQUEST_TIMEOUT,
+					status_code: 408,
+					reason_phrase: 'Session Timer Expired',
+				});
+			}, expires * 1100);
+		}
+	}
+
+	_toggleMuteAudio(mute) {
+		const senders = this._connection.getSenders().filter(sender => {
+			return sender.track && sender.track.kind === 'audio';
+		});
+
+		for (const sender of senders) {
+			sender.track.enabled = !mute;
+		}
+	}
+
+	_toggleMuteVideo(mute) {
+		const senders = this._connection.getSenders().filter(sender => {
+			return sender.track && sender.track.kind === 'video';
+		});
+
+		for (const sender of senders) {
+			sender.track.enabled = !mute;
+		}
+	}
+
+	_newRTCSession(originator, request) {
+		logger.debug('newRTCSession()');
+
+		this._ua.newRTCSession(this, {
+			originator,
+			session: this,
+			request,
+		});
+	}
+
+	_connecting(request) {
+		logger.debug('session connecting');

-    this._is_confirmed = true;
+		logger.debug('emit "connecting"');

-    logger.debug('emit "confirmed"');
+		this.emit('connecting', {
+			request,
+		});
+	}

-    this.emit('confirmed', {
-      originator,
-      ack : ack || null
-    });
-  }
+	_progress(originator, response) {
+		logger.debug('session progress');

-  _ended(originator, message, cause)
-  {
-    logger.debug('session ended');
+		logger.debug('emit "progress"');

-    this._end_time = new Date();
+		this.emit('progress', {
+			originator,
+			response: response || null,
+		});
+	}

-    this._close();
+	_accepted(originator, message) {
+		logger.debug('session accepted');

-    logger.debug('emit "ended"');
+		this._start_time = new Date();

-    this.emit('ended', {
-      originator,
-      message : message || null,
-      cause
-    });
-  }
+		logger.debug('emit "accepted"');

-  _failed(originator, message, cause)
-  {
-    logger.debug('session failed');
+		this.emit('accepted', {
+			originator,
+			response: message || null,
+		});
+	}

-    // Emit private '_failed' event first.
-    logger.debug('emit "_failed"');
+	_confirmed(originator, ack) {
+		logger.debug('session confirmed');

-    this.emit('_failed', {
-      originator,
-      message : message || null,
-      cause
-    });
+		this._is_confirmed = true;

-    this._close();
+		logger.debug('emit "confirmed"');

-    logger.debug('emit "failed"');
+		this.emit('confirmed', {
+			originator,
+			ack: ack || null,
+		});
+	}

-    this.emit('failed', {
-      originator,
-      message : message || null,
-      cause
-    });
-  }
+	_ended(originator, message, cause) {
+		logger.debug('session ended');

-  _onhold(originator)
-  {
-    logger.debug('session onhold');
+		this._end_time = new Date();

-    this._setLocalMediaStatus();
+		this._close();

-    logger.debug('emit "hold"');
+		logger.debug('emit "ended"');

-    this.emit('hold', {
-      originator
-    });
-  }
+		this.emit('ended', {
+			originator,
+			message: message || null,
+			cause,
+		});
+	}

-  _onunhold(originator)
-  {
-    logger.debug('session onunhold');
+	_failed(originator, message, cause) {
+		logger.debug('session failed');

-    this._setLocalMediaStatus();
+		// Emit private '_failed' event first.
+		logger.debug('emit "_failed"');

-    logger.debug('emit "unhold"');
+		this.emit('_failed', {
+			originator,
+			message: message || null,
+			cause,
+		});

-    this.emit('unhold', {
-      originator
-    });
-  }
+		this._close();

-  _onmute({ audio, video })
-  {
-    logger.debug('session onmute');
+		logger.debug('emit "failed"');

-    this._setLocalMediaStatus();
+		this.emit('failed', {
+			originator,
+			message: message || null,
+			cause,
+		});
+	}

-    logger.debug('emit "muted"');
+	_onhold(originator) {
+		logger.debug('session onhold');

-    this.emit('muted', {
-      audio,
-      video
-    });
-  }
+		this._setLocalMediaStatus();

-  _onunmute({ audio, video })
-  {
-    logger.debug('session onunmute');
+		logger.debug('emit "hold"');

-    this._setLocalMediaStatus();
+		this.emit('hold', {
+			originator,
+		});
+	}

-    logger.debug('emit "unmuted"');
+	_onunhold(originator) {
+		logger.debug('session onunhold');

-    this.emit('unmuted', {
-      audio,
-      video
-    });
-  }
+		this._setLocalMediaStatus();
+
+		logger.debug('emit "unhold"');
+
+		this.emit('unhold', {
+			originator,
+		});
+	}
+
+	_onmute({ audio, video }) {
+		logger.debug('session onmute');
+
+		this._setLocalMediaStatus();
+
+		logger.debug('emit "muted"');
+
+		this.emit('muted', {
+			audio,
+			video,
+		});
+	}
+
+	_onunmute({ audio, video }) {
+		logger.debug('session onunmute');
+
+		this._setLocalMediaStatus();
+
+		logger.debug('emit "unmuted"');
+
+		this.emit('unmuted', {
+			audio,
+			video,
+		});
+	}
 };
diff --git a/src/RTCSession/DTMF.js b/src/RTCSession/DTMF.js
index b296bae..abacc0f 100644
--- a/src/RTCSession/DTMF.js
+++ b/src/RTCSession/DTMF.js
@@ -7,185 +7,154 @@ const Utils = require('../Utils');
 const logger = new Logger('RTCSession:DTMF');

 const C = {
-  MIN_DURATION           : 70,
-  MAX_DURATION           : 6000,
-  DEFAULT_DURATION       : 100,
-  MIN_INTER_TONE_GAP     : 50,
-  DEFAULT_INTER_TONE_GAP : 500
+	MIN_DURATION: 70,
+	MAX_DURATION: 6000,
+	DEFAULT_DURATION: 100,
+	MIN_INTER_TONE_GAP: 50,
+	DEFAULT_INTER_TONE_GAP: 500,
 };

-module.exports = class DTMF extends EventEmitter
-{
-  constructor(session)
-  {
-    super();
-
-    this._session = session;
-    this._direction = null;
-    this._tone = null;
-    this._duration = null;
-    this._request = null;
-  }
-
-  get tone()
-  {
-    return this._tone;
-  }
-
-  get duration()
-  {
-    return this._duration;
-  }
-
-  send(tone, options = {})
-  {
-    if (tone === undefined)
-    {
-      throw new TypeError('Not enough arguments');
-    }
-
-    this._direction = 'outgoing';
-
-    // Check RTCSession Status.
-    if (
-      this._session.status !== this._session.C.STATUS_CONFIRMED &&
-      this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK &&
-      this._session.status !== this._session.C.STATUS_1XX_RECEIVED
-    )
-    {
-      throw new Exceptions.InvalidStateError(this._session.status);
-    }
-
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
-    this.eventHandlers = Utils.cloneObject(options.eventHandlers);
-
-    // Check tone type.
-    if (typeof tone === 'string')
-    {
-      tone = tone.toUpperCase();
-    }
-    else if (typeof tone === 'number')
-    {
-      tone = tone.toString();
-    }
-    else
-    {
-      throw new TypeError(`Invalid tone: ${tone}`);
-    }
-
-    // Check tone value.
-    if (!tone.match(/^[0-9A-DR#*]$/))
-    {
-      throw new TypeError(`Invalid tone: ${tone}`);
-    }
-    else
-    {
-      this._tone = tone;
-    }
-
-    // Duration is checked/corrected in RTCSession.
-    this._duration = options.duration;
-
-    extraHeaders.push('Content-Type: application/dtmf-relay');
-
-    let body = `Signal=${this._tone}\r\n`;
-
-    body += `Duration=${this._duration}`;
-
-    this._session.newDTMF({
-      originator : 'local',
-      dtmf       : this,
-      request    : this._request
-    });
-
-    this._session.sendRequest(JsSIP_C.INFO, {
-      extraHeaders,
-      eventHandlers : {
-        onSuccessResponse : (response) =>
-        {
-          this.emit('succeeded', {
-            originator : 'remote',
-            response
-          });
-        },
-        onErrorResponse : (response) =>
-        {
-          if (this.eventHandlers.onFailed)
-          {
-            this.eventHandlers.onFailed();
-          }
-
-          this.emit('failed', {
-            originator : 'remote',
-            response
-          });
-        },
-        onRequestTimeout : () =>
-        {
-          this._session.onRequestTimeout();
-        },
-        onTransportError : () =>
-        {
-          this._session.onTransportError();
-        },
-        onDialogError : () =>
-        {
-          this._session.onDialogError();
-        }
-      },
-      body
-    });
-  }
-
-  init_incoming(request)
-  {
-    const reg_tone = /^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/;
-    const reg_duration = /^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/;
-
-    this._direction = 'incoming';
-    this._request = request;
-
-    request.reply(200);
-
-    if (request.body)
-    {
-      const body = request.body.split('\n');
-
-      if (body.length >= 1)
-      {
-        if (reg_tone.test(body[0]))
-        {
-          this._tone = body[0].replace(reg_tone, '$2');
-        }
-      }
-      if (body.length >=2)
-      {
-        if (reg_duration.test(body[1]))
-        {
-          this._duration = parseInt(body[1].replace(reg_duration, '$2'), 10);
-        }
-      }
-    }
-
-    if (!this._duration)
-    {
-      this._duration = C.DEFAULT_DURATION;
-    }
-
-    if (!this._tone)
-    {
-      logger.debug('invalid INFO DTMF received, discarded');
-    }
-    else
-    {
-      this._session.newDTMF({
-        originator : 'remote',
-        dtmf       : this,
-        request
-      });
-    }
-  }
+module.exports = class DTMF extends EventEmitter {
+	constructor(session) {
+		super();
+
+		this._session = session;
+		this._direction = null;
+		this._tone = null;
+		this._duration = null;
+		this._request = null;
+	}
+
+	get tone() {
+		return this._tone;
+	}
+
+	get duration() {
+		return this._duration;
+	}
+
+	send(tone, options = {}) {
+		if (tone === undefined) {
+			throw new TypeError('Not enough arguments');
+		}
+
+		this._direction = 'outgoing';
+
+		// Check RTCSession Status.
+		if (
+			this._session.status !== this._session.C.STATUS_CONFIRMED &&
+			this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK &&
+			this._session.status !== this._session.C.STATUS_1XX_RECEIVED
+		) {
+			throw new Exceptions.InvalidStateError(this._session.status);
+		}
+
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+		this.eventHandlers = Utils.cloneObject(options.eventHandlers);
+
+		// Check tone type.
+		if (typeof tone === 'string') {
+			tone = tone.toUpperCase();
+		} else if (typeof tone === 'number') {
+			tone = tone.toString();
+		} else {
+			throw new TypeError(`Invalid tone: ${tone}`);
+		}
+
+		// Check tone value.
+		if (!tone.match(/^[0-9A-DR#*]$/)) {
+			throw new TypeError(`Invalid tone: ${tone}`);
+		} else {
+			this._tone = tone;
+		}
+
+		// Duration is checked/corrected in RTCSession.
+		this._duration = options.duration;
+
+		extraHeaders.push('Content-Type: application/dtmf-relay');
+
+		let body = `Signal=${this._tone}\r\n`;
+
+		body += `Duration=${this._duration}`;
+
+		this._session.newDTMF({
+			originator: 'local',
+			dtmf: this,
+			request: this._request,
+		});
+
+		this._session.sendRequest(JsSIP_C.INFO, {
+			extraHeaders,
+			eventHandlers: {
+				onSuccessResponse: response => {
+					this.emit('succeeded', {
+						originator: 'remote',
+						response,
+					});
+				},
+				onErrorResponse: response => {
+					if (this.eventHandlers.onFailed) {
+						this.eventHandlers.onFailed();
+					}
+
+					this.emit('failed', {
+						originator: 'remote',
+						response,
+					});
+				},
+				onRequestTimeout: () => {
+					this._session.onRequestTimeout();
+				},
+				onTransportError: () => {
+					this._session.onTransportError();
+				},
+				onDialogError: () => {
+					this._session.onDialogError();
+				},
+			},
+			body,
+		});
+	}
+
+	init_incoming(request) {
+		const reg_tone = /^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/;
+		const reg_duration = /^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/;
+
+		this._direction = 'incoming';
+		this._request = request;
+
+		request.reply(200);
+
+		if (request.body) {
+			const body = request.body.split('\n');
+
+			if (body.length >= 1) {
+				if (reg_tone.test(body[0])) {
+					this._tone = body[0].replace(reg_tone, '$2');
+				}
+			}
+			if (body.length >= 2) {
+				if (reg_duration.test(body[1])) {
+					this._duration = parseInt(body[1].replace(reg_duration, '$2'), 10);
+				}
+			}
+		}
+
+		if (!this._duration) {
+			this._duration = C.DEFAULT_DURATION;
+		}
+
+		if (!this._tone) {
+			logger.debug('invalid INFO DTMF received, discarded');
+		} else {
+			this._session.newDTMF({
+				originator: 'remote',
+				dtmf: this,
+				request,
+			});
+		}
+	}
 };

 /**
diff --git a/src/RTCSession/Info.js b/src/RTCSession/Info.js
index c331c35..8c57bb9 100644
--- a/src/RTCSession/Info.js
+++ b/src/RTCSession/Info.js
@@ -3,106 +3,96 @@ const JsSIP_C = require('../Constants');
 const Exceptions = require('../Exceptions');
 const Utils = require('../Utils');

-module.exports = class Info extends EventEmitter
-{
-  constructor(session)
-  {
-    super();
-
-    this._session = session;
-    this._direction = null;
-    this._contentType = null;
-    this._body = null;
-  }
-
-  get contentType()
-  {
-    return this._contentType;
-  }
-
-  get body()
-  {
-    return this._body;
-  }
-
-  send(contentType, body, options = {})
-  {
-    this._direction = 'outgoing';
-
-    if (contentType === undefined)
-    {
-      throw new TypeError('Not enough arguments');
-    }
-
-    // Check RTCSession Status.
-    if (this._session.status !== this._session.C.STATUS_CONFIRMED &&
-      this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK)
-    {
-      throw new Exceptions.InvalidStateError(this._session.status);
-    }
-
-    this._contentType = contentType;
-    this._body = body;
-
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
-    extraHeaders.push(`Content-Type: ${contentType}`);
-
-    this._session.newInfo({
-      originator : 'local',
-      info       : this,
-      request    : this.request
-    });
-
-    this._session.sendRequest(JsSIP_C.INFO, {
-      extraHeaders,
-      eventHandlers : {
-        onSuccessResponse : (response) =>
-        {
-          this.emit('succeeded', {
-            originator : 'remote',
-            response
-          });
-        },
-        onErrorResponse : (response) =>
-        {
-          this.emit('failed', {
-            originator : 'remote',
-            response
-          });
-        },
-        onTransportError : () =>
-        {
-          this._session.onTransportError();
-        },
-        onRequestTimeout : () =>
-        {
-          this._session.onRequestTimeout();
-        },
-        onDialogError : () =>
-        {
-          this._session.onDialogError();
-        }
-      },
-      body
-    });
-  }
-
-  init_incoming(request)
-  {
-    this._direction = 'incoming';
-    this.request = request;
-
-    request.reply(200);
-
-    this._contentType = request.hasHeader('Content-Type') ?
-      request.getHeader('Content-Type').toLowerCase() : undefined;
-    this._body = request.body;
-
-    this._session.newInfo({
-      originator : 'remote',
-      info       : this,
-      request
-    });
-  }
+module.exports = class Info extends EventEmitter {
+	constructor(session) {
+		super();
+
+		this._session = session;
+		this._direction = null;
+		this._contentType = null;
+		this._body = null;
+	}
+
+	get contentType() {
+		return this._contentType;
+	}
+
+	get body() {
+		return this._body;
+	}
+
+	send(contentType, body, options = {}) {
+		this._direction = 'outgoing';
+
+		if (contentType === undefined) {
+			throw new TypeError('Not enough arguments');
+		}
+
+		// Check RTCSession Status.
+		if (
+			this._session.status !== this._session.C.STATUS_CONFIRMED &&
+			this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK
+		) {
+			throw new Exceptions.InvalidStateError(this._session.status);
+		}
+
+		this._contentType = contentType;
+		this._body = body;
+
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+		extraHeaders.push(`Content-Type: ${contentType}`);
+
+		this._session.newInfo({
+			originator: 'local',
+			info: this,
+			request: this.request,
+		});
+
+		this._session.sendRequest(JsSIP_C.INFO, {
+			extraHeaders,
+			eventHandlers: {
+				onSuccessResponse: response => {
+					this.emit('succeeded', {
+						originator: 'remote',
+						response,
+					});
+				},
+				onErrorResponse: response => {
+					this.emit('failed', {
+						originator: 'remote',
+						response,
+					});
+				},
+				onTransportError: () => {
+					this._session.onTransportError();
+				},
+				onRequestTimeout: () => {
+					this._session.onRequestTimeout();
+				},
+				onDialogError: () => {
+					this._session.onDialogError();
+				},
+			},
+			body,
+		});
+	}
+
+	init_incoming(request) {
+		this._direction = 'incoming';
+		this.request = request;
+
+		request.reply(200);
+
+		this._contentType = request.hasHeader('Content-Type')
+			? request.getHeader('Content-Type').toLowerCase()
+			: undefined;
+		this._body = request.body;
+
+		this._session.newInfo({
+			originator: 'remote',
+			info: this,
+			request,
+		});
+	}
 };
diff --git a/src/RTCSession/ReferNotifier.js b/src/RTCSession/ReferNotifier.js
index dcf5071..4af0ea0 100644
--- a/src/RTCSession/ReferNotifier.js
+++ b/src/RTCSession/ReferNotifier.js
@@ -4,58 +4,53 @@ const JsSIP_C = require('../Constants');
 const logger = new Logger('RTCSession:ReferNotifier');

 const C = {
-  event_type : 'refer',
-  body_type  : 'message/sipfrag;version=2.0',
-  expires    : 300
+	event_type: 'refer',
+	body_type: 'message/sipfrag;version=2.0',
+	expires: 300,
 };

-module.exports = class ReferNotifier
-{
-  constructor(session, id, expires)
-  {
-    this._session = session;
-    this._id = id;
-    this._expires = expires || C.expires;
-    this._active = true;
-
-    // The creation of a Notifier results in an immediate NOTIFY.
-    this.notify(100);
-  }
-
-  notify(code, reason)
-  {
-    logger.debug('notify()');
-
-    if (this._active === false)
-    {
-      return;
-    }
-
-    reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
-
-    let state;
-
-    if (code >= 200)
-    {
-      state = 'terminated;reason=noresource';
-    }
-    else
-    {
-      state = `active;expires=${this._expires}`;
-    }
-
-    // Put this in a try/catch block.
-    this._session.sendRequest(JsSIP_C.NOTIFY, {
-      extraHeaders : [
-        `Event: ${C.event_type};id=${this._id}`,
-        `Subscription-State: ${state}`,
-        `Content-Type: ${C.body_type}`
-      ],
-      body          : `SIP/2.0 ${code} ${reason}`,
-      eventHandlers : {
-        // If a negative response is received, subscription is canceled.
-        onErrorResponse() { this._active = false; }
-      }
-    });
-  }
+module.exports = class ReferNotifier {
+	constructor(session, id, expires) {
+		this._session = session;
+		this._id = id;
+		this._expires = expires || C.expires;
+		this._active = true;
+
+		// The creation of a Notifier results in an immediate NOTIFY.
+		this.notify(100);
+	}
+
+	notify(code, reason) {
+		logger.debug('notify()');
+
+		if (this._active === false) {
+			return;
+		}
+
+		reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
+
+		let state;
+
+		if (code >= 200) {
+			state = 'terminated;reason=noresource';
+		} else {
+			state = `active;expires=${this._expires}`;
+		}
+
+		// Put this in a try/catch block.
+		this._session.sendRequest(JsSIP_C.NOTIFY, {
+			extraHeaders: [
+				`Event: ${C.event_type};id=${this._id}`,
+				`Subscription-State: ${state}`,
+				`Content-Type: ${C.body_type}`,
+			],
+			body: `SIP/2.0 ${code} ${reason}`,
+			eventHandlers: {
+				// If a negative response is received, subscription is canceled.
+				onErrorResponse() {
+					this._active = false;
+				},
+			},
+		});
+	}
 };
diff --git a/src/RTCSession/ReferSubscriber.js b/src/RTCSession/ReferSubscriber.js
index 0d2dbaa..26231ef 100644
--- a/src/RTCSession/ReferSubscriber.js
+++ b/src/RTCSession/ReferSubscriber.js
@@ -6,163 +6,157 @@ const Utils = require('../Utils');

 const logger = new Logger('RTCSession:ReferSubscriber');

-module.exports = class ReferSubscriber extends EventEmitter
-{
-  constructor(session)
-  {
-    super();
-
-    this._id = null;
-    this._session = session;
-  }
-
-  get id()
-  {
-    return this._id;
-  }
-
-  sendRefer(target, options = {})
-  {
-    logger.debug('sendRefer()');
-
-    const extraHeaders = Utils.cloneArray(options.extraHeaders);
-    const eventHandlers = Utils.cloneObject(options.eventHandlers);
-
-    // Set event handlers.
-    for (const event in eventHandlers)
-    {
-      if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
-      {
-        this.on(event, eventHandlers[event]);
-      }
-    }
-
-    // Replaces URI header field.
-    let replaces = null;
-
-    if (options.replaces)
-    {
-      replaces = options.replaces._request.call_id;
-      replaces += `;to-tag=${options.replaces._to_tag}`;
-      replaces += `;from-tag=${options.replaces._from_tag}`;
-
-      replaces = encodeURIComponent(replaces);
-    }
-
-    // Refer-To header field.
-    const referTo = `Refer-To: <${target}${replaces?`?Replaces=${replaces}`:''}>`;
-
-    extraHeaders.push(referTo);
-
-    // Referred-By header field (if not already present).
-    if (!extraHeaders.some((header) => header.toLowerCase().startsWith('referred-by:')))
-    {
-      const referredBy = `Referred-By: <${this._session._ua._configuration.uri._scheme}:${this._session._ua._configuration.uri._user}@${this._session._ua._configuration.uri._host}>`;
-
-      extraHeaders.push(referredBy);
-    }
-
-    extraHeaders.push(`Contact: ${this._session.contact}`);
-
-    const request = this._session.sendRequest(JsSIP_C.REFER, {
-      extraHeaders,
-      eventHandlers : {
-        onSuccessResponse : (response) =>
-        {
-          this._requestSucceeded(response);
-        },
-        onErrorResponse : (response) =>
-        {
-          this._requestFailed(response, JsSIP_C.causes.REJECTED);
-        },
-        onTransportError : () =>
-        {
-          this._requestFailed(null, JsSIP_C.causes.CONNECTION_ERROR);
-        },
-        onRequestTimeout : () =>
-        {
-          this._requestFailed(null, JsSIP_C.causes.REQUEST_TIMEOUT);
-        },
-        onDialogError : () =>
-        {
-          this._requestFailed(null, JsSIP_C.causes.DIALOG_ERROR);
-        }
-      }
-    });
-
-    this._id = request.cseq;
-  }
-
-  receiveNotify(request)
-  {
-    logger.debug('receiveNotify()');
-
-    if (!request.body)
-    {
-      return;
-    }
-
-    const status_line = Grammar.parse(request.body.trim().split('\r\n', 1)[0], 'Status_Line');
-
-    if (status_line === -1)
-    {
-      logger.debug(`receiveNotify() | error parsing NOTIFY body: "${request.body}"`);
-
-      return;
-    }
-
-    switch (true)
-    {
-      case /^100$/.test(status_line.status_code):
-        this.emit('trying', {
-          request,
-          status_line
-        });
-        break;
-
-      case /^1[0-9]{2}$/.test(status_line.status_code):
-        this.emit('progress', {
-          request,
-          status_line
-        });
-        break;
-
-      case /^2[0-9]{2}$/.test(status_line.status_code):
-        this.emit('accepted', {
-          request,
-          status_line
-        });
-        break;
-
-      default:
-        this.emit('failed', {
-          request,
-          status_line
-        });
-        break;
-    }
-  }
-
-  _requestSucceeded(response)
-  {
-    logger.debug('REFER succeeded');
-
-    logger.debug('emit "requestSucceeded"');
-
-    this.emit('requestSucceeded', {
-      response
-    });
-  }
-
-  _requestFailed(response, cause)
-  {
-    logger.debug('REFER failed');
-
-    logger.debug('emit "requestFailed"');
-
-    this.emit('requestFailed', {
-      response : response || null,
-      cause
-    });
-  }
+module.exports = class ReferSubscriber extends EventEmitter {
+	constructor(session) {
+		super();
+
+		this._id = null;
+		this._session = session;
+	}
+
+	get id() {
+		return this._id;
+	}
+
+	sendRefer(target, options = {}) {
+		logger.debug('sendRefer()');
+
+		const extraHeaders = Utils.cloneArray(options.extraHeaders);
+		const eventHandlers = Utils.cloneObject(options.eventHandlers);
+
+		// Set event handlers.
+		for (const event in eventHandlers) {
+			if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+				this.on(event, eventHandlers[event]);
+			}
+		}
+
+		// Replaces URI header field.
+		let replaces = null;
+
+		if (options.replaces) {
+			replaces = options.replaces._request.call_id;
+			replaces += `;to-tag=${options.replaces._to_tag}`;
+			replaces += `;from-tag=${options.replaces._from_tag}`;
+
+			replaces = encodeURIComponent(replaces);
+		}
+
+		// Refer-To header field.
+		const referTo = `Refer-To: <${target}${replaces ? `?Replaces=${replaces}` : ''}>`;
+
+		extraHeaders.push(referTo);
+
+		// Referred-By header field (if not already present).
+		if (
+			!extraHeaders.some(header =>
+				header.toLowerCase().startsWith('referred-by:')
+			)
+		) {
+			const referredBy = `Referred-By: <${this._session._ua._configuration.uri._scheme}:${this._session._ua._configuration.uri._user}@${this._session._ua._configuration.uri._host}>`;
+
+			extraHeaders.push(referredBy);
+		}
+
+		extraHeaders.push(`Contact: ${this._session.contact}`);
+
+		const request = this._session.sendRequest(JsSIP_C.REFER, {
+			extraHeaders,
+			eventHandlers: {
+				onSuccessResponse: response => {
+					this._requestSucceeded(response);
+				},
+				onErrorResponse: response => {
+					this._requestFailed(response, JsSIP_C.causes.REJECTED);
+				},
+				onTransportError: () => {
+					this._requestFailed(null, JsSIP_C.causes.CONNECTION_ERROR);
+				},
+				onRequestTimeout: () => {
+					this._requestFailed(null, JsSIP_C.causes.REQUEST_TIMEOUT);
+				},
+				onDialogError: () => {
+					this._requestFailed(null, JsSIP_C.causes.DIALOG_ERROR);
+				},
+			},
+		});
+
+		this._id = request.cseq;
+	}
+
+	receiveNotify(request) {
+		logger.debug('receiveNotify()');
+
+		if (!request.body) {
+			return;
+		}
+
+		const status_line = Grammar.parse(
+			request.body.trim().split('\r\n', 1)[0],
+			'Status_Line'
+		);
+
+		if (status_line === -1) {
+			logger.debug(
+				`receiveNotify() | error parsing NOTIFY body: "${request.body}"`
+			);
+
+			return;
+		}
+
+		switch (true) {
+			case /^100$/.test(status_line.status_code): {
+				this.emit('trying', {
+					request,
+					status_line,
+				});
+				break;
+			}
+
+			case /^1[0-9]{2}$/.test(status_line.status_code): {
+				this.emit('progress', {
+					request,
+					status_line,
+				});
+				break;
+			}
+
+			case /^2[0-9]{2}$/.test(status_line.status_code): {
+				this.emit('accepted', {
+					request,
+					status_line,
+				});
+				break;
+			}
+
+			default: {
+				this.emit('failed', {
+					request,
+					status_line,
+				});
+				break;
+			}
+		}
+	}
+
+	_requestSucceeded(response) {
+		logger.debug('REFER succeeded');
+
+		logger.debug('emit "requestSucceeded"');
+
+		this.emit('requestSucceeded', {
+			response,
+		});
+	}
+
+	_requestFailed(response, cause) {
+		logger.debug('REFER failed');
+
+		logger.debug('emit "requestFailed"');
+
+		this.emit('requestFailed', {
+			response: response || null,
+			cause,
+		});
+	}
 };
diff --git a/src/Registrator.d.ts b/src/Registrator.d.ts
index 7d5d6a1..544ddd6 100644
--- a/src/Registrator.d.ts
+++ b/src/Registrator.d.ts
@@ -1,12 +1,12 @@
-import {UA} from './UA'
-import {Transport} from './Transport'
+import { UA } from './UA';
+import { Transport } from './Transport';

 export type ExtraContactParams = Record<string, string | number | boolean>;

 export class Registrator {
-  constructor(ua: UA, transport: Transport);
+	constructor(ua: UA, transport: Transport);

-  setExtraHeaders(extraHeaders: string[]): void;
+	setExtraHeaders(extraHeaders: string[]): void;

-  setExtraContactParams(extraContactParams: ExtraContactParams): void;
+	setExtraContactParams(extraContactParams: ExtraContactParams): void;
 }
diff --git a/src/Registrator.js b/src/Registrator.js
index 0a70465..7ce227b 100644
--- a/src/Registrator.js
+++ b/src/Registrator.js
@@ -8,436 +8,406 @@ const logger = new Logger('Registrator');

 const MIN_REGISTER_EXPIRES = 10; // In seconds.

-module.exports = class Registrator
-{
-  constructor(ua, transport)
-  {
-    // Force reg_id to 1.
-    this._reg_id = 1;
-
-    this._ua = ua;
-    this._transport = transport;
-
-    this._registrar = ua.configuration.registrar_server;
-    this._expires = ua.configuration.register_expires;
+module.exports = class Registrator {
+	constructor(ua, transport) {
+		// Force reg_id to 1.
+		this._reg_id = 1;

-    // Call-ID and CSeq values RFC3261 10.2.
-    this._call_id = Utils.createRandomToken(22);
-    this._cseq = 0;
-
-    this._to_uri = ua.configuration.uri;
-
-    this._registrationTimer = null;
-
-    // Ongoing Register request.
-    this._registering = false;
-
-    // Set status.
-    this._registered = false;
-
-    // Contact header.
-    this._contact = this._ua.contact.toString();
-
-    // Sip.ice media feature tag (RFC 5768).
-    this._contact += ';+sip.ice';
-
-    // Custom headers for REGISTER and un-REGISTER.
-    this._extraHeaders = [];
-
-    // Custom Contact header params for REGISTER and un-REGISTER.
-    this._extraContactParams = '';
-
-    // Contents of the sip.instance Contact header parameter.
-    this._sipInstance = `"<urn:uuid:${this._ua.configuration.instance_id}>"`;
-
-    this._contact += `;reg-id=${this._reg_id}`;
-    this._contact += `;+sip.instance=${this._sipInstance}`;
-  }
-
-  get registered()
-  {
-    return this._registered;
-  }
-
-  setExtraHeaders(extraHeaders)
-  {
-    if (!Array.isArray(extraHeaders))
-    {
-      extraHeaders = [];
-    }
-
-    this._extraHeaders = extraHeaders.slice();
-  }
-
-  setExtraContactParams(extraContactParams)
-  {
-    if (!(extraContactParams instanceof Object))
-    {
-      extraContactParams = {};
-    }
-
-    // Reset it.
-    this._extraContactParams = '';
-
-    for (const param_key in extraContactParams)
-    {
-      if (Object.prototype.hasOwnProperty.call(extraContactParams, param_key))
-      {
-        const param_value = extraContactParams[param_key];
-
-        this._extraContactParams += (`;${param_key}`);
-        if (param_value)
-        {
-          this._extraContactParams += (`=${param_value}`);
-        }
-      }
-    }
-  }
-
-  register()
-  {
-    if (this._registering)
-    {
-      logger.debug('Register request in progress...');
-
-      return;
-    }
-
-    const extraHeaders = Utils.cloneArray(this._extraHeaders);
-
-    let contactValue;
-
-    if (this._expires)
-    {
-      contactValue = `${this._contact};expires=${this._expires}${this._extraContactParams}`;
-      extraHeaders.push(`Expires: ${this._expires}`);
-    }
-    else
-    {
-      contactValue = `${this._contact}${this._extraContactParams}`;
-    }
-
-    extraHeaders.push(`Contact: ${contactValue}`);
-
-    let fromTag = Utils.newTag();
-
-    if (this._ua.configuration.register_from_tag_trail)
-    {
-      if (typeof this._ua.configuration.register_from_tag_trail === 'function')
-      {
-        fromTag += this._ua.configuration.register_from_tag_trail();
-      }
-      else
-      {
-        fromTag += this._ua.configuration.register_from_tag_trail;
-      }
-    }
-
-    const request = new SIPMessage.OutgoingRequest(
-      JsSIP_C.REGISTER, this._registrar, this._ua, {
-        'to_uri'   : this._to_uri,
-        'call_id'  : this._call_id,
-        'cseq'     : (this._cseq += 1),
-        'from_tag' : fromTag
-      }, extraHeaders);
-
-    const request_sender = new RequestSender(this._ua, request, {
-      onRequestTimeout : () =>
-      {
-        this._registrationFailure(null, JsSIP_C.causes.REQUEST_TIMEOUT);
-      },
-      onTransportError : () =>
-      {
-        this._registrationFailure(null, JsSIP_C.causes.CONNECTION_ERROR);
-      },
-      // Increase the CSeq on authentication.
-      onAuthenticated : () =>
-      {
-        this._cseq += 1;
-      },
-      onReceiveResponse : (response) =>
-      {
-        // Discard responses to older REGISTER/un-REGISTER requests.
-        if (response.cseq !== this._cseq)
-        {
-          return;
-        }
-
-        // Clear registration timer.
-        if (this._registrationTimer !== null)
-        {
-          clearTimeout(this._registrationTimer);
-          this._registrationTimer = null;
-        }
-
-        switch (true)
-        {
-          case /^1[0-9]{2}$/.test(response.status_code):
-          {
-            // Ignore provisional responses.
-            break;
-          }
-
-          case /^2[0-9]{2}$/.test(response.status_code):
-          {
-            this._registering = false;
-
-            if (!response.hasHeader('Contact'))
-            {
-              logger.debug('no Contact header in response to REGISTER, response ignored');
-
-              break;
-            }
-
-            const contacts = response.headers['Contact']
-              .reduce((a, b) => a.concat(b.parsed), []);
-
-            // Get the Contact pointing to us and update the expires value accordingly.
-            // Try to find a matching Contact using sip.instance and reg-id.
-            let contact = contacts.find((element) => (
-              (this._sipInstance === element.getParam('+sip.instance')) &&
-              (this._reg_id === parseInt(element.getParam('reg-id')))
-            ));
-
-            // If no match was found using the sip.instance try comparing the URIs.
-            if (!contact)
-            {
-              contact = contacts.find((element) => (
-                (element.uri.user === this._ua.contact.uri.user)
-              ));
-            }
-
-            if (!contact)
-            {
-              logger.debug('no Contact header pointing to us, response ignored');
-
-              break;
-            }
-
-            let expires = contact.getParam('expires');
-
-            if (!expires && response.hasHeader('expires'))
-            {
-              expires = response.getHeader('expires');
-            }
-
-            if (!expires)
-            {
-              expires = this._expires;
-            }
-
-            expires = Number(expires);
-
-            if (expires < MIN_REGISTER_EXPIRES)
-              expires = MIN_REGISTER_EXPIRES;
-
-            const timeout = expires > 64
-              ? (expires * 1000 / 2) +
-                Math.floor(((expires / 2) - 32) * 1000 * Math.random())
-              : (expires * 1000) - 5000;
-
-            // Re-Register or emit an event before the expiration interval has elapsed.
-            // For that, decrease the expires value. ie: 3 seconds.
-            this._registrationTimer = setTimeout(() =>
-            {
-              this._registrationTimer = null;
-              // If there are no listeners for registrationExpiring, renew registration.
-              // If there are listeners, let the function listening do the register call.
-              if (this._ua.listeners('registrationExpiring').length === 0)
-              {
-                this.register();
-              }
-              else
-              {
-                this._ua.emit('registrationExpiring');
-              }
-            }, timeout);
-
-            // Save gruu values.
-            if (contact.hasParam('temp-gruu'))
-            {
-              this._ua.contact.temp_gruu = contact.getParam('temp-gruu').replace(/"/g, '');
-            }
-            if (contact.hasParam('pub-gruu'))
-            {
-              this._ua.contact.pub_gruu = contact.getParam('pub-gruu').replace(/"/g, '');
-            }
-
-            if (!this._registered)
-            {
-              this._registered = true;
-              this._ua.registered({ response });
-            }
-
-            break;
-          }
-
-          // Interval too brief RFC3261 10.2.8.
-          case /^423$/.test(response.status_code):
-          {
-            if (response.hasHeader('min-expires'))
-            {
-              // Increase our registration interval to the suggested minimum.
-              this._expires = Number(response.getHeader('min-expires'));
-
-              if (this._expires < MIN_REGISTER_EXPIRES)
-                this._expires = MIN_REGISTER_EXPIRES;
-
-              // Assure register re-try with new expire.
-              this._registering = false;
-
-              // Attempt the registration again immediately.
-              this.register();
-            }
-            else
-            { // This response MUST contain a Min-Expires header field.
-              logger.debug('423 response received for REGISTER without Min-Expires');
-
-              this._registrationFailure(response, JsSIP_C.causes.SIP_FAILURE_CODE);
-            }
-
-            break;
-          }
-
-          default:
-          {
-            const cause = Utils.sipErrorCause(response.status_code);
-
-            this._registrationFailure(response, cause);
-          }
-        }
-      }
-    });
-
-    this._registering = true;
-    request_sender.send();
-  }
-
-  unregister(options = {})
-  {
-    if (!this._registered)
-    {
-      logger.debug('already unregistered');
-
-      return;
-    }
-
-    this._registered = false;
-
-    // Clear the registration timer.
-    if (this._registrationTimer !== null)
-    {
-      clearTimeout(this._registrationTimer);
-      this._registrationTimer = null;
-    }
-
-    const extraHeaders = Utils.cloneArray(this._extraHeaders);
-
-    if (options.all)
-    {
-      extraHeaders.push(`Contact: *${this._extraContactParams}`);
-    }
-    else
-    {
-      extraHeaders.push(`Contact: ${this._contact};expires=0${this._extraContactParams}`);
-    }
-
-    extraHeaders.push('Expires: 0');
-
-    const request = new SIPMessage.OutgoingRequest(
-      JsSIP_C.REGISTER, this._registrar, this._ua, {
-        'to_uri'  : this._to_uri,
-        'call_id' : this._call_id,
-        'cseq'    : (this._cseq += 1)
-      }, extraHeaders);
-
-    const request_sender = new RequestSender(this._ua, request, {
-      onRequestTimeout : () =>
-      {
-        this._unregistered(null, JsSIP_C.causes.REQUEST_TIMEOUT);
-      },
-      onTransportError : () =>
-      {
-        this._unregistered(null, JsSIP_C.causes.CONNECTION_ERROR);
-      },
-      // Increase the CSeq on authentication.
-      onAuthenticated : () =>
-      {
-        this._cseq += 1;
-      },
-      onReceiveResponse : (response) =>
-      {
-        switch (true)
-        {
-          case /^1[0-9]{2}$/.test(response.status_code):
-            // Ignore provisional responses.
-            break;
-          case /^2[0-9]{2}$/.test(response.status_code):
-            this._unregistered(response);
-            break;
-          default:
-          {
-            const cause = Utils.sipErrorCause(response.status_code);
-
-            this._unregistered(response, cause);
-          }
-        }
-      }
-    });
-
-    request_sender.send();
-  }
-
-  close()
-  {
-    if (this._registered)
-    {
-      this.unregister();
-    }
-  }
-
-
-  onTransportClosed()
-  {
-    this._registering = false;
-    if (this._registrationTimer !== null)
-    {
-      clearTimeout(this._registrationTimer);
-      this._registrationTimer = null;
-    }
-
-    if (this._registered)
-    {
-      this._registered = false;
-      this._ua.unregistered({});
-    }
-  }
-
-  _registrationFailure(response, cause)
-  {
-    this._registering = false;
-    this._ua.registrationFailed({
-      response : response || null,
-      cause
-    });
-
-    if (this._registered)
-    {
-      this._registered = false;
-      this._ua.unregistered({
-        response : response || null,
-        cause
-      });
-    }
-  }
-
-  _unregistered(response, cause)
-  {
-    this._registering = false;
-    this._registered = false;
-    this._ua.unregistered({
-      response : response || null,
-      cause    : cause || null
-    });
-  }
+		this._ua = ua;
+		this._transport = transport;
+
+		this._registrar = ua.configuration.registrar_server;
+		this._expires = ua.configuration.register_expires;
+
+		// Call-ID and CSeq values RFC3261 10.2.
+		this._call_id = Utils.createRandomToken(22);
+		this._cseq = 0;
+
+		this._to_uri = ua.configuration.uri;
+
+		this._registrationTimer = null;
+
+		// Ongoing Register request.
+		this._registering = false;
+
+		// Set status.
+		this._registered = false;
+
+		// Contact header.
+		this._contact = this._ua.contact.toString();
+
+		// Sip.ice media feature tag (RFC 5768).
+		this._contact += ';+sip.ice';
+
+		// Custom headers for REGISTER and un-REGISTER.
+		this._extraHeaders = [];
+
+		// Custom Contact header params for REGISTER and un-REGISTER.
+		this._extraContactParams = '';
+
+		// Contents of the sip.instance Contact header parameter.
+		this._sipInstance = `"<urn:uuid:${this._ua.configuration.instance_id}>"`;
+
+		this._contact += `;reg-id=${this._reg_id}`;
+		this._contact += `;+sip.instance=${this._sipInstance}`;
+	}
+
+	get registered() {
+		return this._registered;
+	}
+
+	setExtraHeaders(extraHeaders) {
+		if (!Array.isArray(extraHeaders)) {
+			extraHeaders = [];
+		}
+
+		this._extraHeaders = extraHeaders.slice();
+	}
+
+	setExtraContactParams(extraContactParams) {
+		if (!(extraContactParams instanceof Object)) {
+			extraContactParams = {};
+		}
+
+		// Reset it.
+		this._extraContactParams = '';
+
+		for (const param_key in extraContactParams) {
+			if (Object.prototype.hasOwnProperty.call(extraContactParams, param_key)) {
+				const param_value = extraContactParams[param_key];
+
+				this._extraContactParams += `;${param_key}`;
+				if (param_value) {
+					this._extraContactParams += `=${param_value}`;
+				}
+			}
+		}
+	}
+
+	register() {
+		if (this._registering) {
+			logger.debug('Register request in progress...');
+
+			return;
+		}
+
+		const extraHeaders = Utils.cloneArray(this._extraHeaders);
+
+		let contactValue;
+
+		if (this._expires) {
+			contactValue = `${this._contact};expires=${this._expires}${this._extraContactParams}`;
+			extraHeaders.push(`Expires: ${this._expires}`);
+		} else {
+			contactValue = `${this._contact}${this._extraContactParams}`;
+		}
+
+		extraHeaders.push(`Contact: ${contactValue}`);
+
+		let fromTag = Utils.newTag();
+
+		if (this._ua.configuration.register_from_tag_trail) {
+			if (
+				typeof this._ua.configuration.register_from_tag_trail === 'function'
+			) {
+				fromTag += this._ua.configuration.register_from_tag_trail();
+			} else {
+				fromTag += this._ua.configuration.register_from_tag_trail;
+			}
+		}
+
+		const request = new SIPMessage.OutgoingRequest(
+			JsSIP_C.REGISTER,
+			this._registrar,
+			this._ua,
+			{
+				to_uri: this._to_uri,
+				call_id: this._call_id,
+				cseq: (this._cseq += 1),
+				from_tag: fromTag,
+			},
+			extraHeaders
+		);
+
+		const request_sender = new RequestSender(this._ua, request, {
+			onRequestTimeout: () => {
+				this._registrationFailure(null, JsSIP_C.causes.REQUEST_TIMEOUT);
+			},
+			onTransportError: () => {
+				this._registrationFailure(null, JsSIP_C.causes.CONNECTION_ERROR);
+			},
+			// Increase the CSeq on authentication.
+			onAuthenticated: () => {
+				this._cseq += 1;
+			},
+			onReceiveResponse: response => {
+				// Discard responses to older REGISTER/un-REGISTER requests.
+				if (response.cseq !== this._cseq) {
+					return;
+				}
+
+				// Clear registration timer.
+				if (this._registrationTimer !== null) {
+					clearTimeout(this._registrationTimer);
+					this._registrationTimer = null;
+				}
+
+				switch (true) {
+					case /^1[0-9]{2}$/.test(response.status_code): {
+						// Ignore provisional responses.
+						break;
+					}
+
+					case /^2[0-9]{2}$/.test(response.status_code): {
+						this._registering = false;
+
+						if (!response.hasHeader('Contact')) {
+							logger.debug(
+								'no Contact header in response to REGISTER, response ignored'
+							);
+
+							break;
+						}
+
+						const contacts = response.headers['Contact'].reduce(
+							(a, b) => a.concat(b.parsed),
+							[]
+						);
+
+						// Get the Contact pointing to us and update the expires value accordingly.
+						// Try to find a matching Contact using sip.instance and reg-id.
+						let contact = contacts.find(
+							element =>
+								this._sipInstance === element.getParam('+sip.instance') &&
+								this._reg_id === parseInt(element.getParam('reg-id'))
+						);
+
+						// If no match was found using the sip.instance try comparing the URIs.
+						if (!contact) {
+							contact = contacts.find(
+								element => element.uri.user === this._ua.contact.uri.user
+							);
+						}
+
+						if (!contact) {
+							logger.debug(
+								'no Contact header pointing to us, response ignored'
+							);
+
+							break;
+						}
+
+						let expires = contact.getParam('expires');
+
+						if (!expires && response.hasHeader('expires')) {
+							expires = response.getHeader('expires');
+						}
+
+						if (!expires) {
+							expires = this._expires;
+						}
+
+						expires = Number(expires);
+
+						if (expires < MIN_REGISTER_EXPIRES) {
+							expires = MIN_REGISTER_EXPIRES;
+						}
+
+						const timeout =
+							expires > 64
+								? (expires * 1000) / 2 +
+									Math.floor((expires / 2 - 32) * 1000 * Math.random())
+								: expires * 1000 - 5000;
+
+						// Re-Register or emit an event before the expiration interval has elapsed.
+						// For that, decrease the expires value. ie: 3 seconds.
+						this._registrationTimer = setTimeout(() => {
+							this._registrationTimer = null;
+							// If there are no listeners for registrationExpiring, renew registration.
+							// If there are listeners, let the function listening do the register call.
+							if (this._ua.listeners('registrationExpiring').length === 0) {
+								this.register();
+							} else {
+								this._ua.emit('registrationExpiring');
+							}
+						}, timeout);
+
+						// Save gruu values.
+						if (contact.hasParam('temp-gruu')) {
+							this._ua.contact.temp_gruu = contact
+								.getParam('temp-gruu')
+								.replace(/"/g, '');
+						}
+						if (contact.hasParam('pub-gruu')) {
+							this._ua.contact.pub_gruu = contact
+								.getParam('pub-gruu')
+								.replace(/"/g, '');
+						}
+
+						if (!this._registered) {
+							this._registered = true;
+							this._ua.registered({ response });
+						}
+
+						break;
+					}
+
+					// Interval too brief RFC3261 10.2.8.
+					case /^423$/.test(response.status_code): {
+						if (response.hasHeader('min-expires')) {
+							// Increase our registration interval to the suggested minimum.
+							this._expires = Number(response.getHeader('min-expires'));
+
+							if (this._expires < MIN_REGISTER_EXPIRES) {
+								this._expires = MIN_REGISTER_EXPIRES;
+							}
+
+							// Assure register re-try with new expire.
+							this._registering = false;
+
+							// Attempt the registration again immediately.
+							this.register();
+						} else {
+							// This response MUST contain a Min-Expires header field.
+							logger.debug(
+								'423 response received for REGISTER without Min-Expires'
+							);
+
+							this._registrationFailure(
+								response,
+								JsSIP_C.causes.SIP_FAILURE_CODE
+							);
+						}
+
+						break;
+					}
+
+					default: {
+						const cause = Utils.sipErrorCause(response.status_code);
+
+						this._registrationFailure(response, cause);
+					}
+				}
+			},
+		});
+
+		this._registering = true;
+		request_sender.send();
+	}
+
+	unregister(options = {}) {
+		if (!this._registered) {
+			logger.debug('already unregistered');
+
+			return;
+		}
+
+		this._registered = false;
+
+		// Clear the registration timer.
+		if (this._registrationTimer !== null) {
+			clearTimeout(this._registrationTimer);
+			this._registrationTimer = null;
+		}
+
+		const extraHeaders = Utils.cloneArray(this._extraHeaders);
+
+		if (options.all) {
+			extraHeaders.push(`Contact: *${this._extraContactParams}`);
+		} else {
+			extraHeaders.push(
+				`Contact: ${this._contact};expires=0${this._extraContactParams}`
+			);
+		}
+
+		extraHeaders.push('Expires: 0');
+
+		const request = new SIPMessage.OutgoingRequest(
+			JsSIP_C.REGISTER,
+			this._registrar,
+			this._ua,
+			{
+				to_uri: this._to_uri,
+				call_id: this._call_id,
+				cseq: (this._cseq += 1),
+			},
+			extraHeaders
+		);
+
+		const request_sender = new RequestSender(this._ua, request, {
+			onRequestTimeout: () => {
+				this._unregistered(null, JsSIP_C.causes.REQUEST_TIMEOUT);
+			},
+			onTransportError: () => {
+				this._unregistered(null, JsSIP_C.causes.CONNECTION_ERROR);
+			},
+			// Increase the CSeq on authentication.
+			onAuthenticated: () => {
+				this._cseq += 1;
+			},
+			onReceiveResponse: response => {
+				switch (true) {
+					case /^1[0-9]{2}$/.test(response.status_code): {
+						// Ignore provisional responses.
+						break;
+					}
+					case /^2[0-9]{2}$/.test(response.status_code): {
+						this._unregistered(response);
+						break;
+					}
+					default: {
+						const cause = Utils.sipErrorCause(response.status_code);
+
+						this._unregistered(response, cause);
+					}
+				}
+			},
+		});
+
+		request_sender.send();
+	}
+
+	close() {
+		if (this._registered) {
+			this.unregister();
+		}
+	}
+
+	onTransportClosed() {
+		this._registering = false;
+		if (this._registrationTimer !== null) {
+			clearTimeout(this._registrationTimer);
+			this._registrationTimer = null;
+		}
+
+		if (this._registered) {
+			this._registered = false;
+			this._ua.unregistered({});
+		}
+	}
+
+	_registrationFailure(response, cause) {
+		this._registering = false;
+		this._ua.registrationFailed({
+			response: response || null,
+			cause,
+		});
+
+		if (this._registered) {
+			this._registered = false;
+			this._ua.unregistered({
+				response: response || null,
+				cause,
+			});
+		}
+	}
+
+	_unregistered(response, cause) {
+		this._registering = false;
+		this._registered = false;
+		this._ua.unregistered({
+			response: response || null,
+			cause: cause || null,
+		});
+	}
 };
diff --git a/src/RequestSender.js b/src/RequestSender.js
index f4e55ba..c7ddc35 100644
--- a/src/RequestSender.js
+++ b/src/RequestSender.js
@@ -7,163 +7,176 @@ const logger = new Logger('RequestSender');

 // Default event handlers.
 const EventHandlers = {
-  onRequestTimeout  : () => {},
-  onTransportError  : () => {},
-  onReceiveResponse : () => {},
-  onAuthenticated   : () => {}
+	onRequestTimeout: () => {},
+	onTransportError: () => {},
+	onReceiveResponse: () => {},
+	onAuthenticated: () => {},
 };

-module.exports = class RequestSender
-{
-  constructor(ua, request, eventHandlers)
-  {
-    this._ua = ua;
-    this._eventHandlers = eventHandlers;
-    this._method = request.method;
-    this._request = request;
-    this._auth = null;
-    this._challenged = false;
-    this._staled = false;
-
-    // Define the undefined handlers.
-    for (const handler in EventHandlers)
-    {
-      if (Object.prototype.hasOwnProperty.call(EventHandlers, handler))
-      {
-        if (!this._eventHandlers[handler])
-        {
-          this._eventHandlers[handler] = EventHandlers[handler];
-        }
-      }
-    }
-
-    // If ua is in closing process or even closed just allow sending Bye and ACK.
-    if (ua.status === ua.C.STATUS_USER_CLOSED &&
-        (this._method !== JsSIP_C.BYE || this._method !== JsSIP_C.ACK))
-    {
-      this._eventHandlers.onTransportError();
-    }
-  }
-
-  /**
-  * Create the client transaction and send the message.
-  */
-  send()
-  {
-    const eventHandlers = {
-      onRequestTimeout  : () => { this._eventHandlers.onRequestTimeout(); },
-      onTransportError  : () => { this._eventHandlers.onTransportError(); },
-      onReceiveResponse : (response) => { this._receiveResponse(response); }
-    };
-
-    switch (this._method)
-    {
-      case 'INVITE':
-        this.clientTransaction = new Transactions.InviteClientTransaction(
-          this._ua, this._ua.transport, this._request, eventHandlers);
-        break;
-      case 'ACK':
-        this.clientTransaction = new Transactions.AckClientTransaction(
-          this._ua, this._ua.transport, this._request, eventHandlers);
-        break;
-      default:
-        this.clientTransaction = new Transactions.NonInviteClientTransaction(
-          this._ua, this._ua.transport, this._request, eventHandlers);
-    }
-    // If authorization JWT is present, use it.
-    if (this._ua._configuration.authorization_jwt)
-    {
-      this._request.setHeader('Authorization', this._ua._configuration.authorization_jwt);
-    }
-
-    this.clientTransaction.send();
-  }
-
-  /**
-  * Called from client transaction when receiving a correct response to the request.
-  * Authenticate request if needed or pass the response back to the applicant.
-  */
-  _receiveResponse(response)
-  {
-    let challenge;
-    let authorization_header_name;
-    const status_code = response.status_code;
-
-    /*
-    * Authentication
-    * Authenticate once. _challenged_ flag used to avoid infinite authentications.
-    */
-    if ((status_code === 401 || status_code === 407) &&
-        (this._ua.configuration.password !== null || this._ua.configuration.ha1 !== null))
-    {
-
-      // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header.
-      if (response.status_code === 401)
-      {
-        challenge = response.parseHeader('www-authenticate');
-        authorization_header_name = 'authorization';
-      }
-      else
-      {
-        challenge = response.parseHeader('proxy-authenticate');
-        authorization_header_name = 'proxy-authorization';
-      }
-
-      // Verify it seems a valid challenge.
-      if (!challenge)
-      {
-        logger.debug(`${response.status_code} with wrong or missing challenge, cannot authenticate`);
-        this._eventHandlers.onReceiveResponse(response);
-
-        return;
-      }
-
-      if (!this._challenged || (!this._staled && challenge.stale === true))
-      {
-        if (!this._auth)
-        {
-          this._auth = new DigestAuthentication({
-            username : this._ua.configuration.authorization_user,
-            password : this._ua.configuration.password,
-            realm    : this._ua.configuration.realm,
-            ha1      : this._ua.configuration.ha1
-          });
-        }
-
-        // Verify that the challenge is really valid.
-        if (!this._auth.authenticate(this._request, challenge))
-        {
-          this._eventHandlers.onReceiveResponse(response);
-
-          return;
-        }
-        this._challenged = true;
-
-        // Update ha1 and realm in the UA.
-        this._ua.set('realm', this._auth.get('realm'));
-        this._ua.set('ha1', this._auth.get('ha1'));
-
-        if (challenge.stale)
-        {
-          this._staled = true;
-        }
-
-        this._request = this._request.clone();
-        this._request.cseq += 1;
-        this._request.setHeader('cseq', `${this._request.cseq} ${this._method}`);
-        this._request.setHeader(authorization_header_name, this._auth.toString());
-
-        this._eventHandlers.onAuthenticated(this._request);
-        this.send();
-      }
-      else
-      {
-        this._eventHandlers.onReceiveResponse(response);
-      }
-    }
-    else
-    {
-      this._eventHandlers.onReceiveResponse(response);
-    }
-  }
+module.exports = class RequestSender {
+	constructor(ua, request, eventHandlers) {
+		this._ua = ua;
+		this._eventHandlers = eventHandlers;
+		this._method = request.method;
+		this._request = request;
+		this._auth = null;
+		this._challenged = false;
+		this._staled = false;
+
+		// Define the undefined handlers.
+		for (const handler in EventHandlers) {
+			if (Object.prototype.hasOwnProperty.call(EventHandlers, handler)) {
+				if (!this._eventHandlers[handler]) {
+					this._eventHandlers[handler] = EventHandlers[handler];
+				}
+			}
+		}
+
+		// If ua is in closing process or even closed just allow sending Bye and ACK.
+		if (
+			ua.status === ua.C.STATUS_USER_CLOSED &&
+			(this._method !== JsSIP_C.BYE || this._method !== JsSIP_C.ACK)
+		) {
+			this._eventHandlers.onTransportError();
+		}
+	}
+
+	/**
+	 * Create the client transaction and send the message.
+	 */
+	send() {
+		const eventHandlers = {
+			onRequestTimeout: () => {
+				this._eventHandlers.onRequestTimeout();
+			},
+			onTransportError: () => {
+				this._eventHandlers.onTransportError();
+			},
+			onReceiveResponse: response => {
+				this._receiveResponse(response);
+			},
+		};
+
+		switch (this._method) {
+			case 'INVITE': {
+				this.clientTransaction = new Transactions.InviteClientTransaction(
+					this._ua,
+					this._ua.transport,
+					this._request,
+					eventHandlers
+				);
+				break;
+			}
+			case 'ACK': {
+				this.clientTransaction = new Transactions.AckClientTransaction(
+					this._ua,
+					this._ua.transport,
+					this._request,
+					eventHandlers
+				);
+				break;
+			}
+			default: {
+				this.clientTransaction = new Transactions.NonInviteClientTransaction(
+					this._ua,
+					this._ua.transport,
+					this._request,
+					eventHandlers
+				);
+			}
+		}
+		// If authorization JWT is present, use it.
+		if (this._ua._configuration.authorization_jwt) {
+			this._request.setHeader(
+				'Authorization',
+				this._ua._configuration.authorization_jwt
+			);
+		}
+
+		this.clientTransaction.send();
+	}
+
+	/**
+	 * Called from client transaction when receiving a correct response to the request.
+	 * Authenticate request if needed or pass the response back to the applicant.
+	 */
+	_receiveResponse(response) {
+		let challenge;
+		let authorization_header_name;
+		const status_code = response.status_code;
+
+		/*
+		 * Authentication
+		 * Authenticate once. _challenged_ flag used to avoid infinite authentications.
+		 */
+		if (
+			(status_code === 401 || status_code === 407) &&
+			(this._ua.configuration.password !== null ||
+				this._ua.configuration.ha1 !== null)
+		) {
+			// Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header.
+			if (response.status_code === 401) {
+				challenge = response.parseHeader('www-authenticate');
+				authorization_header_name = 'authorization';
+			} else {
+				challenge = response.parseHeader('proxy-authenticate');
+				authorization_header_name = 'proxy-authorization';
+			}
+
+			// Verify it seems a valid challenge.
+			if (!challenge) {
+				logger.debug(
+					`${response.status_code} with wrong or missing challenge, cannot authenticate`
+				);
+				this._eventHandlers.onReceiveResponse(response);
+
+				return;
+			}
+
+			if (!this._challenged || (!this._staled && challenge.stale === true)) {
+				if (!this._auth) {
+					this._auth = new DigestAuthentication({
+						username: this._ua.configuration.authorization_user,
+						password: this._ua.configuration.password,
+						realm: this._ua.configuration.realm,
+						ha1: this._ua.configuration.ha1,
+					});
+				}
+
+				// Verify that the challenge is really valid.
+				if (!this._auth.authenticate(this._request, challenge)) {
+					this._eventHandlers.onReceiveResponse(response);
+
+					return;
+				}
+				this._challenged = true;
+
+				// Update ha1 and realm in the UA.
+				this._ua.set('realm', this._auth.get('realm'));
+				this._ua.set('ha1', this._auth.get('ha1'));
+
+				if (challenge.stale) {
+					this._staled = true;
+				}
+
+				this._request = this._request.clone();
+				this._request.cseq += 1;
+				this._request.setHeader(
+					'cseq',
+					`${this._request.cseq} ${this._method}`
+				);
+				this._request.setHeader(
+					authorization_header_name,
+					this._auth.toString()
+				);
+
+				this._eventHandlers.onAuthenticated(this._request);
+				this.send();
+			} else {
+				this._eventHandlers.onReceiveResponse(response);
+			}
+		} else {
+			this._eventHandlers.onReceiveResponse(response);
+		}
+	}
 };
diff --git a/src/SIPMessage.d.ts b/src/SIPMessage.d.ts
index 9b73733..31a20a9 100644
--- a/src/SIPMessage.d.ts
+++ b/src/SIPMessage.d.ts
@@ -1,52 +1,52 @@
-import {NameAddrHeader} from './NameAddrHeader'
-import {URI} from './URI'
+import { NameAddrHeader } from './NameAddrHeader';
+import { URI } from './URI';

 declare class IncomingMessage {
-  method: string
-  from: NameAddrHeader
-  to: NameAddrHeader
-  body: string
+	method: string;
+	from: NameAddrHeader;
+	to: NameAddrHeader;
+	body: string;

-  constructor();
+	constructor();

-  countHeader(name: string): number;
+	countHeader(name: string): number;

-  getHeader(name: string): string;
+	getHeader(name: string): string;

-  getHeaders(name: string): string[];
+	getHeaders(name: string): string[];

-  hasHeader(name: string): boolean;
+	hasHeader(name: string): boolean;

-  parseHeader<T = unknown>(name: string, idx?: number): T;
+	parseHeader<T = unknown>(name: string, idx?: number): T;

-  toString(): string;
+	toString(): string;
 }

 export class IncomingRequest extends IncomingMessage {
-  ruri: URI
+	ruri: URI;
 }

 export class IncomingResponse extends IncomingMessage {
-  status_code: number
-  reason_phrase: string
+	status_code: number;
+	reason_phrase: string;
 }

 export class OutgoingRequest {
-  method: string
-  ruri: URI
-  cseq: number
-  call_id: string
-  from: NameAddrHeader
-  to: NameAddrHeader
-  body: string
+	method: string;
+	ruri: URI;
+	cseq: number;
+	call_id: string;
+	from: NameAddrHeader;
+	to: NameAddrHeader;
+	body: string;

-  setHeader(name: string, value: string | string[]): void;
+	setHeader(name: string, value: string | string[]): void;

-  getHeader(name: string): string;
+	getHeader(name: string): string;

-  getHeaders(name: string): string[];
+	getHeaders(name: string): string[];

-  hasHeader(name: string): boolean;
+	hasHeader(name: string): boolean;

-  toString(): string;
+	toString(): string;
 }
diff --git a/src/SIPMessage.js b/src/SIPMessage.js
index 15d46e5..e2d205f 100644
--- a/src/SIPMessage.js
+++ b/src/SIPMessage.js
@@ -17,768 +17,668 @@ const logger = new Logger('SIPMessage');
  * -param {Object} [headers] extra headers
  * -param {String} [body]
  */
-class OutgoingRequest
-{
-  constructor(method, ruri, ua, params, extraHeaders, body)
-  {
-    // Mandatory parameters check.
-    if (!method || !ruri || !ua)
-    {
-      return null;
-    }
-
-    params = params || {};
-
-    this.ua = ua;
-    this.headers = {};
-    this.method = method;
-    this.ruri = ruri;
-    this.body = body;
-    this.extraHeaders = Utils.cloneArray(extraHeaders);
-
-    if (this.ua.configuration.extra_headers)
-    {
-      this.extraHeaders = this.extraHeaders.concat(this.ua.configuration.extra_headers);
-    }
-
-    // Fill the Common SIP Request Headers.
-
-    // Route.
-    if (params.route_set)
-    {
-      this.setHeader('route', params.route_set);
-    }
-    else if (ua.configuration.use_preloaded_route)
-    {
-      this.setHeader('route', `<${ua.transport.sip_uri};lr>`);
-    }
-
-    // Via.
-    // Empty Via header. Will be filled by the client transaction.
-    this.setHeader('via', '');
-
-    // Max-Forwards.
-    this.setHeader('max-forwards', JsSIP_C.MAX_FORWARDS);
-
-    // To
-    const to_uri = params.to_uri || ruri;
-    const to_params = params.to_tag ? { tag: params.to_tag } : null;
-    const to_display_name = typeof params.to_display_name !== 'undefined' ? params.to_display_name : null;
-
-    this.to = new NameAddrHeader(to_uri, to_display_name, to_params);
-    this.setHeader('to', this.to.toString());
-
-    // From.
-    const from_uri = params.from_uri || ua.configuration.uri;
-    const from_params = { tag: params.from_tag || Utils.newTag() };
-    let display_name;
-
-    if (typeof params.from_display_name !== 'undefined')
-    {
-      display_name = params.from_display_name;
-    }
-    else if (ua.configuration.display_name)
-    {
-      display_name = ua.configuration.display_name;
-    }
-    else
-    {
-      display_name = null;
-    }
-
-    this.from = new NameAddrHeader(from_uri, display_name, from_params);
-    this.setHeader('from', this.from.toString());
-
-    // Call-ID.
-    const call_id = params.call_id ||
-      (ua.configuration.jssip_id + Utils.createRandomToken(15));
-
-    this.call_id = call_id;
-    this.setHeader('call-id', call_id);
-
-    // CSeq.
-    const cseq = params.cseq || Math.floor(Math.random() * 10000);
-
-    this.cseq = cseq;
-    this.setHeader('cseq', `${cseq} ${method}`);
-  }
-
-  /**
-   * Replace the the given header by the given value.
-   * -param {String} name header name
-   * -param {String | Array} value header value
-   */
-  setHeader(name, value)
-  {
-    // Remove the header from extraHeaders if present.
-    const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
-    for (let idx=0; idx<this.extraHeaders.length; idx++)
-    {
-      if (regexp.test(this.extraHeaders[idx]))
-      {
-        this.extraHeaders.splice(idx, 1);
-      }
-    }
-
-    this.headers[Utils.headerize(name)] = (Array.isArray(value)) ? value : [ value ];
-  }
-
-  /**
-   * Get the value of the given header name at the given position.
-   * -param {String} name header name
-   * -returns {String|undefined} Returns the specified header, null if header doesn't exist.
-   */
-  getHeader(name)
-  {
-    const headers = this.headers[Utils.headerize(name)];
-
-    if (headers)
-    {
-      if (headers[0])
-      {
-        return headers[0];
-      }
-    }
-    else
-    {
-      const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
-      for (const header of this.extraHeaders)
-      {
-        if (regexp.test(header))
-        {
-          return header.substring(header.indexOf(':')+1).trim();
-        }
-      }
-    }
-
-    return;
-  }
-
-  /**
-   * Get the header/s of the given name.
-   * -param {String} name header name
-   * -returns {Array} Array with all the headers of the specified name.
-   */
-  getHeaders(name)
-  {
-    const headers = this.headers[Utils.headerize(name)];
-    const result = [];
-
-    if (headers)
-    {
-      for (const header of headers)
-      {
-        result.push(header);
-      }
-
-      return result;
-    }
-    else
-    {
-      const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
-      for (const header of this.extraHeaders)
-      {
-        if (regexp.test(header))
-        {
-          result.push(header.substring(header.indexOf(':')+1).trim());
-        }
-      }
-
-      return result;
-    }
-  }
-
-  /**
-   * Verify the existence of the given header.
-   * -param {String} name header name
-   * -returns {boolean} true if header with given name exists, false otherwise
-   */
-  hasHeader(name)
-  {
-    if (this.headers[Utils.headerize(name)])
-    {
-      return true;
-    }
-    else
-    {
-      const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
-      for (const header of this.extraHeaders)
-      {
-        if (regexp.test(header))
-        {
-          return true;
-        }
-      }
-    }
-
-    return false;
-  }
-
-  /**
-   * Parse the current body as a SDP and store the resulting object
-   * into this.sdp.
-   * -param {Boolean} force: Parse even if this.sdp already exists.
-   *
-   * Returns this.sdp.
-   */
-  parseSDP(force)
-  {
-    if (!force && this.sdp)
-    {
-      return this.sdp;
-    }
-    else
-    {
-      this.sdp = sdp_transform.parse(this.body || '');
-
-      return this.sdp;
-    }
-  }
-
-  toString()
-  {
-    let msg = `${this.method} ${this.ruri} SIP/2.0\r\n`;
-
-    for (const headerName in this.headers)
-    {
-      if (Object.prototype.hasOwnProperty.call(this.headers, headerName))
-      {
-        for (const headerValue of this.headers[headerName])
-        {
-          msg += `${headerName}: ${headerValue}\r\n`;
-        }
-      }
-    }
-
-    for (const header of this.extraHeaders)
-    {
-      msg += `${header.trim()}\r\n`;
-    }
-
-    // Supported.
-    const supported = [];
-
-    switch (this.method)
-    {
-      case JsSIP_C.REGISTER:
-        supported.push('path', 'gruu');
-        break;
-      case JsSIP_C.INVITE:
-        if (this.ua.configuration.session_timers)
-        {
-          supported.push('timer');
-        }
-        if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu)
-        {
-          supported.push('gruu');
-        }
-        supported.push('ice', 'replaces');
-        break;
-      case JsSIP_C.UPDATE:
-        if (this.ua.configuration.session_timers)
-        {
-          supported.push('timer');
-        }
-        supported.push('ice');
-        break;
-    }
-
-    supported.push('outbound');
-
-    const userAgent = this.ua.configuration.user_agent || JsSIP_C.USER_AGENT;
-
-    // Allow.
-    msg += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
-    msg += `Supported: ${supported}\r\n`;
-    msg += `User-Agent: ${userAgent}\r\n`;
-
-    if (this.body)
-    {
-      const length = Utils.str_utf8_length(this.body);
-
-      msg += `Content-Length: ${length}\r\n\r\n`;
-      msg += this.body;
-    }
-    else
-    {
-      msg += 'Content-Length: 0\r\n\r\n';
-    }
-
-    return msg;
-  }
-
-  clone()
-  {
-    const request = new OutgoingRequest(this.method, this.ruri, this.ua);
-
-    Object.keys(this.headers).forEach(function(name)
-    {
-      request.headers[name] = this.headers[name].slice();
-    }, this);
-
-    request.body = this.body;
-    request.extraHeaders = Utils.cloneArray(this.extraHeaders);
-    request.to = this.to;
-    request.from = this.from;
-    request.call_id = this.call_id;
-    request.cseq = this.cseq;
-
-    return request;
-  }
+class OutgoingRequest {
+	constructor(method, ruri, ua, params, extraHeaders, body) {
+		// Mandatory parameters check.
+		if (!method || !ruri || !ua) {
+			return null;
+		}
+
+		params = params || {};
+
+		this.ua = ua;
+		this.headers = {};
+		this.method = method;
+		this.ruri = ruri;
+		this.body = body;
+		this.extraHeaders = Utils.cloneArray(extraHeaders);
+
+		if (this.ua.configuration.extra_headers) {
+			this.extraHeaders = this.extraHeaders.concat(
+				this.ua.configuration.extra_headers
+			);
+		}
+
+		// Fill the Common SIP Request Headers.
+
+		// Route.
+		if (params.route_set) {
+			this.setHeader('route', params.route_set);
+		} else if (ua.configuration.use_preloaded_route) {
+			this.setHeader('route', `<${ua.transport.sip_uri};lr>`);
+		}
+
+		// Via.
+		// Empty Via header. Will be filled by the client transaction.
+		this.setHeader('via', '');
+
+		// Max-Forwards.
+		this.setHeader('max-forwards', JsSIP_C.MAX_FORWARDS);
+
+		// To
+		const to_uri = params.to_uri || ruri;
+		const to_params = params.to_tag ? { tag: params.to_tag } : null;
+		const to_display_name =
+			typeof params.to_display_name !== 'undefined'
+				? params.to_display_name
+				: null;
+
+		this.to = new NameAddrHeader(to_uri, to_display_name, to_params);
+		this.setHeader('to', this.to.toString());
+
+		// From.
+		const from_uri = params.from_uri || ua.configuration.uri;
+		const from_params = { tag: params.from_tag || Utils.newTag() };
+		let display_name;
+
+		if (typeof params.from_display_name !== 'undefined') {
+			display_name = params.from_display_name;
+		} else if (ua.configuration.display_name) {
+			display_name = ua.configuration.display_name;
+		} else {
+			display_name = null;
+		}
+
+		this.from = new NameAddrHeader(from_uri, display_name, from_params);
+		this.setHeader('from', this.from.toString());
+
+		// Call-ID.
+		const call_id =
+			params.call_id || ua.configuration.jssip_id + Utils.createRandomToken(15);
+
+		this.call_id = call_id;
+		this.setHeader('call-id', call_id);
+
+		// CSeq.
+		const cseq = params.cseq || Math.floor(Math.random() * 10000);
+
+		this.cseq = cseq;
+		this.setHeader('cseq', `${cseq} ${method}`);
+	}
+
+	/**
+	 * Replace the the given header by the given value.
+	 * -param {String} name header name
+	 * -param {String | Array} value header value
+	 */
+	setHeader(name, value) {
+		// Remove the header from extraHeaders if present.
+		const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+		for (let idx = 0; idx < this.extraHeaders.length; idx++) {
+			if (regexp.test(this.extraHeaders[idx])) {
+				this.extraHeaders.splice(idx, 1);
+			}
+		}
+
+		this.headers[Utils.headerize(name)] = Array.isArray(value)
+			? value
+			: [value];
+	}
+
+	/**
+	 * Get the value of the given header name at the given position.
+	 * -param {String} name header name
+	 * -returns {String|undefined} Returns the specified header, null if header doesn't exist.
+	 */
+	getHeader(name) {
+		const headers = this.headers[Utils.headerize(name)];
+
+		if (headers) {
+			if (headers[0]) {
+				return headers[0];
+			}
+		} else {
+			const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+			for (const header of this.extraHeaders) {
+				if (regexp.test(header)) {
+					return header.substring(header.indexOf(':') + 1).trim();
+				}
+			}
+		}
+
+		return;
+	}
+
+	/**
+	 * Get the header/s of the given name.
+	 * -param {String} name header name
+	 * -returns {Array} Array with all the headers of the specified name.
+	 */
+	getHeaders(name) {
+		const headers = this.headers[Utils.headerize(name)];
+		const result = [];
+
+		if (headers) {
+			for (const header of headers) {
+				result.push(header);
+			}
+
+			return result;
+		} else {
+			const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+			for (const header of this.extraHeaders) {
+				if (regexp.test(header)) {
+					result.push(header.substring(header.indexOf(':') + 1).trim());
+				}
+			}
+
+			return result;
+		}
+	}
+
+	/**
+	 * Verify the existence of the given header.
+	 * -param {String} name header name
+	 * -returns {boolean} true if header with given name exists, false otherwise
+	 */
+	hasHeader(name) {
+		if (this.headers[Utils.headerize(name)]) {
+			return true;
+		} else {
+			const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+			for (const header of this.extraHeaders) {
+				if (regexp.test(header)) {
+					return true;
+				}
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Parse the current body as a SDP and store the resulting object
+	 * into this.sdp.
+	 * -param {Boolean} force: Parse even if this.sdp already exists.
+	 *
+	 * Returns this.sdp.
+	 */
+	parseSDP(force) {
+		if (!force && this.sdp) {
+			return this.sdp;
+		} else {
+			this.sdp = sdp_transform.parse(this.body || '');
+
+			return this.sdp;
+		}
+	}
+
+	toString() {
+		let msg = `${this.method} ${this.ruri} SIP/2.0\r\n`;
+
+		for (const headerName in this.headers) {
+			if (Object.prototype.hasOwnProperty.call(this.headers, headerName)) {
+				for (const headerValue of this.headers[headerName]) {
+					msg += `${headerName}: ${headerValue}\r\n`;
+				}
+			}
+		}
+
+		for (const header of this.extraHeaders) {
+			msg += `${header.trim()}\r\n`;
+		}
+
+		// Supported.
+		const supported = [];
+
+		switch (this.method) {
+			case JsSIP_C.REGISTER: {
+				supported.push('path', 'gruu');
+				break;
+			}
+			case JsSIP_C.INVITE: {
+				if (this.ua.configuration.session_timers) {
+					supported.push('timer');
+				}
+				if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
+					supported.push('gruu');
+				}
+				supported.push('ice', 'replaces');
+				break;
+			}
+			case JsSIP_C.UPDATE: {
+				if (this.ua.configuration.session_timers) {
+					supported.push('timer');
+				}
+				supported.push('ice');
+				break;
+			}
+		}
+
+		supported.push('outbound');
+
+		const userAgent = this.ua.configuration.user_agent || JsSIP_C.USER_AGENT;
+
+		// Allow.
+		msg += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+		msg += `Supported: ${supported}\r\n`;
+		msg += `User-Agent: ${userAgent}\r\n`;
+
+		if (this.body) {
+			const length = Utils.str_utf8_length(this.body);
+
+			msg += `Content-Length: ${length}\r\n\r\n`;
+			msg += this.body;
+		} else {
+			msg += 'Content-Length: 0\r\n\r\n';
+		}
+
+		return msg;
+	}
+
+	clone() {
+		const request = new OutgoingRequest(this.method, this.ruri, this.ua);
+
+		Object.keys(this.headers).forEach(function (name) {
+			request.headers[name] = this.headers[name].slice();
+		}, this);
+
+		request.body = this.body;
+		request.extraHeaders = Utils.cloneArray(this.extraHeaders);
+		request.to = this.to;
+		request.from = this.from;
+		request.call_id = this.call_id;
+		request.cseq = this.cseq;
+
+		return request;
+	}
 }

-class InitialOutgoingInviteRequest extends OutgoingRequest
-{
-  constructor(ruri, ua, params, extraHeaders, body)
-  {
-    super(JsSIP_C.INVITE, ruri, ua, params, extraHeaders, body);
+class InitialOutgoingInviteRequest extends OutgoingRequest {
+	constructor(ruri, ua, params, extraHeaders, body) {
+		super(JsSIP_C.INVITE, ruri, ua, params, extraHeaders, body);

-    this.transaction = null;
-  }
+		this.transaction = null;
+	}

-  cancel(reason)
-  {
-    this.transaction.cancel(reason);
-  }
+	cancel(reason) {
+		this.transaction.cancel(reason);
+	}

-  clone()
-  {
-    const request = new InitialOutgoingInviteRequest(this.ruri, this.ua);
+	clone() {
+		const request = new InitialOutgoingInviteRequest(this.ruri, this.ua);

-    Object.keys(this.headers).forEach(function(name)
-    {
-      request.headers[name] = this.headers[name].slice();
-    }, this);
+		Object.keys(this.headers).forEach(function (name) {
+			request.headers[name] = this.headers[name].slice();
+		}, this);

-    request.body = this.body;
-    request.extraHeaders = Utils.cloneArray(this.extraHeaders);
-    request.to = this.to;
-    request.from = this.from;
-    request.call_id = this.call_id;
-    request.cseq = this.cseq;
+		request.body = this.body;
+		request.extraHeaders = Utils.cloneArray(this.extraHeaders);
+		request.to = this.to;
+		request.from = this.from;
+		request.call_id = this.call_id;
+		request.cseq = this.cseq;

-    request.transaction = this.transaction;
+		request.transaction = this.transaction;

-    return request;
-  }
+		return request;
+	}
 }

-class IncomingMessage
-{
-  constructor()
-  {
-    this.data = null;
-    this.headers = null;
-    this.method = null;
-    this.via = null;
-    this.via_branch = null;
-    this.call_id = null;
-    this.cseq = null;
-    this.from = null;
-    this.from_tag = null;
-    this.to = null;
-    this.to_tag = null;
-    this.body = null;
-    this.sdp = null;
-  }
-
-  /**
-  * Insert a header of the given name and value into the last position of the
-  * header array.
-  */
-  addHeader(name, value)
-  {
-    const header = { raw: value };
-
-    name = Utils.headerize(name);
-
-    if (this.headers[name])
-    {
-      this.headers[name].push(header);
-    }
-    else
-    {
-      this.headers[name] = [ header ];
-    }
-  }
-
-  /**
-   * Get the value of the given header name at the given position.
-   */
-  getHeader(name)
-  {
-    const header = this.headers[Utils.headerize(name)];
-
-    if (header)
-    {
-      if (header[0])
-      {
-        return header[0].raw;
-      }
-    }
-    else
-    {
-      return;
-    }
-  }
-
-  /**
-   * Get the header/s of the given name.
-   */
-  getHeaders(name)
-  {
-    const headers = this.headers[Utils.headerize(name)];
-    const result = [];
-
-    if (!headers)
-    {
-      return [];
-    }
-
-    for (const header of headers)
-    {
-      result.push(header.raw);
-    }
-
-    return result;
-  }
-
-  /**
-   * Verify the existence of the given header.
-   */
-  hasHeader(name)
-  {
-    return (this.headers[Utils.headerize(name)]) ? true : false;
-  }
-
-  /**
-  * Parse the given header on the given index.
-  * -param {String} name header name
-  * -param {Number} [idx=0] header index
-  * -returns {Object|undefined} Parsed header object, undefined if the header
-  *  is not present or in case of a parsing error.
-  */
-  parseHeader(name, idx = 0)
-  {
-    name = Utils.headerize(name);
-
-    if (!this.headers[name])
-    {
-      logger.debug(`header "${name}" not present`);
-
-      return;
-    }
-    else if (idx >= this.headers[name].length)
-    {
-      logger.debug(`not so many "${name}" headers present`);
-
-      return;
-    }
-
-    const header = this.headers[name][idx];
-    const value = header.raw;
-
-    if (header.parsed)
-    {
-      return header.parsed;
-    }
-
-    // Substitute '-' by '_' for grammar rule matching.
-    const parsed = Grammar.parse(value, name.replace(/-/g, '_'));
-
-    if (parsed === -1)
-    {
-      this.headers[name].splice(idx, 1); // delete from headers
-      logger.debug(`error parsing "${name}" header field with value "${value}"`);
-
-      return;
-    }
-    else
-    {
-      header.parsed = parsed;
-
-      return parsed;
-    }
-  }
-
-  /**
-   * Message Header attribute selector. Alias of parseHeader.
-   * -param {String} name header name
-   * -param {Number} [idx=0] header index
-   * -returns {Object|undefined} Parsed header object, undefined if the header
-   *  is not present or in case of a parsing error.
-   *
-   * -example
-   * message.s('via',3).port
-   */
-  s(name, idx)
-  {
-    return this.parseHeader(name, idx);
-  }
-
-  /**
-  * Replace the value of the given header by the value.
-  * -param {String} name header name
-  * -param {String} value header value
-  */
-  setHeader(name, value)
-  {
-    const header = { raw: value };
-
-    this.headers[Utils.headerize(name)] = [ header ];
-  }
-
-  /**
-   * Parse the current body as a SDP and store the resulting object
-   * into this.sdp.
-   * -param {Boolean} force: Parse even if this.sdp already exists.
-   *
-   * Returns this.sdp.
-   */
-  parseSDP(force)
-  {
-    if (!force && this.sdp)
-    {
-      return this.sdp;
-    }
-    else
-    {
-      this.sdp = sdp_transform.parse(this.body || '');
-
-      return this.sdp;
-    }
-  }
-
-  toString()
-  {
-    return this.data;
-  }
+class IncomingMessage {
+	constructor() {
+		this.data = null;
+		this.headers = null;
+		this.method = null;
+		this.via = null;
+		this.via_branch = null;
+		this.call_id = null;
+		this.cseq = null;
+		this.from = null;
+		this.from_tag = null;
+		this.to = null;
+		this.to_tag = null;
+		this.body = null;
+		this.sdp = null;
+	}
+
+	/**
+	 * Insert a header of the given name and value into the last position of the
+	 * header array.
+	 */
+	addHeader(name, value) {
+		const header = { raw: value };
+
+		name = Utils.headerize(name);
+
+		if (this.headers[name]) {
+			this.headers[name].push(header);
+		} else {
+			this.headers[name] = [header];
+		}
+	}
+
+	/**
+	 * Get the value of the given header name at the given position.
+	 */
+	getHeader(name) {
+		const header = this.headers[Utils.headerize(name)];
+
+		if (header) {
+			if (header[0]) {
+				return header[0].raw;
+			}
+		} else {
+			return;
+		}
+	}
+
+	/**
+	 * Get the header/s of the given name.
+	 */
+	getHeaders(name) {
+		const headers = this.headers[Utils.headerize(name)];
+		const result = [];
+
+		if (!headers) {
+			return [];
+		}
+
+		for (const header of headers) {
+			result.push(header.raw);
+		}
+
+		return result;
+	}
+
+	/**
+	 * Verify the existence of the given header.
+	 */
+	hasHeader(name) {
+		return this.headers[Utils.headerize(name)] ? true : false;
+	}
+
+	/**
+	 * Parse the given header on the given index.
+	 * -param {String} name header name
+	 * -param {Number} [idx=0] header index
+	 * -returns {Object|undefined} Parsed header object, undefined if the header
+	 *  is not present or in case of a parsing error.
+	 */
+	parseHeader(name, idx = 0) {
+		name = Utils.headerize(name);
+
+		if (!this.headers[name]) {
+			logger.debug(`header "${name}" not present`);
+
+			return;
+		} else if (idx >= this.headers[name].length) {
+			logger.debug(`not so many "${name}" headers present`);
+
+			return;
+		}
+
+		const header = this.headers[name][idx];
+		const value = header.raw;
+
+		if (header.parsed) {
+			return header.parsed;
+		}
+
+		// Substitute '-' by '_' for grammar rule matching.
+		const parsed = Grammar.parse(value, name.replace(/-/g, '_'));
+
+		if (parsed === -1) {
+			this.headers[name].splice(idx, 1); // delete from headers
+			logger.debug(
+				`error parsing "${name}" header field with value "${value}"`
+			);
+
+			return;
+		} else {
+			header.parsed = parsed;
+
+			return parsed;
+		}
+	}
+
+	/**
+	 * Message Header attribute selector. Alias of parseHeader.
+	 * -param {String} name header name
+	 * -param {Number} [idx=0] header index
+	 * -returns {Object|undefined} Parsed header object, undefined if the header
+	 *  is not present or in case of a parsing error.
+	 *
+	 * -example
+	 * message.s('via',3).port
+	 */
+	s(name, idx) {
+		return this.parseHeader(name, idx);
+	}
+
+	/**
+	 * Replace the value of the given header by the value.
+	 * -param {String} name header name
+	 * -param {String} value header value
+	 */
+	setHeader(name, value) {
+		const header = { raw: value };
+
+		this.headers[Utils.headerize(name)] = [header];
+	}
+
+	/**
+	 * Parse the current body as a SDP and store the resulting object
+	 * into this.sdp.
+	 * -param {Boolean} force: Parse even if this.sdp already exists.
+	 *
+	 * Returns this.sdp.
+	 */
+	parseSDP(force) {
+		if (!force && this.sdp) {
+			return this.sdp;
+		} else {
+			this.sdp = sdp_transform.parse(this.body || '');
+
+			return this.sdp;
+		}
+	}
+
+	toString() {
+		return this.data;
+	}
 }

-class IncomingRequest extends IncomingMessage
-{
-  constructor(ua)
-  {
-    super();
-
-    this.ua = ua;
-    this.headers = {};
-    this.ruri = null;
-    this.transport = null;
-    this.server_transaction = null;
-  }
-
-  /**
-  * Stateful reply.
-  * -param {Number} code status code
-  * -param {String} reason reason phrase
-  * -param {Object} headers extra headers
-  * -param {String} body body
-  * -param {Function} [onSuccess] onSuccess callback
-  * -param {Function} [onFailure] onFailure callback
-  */
-  reply(code, reason, extraHeaders, body, onSuccess, onFailure)
-  {
-    const supported = [];
-    let to = this.getHeader('To');
-
-    code = code || null;
-    reason = reason || null;
-
-    // Validate code and reason values.
-    if (!code || (code < 100 || code > 699))
-    {
-      throw new TypeError(`Invalid status_code: ${code}`);
-    }
-    else if (reason && typeof reason !== 'string' && !(reason instanceof String))
-    {
-      throw new TypeError(`Invalid reason_phrase: ${reason}`);
-    }
-
-    reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
-    extraHeaders = Utils.cloneArray(extraHeaders);
-
-    if (this.ua.configuration.extra_headers)
-    {
-      extraHeaders = extraHeaders.concat(this.ua.configuration.extra_headers);
-    }
-
-    let response = `SIP/2.0 ${code} ${reason}\r\n`;
-
-    if (this.method === JsSIP_C.INVITE && code > 100 && code <= 200)
-    {
-      const headers = this.getHeaders('record-route');
-
-      for (const header of headers)
-      {
-        response += `Record-Route: ${header}\r\n`;
-      }
-    }
-
-    const vias = this.getHeaders('via');
-
-    for (const via of vias)
-    {
-      response += `Via: ${via}\r\n`;
-    }
-
-    if (!this.to_tag && code > 100)
-    {
-      to += `;tag=${Utils.newTag()}`;
-    }
-    else if (this.to_tag && !this.s('to').hasParam('tag'))
-    {
-      to += `;tag=${this.to_tag}`;
-    }
-
-    response += `To: ${to}\r\n`;
-    response += `From: ${this.getHeader('From')}\r\n`;
-    response += `Call-ID: ${this.call_id}\r\n`;
-    response += `CSeq: ${this.cseq} ${this.method}\r\n`;
-
-    for (const header of extraHeaders)
-    {
-      response += `${header.trim()}\r\n`;
-    }
-
-    // Supported.
-    switch (this.method)
-    {
-      case JsSIP_C.INVITE:
-        if (this.ua.configuration.session_timers)
-        {
-          supported.push('timer');
-        }
-        if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu)
-        {
-          supported.push('gruu');
-        }
-        supported.push('ice', 'replaces');
-        break;
-      case JsSIP_C.UPDATE:
-        if (this.ua.configuration.session_timers)
-        {
-          supported.push('timer');
-        }
-        if (body)
-        {
-          supported.push('ice');
-        }
-        supported.push('replaces');
-    }
-
-    supported.push('outbound');
-
-    // Allow and Accept.
-    if (this.method === JsSIP_C.OPTIONS)
-    {
-      response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
-      response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
-    }
-    else if (code === 405)
-    {
-      response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
-    }
-    else if (code === 415)
-    {
-      response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
-    }
-
-    response += `Supported: ${supported}\r\n`;
-
-    if (body)
-    {
-      const length = Utils.str_utf8_length(body);
-
-      response += 'Content-Type: application/sdp\r\n';
-      response += `Content-Length: ${length}\r\n\r\n`;
-      response += body;
-    }
-    else
-    {
-      response += `Content-Length: ${0}\r\n\r\n`;
-    }
-
-    this.server_transaction.receiveResponse(code, response, onSuccess, onFailure);
-  }
-
-  /**
-  * Stateless reply.
-  * -param {Number} code status code
-  * -param {String} reason reason phrase
-  */
-  reply_sl(code = null, reason = null)
-  {
-    const vias = this.getHeaders('via');
-
-    // Validate code and reason values.
-    if (!code || (code < 100 || code > 699))
-    {
-      throw new TypeError(`Invalid status_code: ${code}`);
-    }
-    else if (reason && typeof reason !== 'string' && !(reason instanceof String))
-    {
-      throw new TypeError(`Invalid reason_phrase: ${reason}`);
-    }
-
-    reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
-
-    let response = `SIP/2.0 ${code} ${reason}\r\n`;
-
-    for (const via of vias)
-    {
-      response += `Via: ${via}\r\n`;
-    }
-
-    let to = this.getHeader('To');
-
-    if (!this.to_tag && code > 100)
-    {
-      to += `;tag=${Utils.newTag()}`;
-    }
-    else if (this.to_tag && !this.s('to').hasParam('tag'))
-    {
-      to += `;tag=${this.to_tag}`;
-    }
-
-    response += `To: ${to}\r\n`;
-    response += `From: ${this.getHeader('From')}\r\n`;
-    response += `Call-ID: ${this.call_id}\r\n`;
-    response += `CSeq: ${this.cseq} ${this.method}\r\n`;
-
-    if (this.ua.configuration.extra_headers)
-    {
-      for (const header of this.ua.configuration.extra_headers)
-      {
-        response += `${header.trim()}\r\n`;
-      }
-    }
-
-    response += `Content-Length: ${0}\r\n\r\n`;
-
-    this.transport.send(response);
-  }
+class IncomingRequest extends IncomingMessage {
+	constructor(ua) {
+		super();
+
+		this.ua = ua;
+		this.headers = {};
+		this.ruri = null;
+		this.transport = null;
+		this.server_transaction = null;
+	}
+
+	/**
+	 * Stateful reply.
+	 * -param {Number} code status code
+	 * -param {String} reason reason phrase
+	 * -param {Object} headers extra headers
+	 * -param {String} body body
+	 * -param {Function} [onSuccess] onSuccess callback
+	 * -param {Function} [onFailure] onFailure callback
+	 */
+	reply(code, reason, extraHeaders, body, onSuccess, onFailure) {
+		const supported = [];
+		let to = this.getHeader('To');
+
+		code = code || null;
+		reason = reason || null;
+
+		// Validate code and reason values.
+		if (!code || code < 100 || code > 699) {
+			throw new TypeError(`Invalid status_code: ${code}`);
+		} else if (
+			reason &&
+			typeof reason !== 'string' &&
+			!(reason instanceof String)
+		) {
+			throw new TypeError(`Invalid reason_phrase: ${reason}`);
+		}
+
+		reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
+		extraHeaders = Utils.cloneArray(extraHeaders);
+
+		if (this.ua.configuration.extra_headers) {
+			extraHeaders = extraHeaders.concat(this.ua.configuration.extra_headers);
+		}
+
+		let response = `SIP/2.0 ${code} ${reason}\r\n`;
+
+		if (this.method === JsSIP_C.INVITE && code > 100 && code <= 200) {
+			const headers = this.getHeaders('record-route');
+
+			for (const header of headers) {
+				response += `Record-Route: ${header}\r\n`;
+			}
+		}
+
+		const vias = this.getHeaders('via');
+
+		for (const via of vias) {
+			response += `Via: ${via}\r\n`;
+		}
+
+		if (!this.to_tag && code > 100) {
+			to += `;tag=${Utils.newTag()}`;
+		} else if (this.to_tag && !this.s('to').hasParam('tag')) {
+			to += `;tag=${this.to_tag}`;
+		}
+
+		response += `To: ${to}\r\n`;
+		response += `From: ${this.getHeader('From')}\r\n`;
+		response += `Call-ID: ${this.call_id}\r\n`;
+		response += `CSeq: ${this.cseq} ${this.method}\r\n`;
+
+		for (const header of extraHeaders) {
+			response += `${header.trim()}\r\n`;
+		}
+
+		// Supported.
+		switch (this.method) {
+			case JsSIP_C.INVITE: {
+				if (this.ua.configuration.session_timers) {
+					supported.push('timer');
+				}
+				if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
+					supported.push('gruu');
+				}
+				supported.push('ice', 'replaces');
+				break;
+			}
+			case JsSIP_C.UPDATE: {
+				if (this.ua.configuration.session_timers) {
+					supported.push('timer');
+				}
+				if (body) {
+					supported.push('ice');
+				}
+				supported.push('replaces');
+			}
+		}
+
+		supported.push('outbound');
+
+		// Allow and Accept.
+		if (this.method === JsSIP_C.OPTIONS) {
+			response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+			response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
+		} else if (code === 405) {
+			response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+		} else if (code === 415) {
+			response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
+		}
+
+		response += `Supported: ${supported}\r\n`;
+
+		if (body) {
+			const length = Utils.str_utf8_length(body);
+
+			response += 'Content-Type: application/sdp\r\n';
+			response += `Content-Length: ${length}\r\n\r\n`;
+			response += body;
+		} else {
+			response += `Content-Length: ${0}\r\n\r\n`;
+		}
+
+		this.server_transaction.receiveResponse(
+			code,
+			response,
+			onSuccess,
+			onFailure
+		);
+	}
+
+	/**
+	 * Stateless reply.
+	 * -param {Number} code status code
+	 * -param {String} reason reason phrase
+	 */
+	reply_sl(code = null, reason = null) {
+		const vias = this.getHeaders('via');
+
+		// Validate code and reason values.
+		if (!code || code < 100 || code > 699) {
+			throw new TypeError(`Invalid status_code: ${code}`);
+		} else if (
+			reason &&
+			typeof reason !== 'string' &&
+			!(reason instanceof String)
+		) {
+			throw new TypeError(`Invalid reason_phrase: ${reason}`);
+		}
+
+		reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
+
+		let response = `SIP/2.0 ${code} ${reason}\r\n`;
+
+		for (const via of vias) {
+			response += `Via: ${via}\r\n`;
+		}
+
+		let to = this.getHeader('To');
+
+		if (!this.to_tag && code > 100) {
+			to += `;tag=${Utils.newTag()}`;
+		} else if (this.to_tag && !this.s('to').hasParam('tag')) {
+			to += `;tag=${this.to_tag}`;
+		}
+
+		response += `To: ${to}\r\n`;
+		response += `From: ${this.getHeader('From')}\r\n`;
+		response += `Call-ID: ${this.call_id}\r\n`;
+		response += `CSeq: ${this.cseq} ${this.method}\r\n`;
+
+		if (this.ua.configuration.extra_headers) {
+			for (const header of this.ua.configuration.extra_headers) {
+				response += `${header.trim()}\r\n`;
+			}
+		}
+
+		response += `Content-Length: ${0}\r\n\r\n`;
+
+		this.transport.send(response);
+	}
 }

-class IncomingResponse extends IncomingMessage
-{
-  constructor()
-  {
-    super();
+class IncomingResponse extends IncomingMessage {
+	constructor() {
+		super();

-    this.headers = {};
-    this.status_code = null;
-    this.reason_phrase = null;
-  }
+		this.headers = {};
+		this.status_code = null;
+		this.reason_phrase = null;
+	}
 }

 module.exports = {
-  OutgoingRequest,
-  InitialOutgoingInviteRequest,
-  IncomingRequest,
-  IncomingResponse
+	OutgoingRequest,
+	InitialOutgoingInviteRequest,
+	IncomingRequest,
+	IncomingResponse,
 };
diff --git a/src/Socket.d.ts b/src/Socket.d.ts
index 6f63543..034ac62 100644
--- a/src/Socket.d.ts
+++ b/src/Socket.d.ts
@@ -1,29 +1,29 @@
-export interface WeightedSocket  {
-  socket: Socket;
-  weight: number
+export interface WeightedSocket {
+	socket: Socket;
+	weight: number;
 }

 export class Socket {
-  get via_transport(): string;
-  set via_transport(value: string);
+	get via_transport(): string;
+	set via_transport(value: string);

-  get url(): string;
+	get url(): string;

-  get sip_uri(): string;
+	get sip_uri(): string;

-  connect(): void;
+	connect(): void;

-  disconnect(): void;
+	disconnect(): void;

-  send(message: string | ArrayBufferLike | Blob | ArrayBufferView): boolean;
+	send(message: string | ArrayBufferLike | Blob | ArrayBufferView): boolean;

-  isConnected(): boolean;
+	isConnected(): boolean;

-  isConnecting(): boolean;
+	isConnecting(): boolean;

-  onconnect(): void;
+	onconnect(): void;

-  ondisconnect(error: boolean, code?: number, reason?: string): void;
+	ondisconnect(error: boolean, code?: number, reason?: string): void;

-  ondata<T>(event: T): void;
+	ondata<T>(event: T): void;
 }
diff --git a/src/Socket.js b/src/Socket.js
index a45b4ad..e502360 100644
--- a/src/Socket.js
+++ b/src/Socket.js
@@ -23,65 +23,49 @@ const logger = new Logger('Socket');
  *
  */

-exports.isSocket = (socket) =>
-{
-  // Ignore if an array is given.
-  if (Array.isArray(socket))
-  {
-    return false;
-  }
+exports.isSocket = socket => {
+	// Ignore if an array is given.
+	if (Array.isArray(socket)) {
+		return false;
+	}

-  if (typeof socket === 'undefined')
-  {
-    logger.warn('undefined JsSIP.Socket instance');
+	if (typeof socket === 'undefined') {
+		logger.warn('undefined JsSIP.Socket instance');

-    return false;
-  }
+		return false;
+	}

-  // Check Properties.
-  try
-  {
-    if (!Utils.isString(socket.url))
-    {
-      logger.warn('missing or invalid JsSIP.Socket url property');
-      throw new Error('Missing or invalid JsSIP.Socket url property');
-    }
+	// Check Properties.
+	try {
+		if (!Utils.isString(socket.url)) {
+			logger.warn('missing or invalid JsSIP.Socket url property');
+			throw new Error('Missing or invalid JsSIP.Socket url property');
+		}

-    if (!Utils.isString(socket.via_transport))
-    {
-      logger.warn('missing or invalid JsSIP.Socket via_transport property');
-      throw new Error('Missing or invalid JsSIP.Socket via_transport property');
-    }
+		if (!Utils.isString(socket.via_transport)) {
+			logger.warn('missing or invalid JsSIP.Socket via_transport property');
+			throw new Error('Missing or invalid JsSIP.Socket via_transport property');
+		}

-    if (Grammar.parse(socket.sip_uri, 'SIP_URI') === -1)
-    {
-      logger.warn('missing or invalid JsSIP.Socket sip_uri property');
-      throw new Error('missing or invalid JsSIP.Socket sip_uri property');
-    }
-  }
-  // eslint-disable-next-line no-unused-vars
-  catch (error)
-  {
-    return false;
-  }
+		if (Grammar.parse(socket.sip_uri, 'SIP_URI') === -1) {
+			logger.warn('missing or invalid JsSIP.Socket sip_uri property');
+			throw new Error('missing or invalid JsSIP.Socket sip_uri property');
+		}
+	} catch (error) {
+		return false;
+	}

-  // Check Methods.
-  try
-  {
-    [ 'connect', 'disconnect', 'send' ].forEach((method) =>
-    {
-      if (!Utils.isFunction(socket[method]))
-      {
-        logger.warn(`missing or invalid JsSIP.Socket method: ${method}`);
-        throw new Error(`Missing or invalid JsSIP.Socket method: ${method}`);
-      }
-    });
-  }
-  // eslint-disable-next-line no-unused-vars
-  catch (error)
-  {
-    return false;
-  }
+	// Check Methods.
+	try {
+		['connect', 'disconnect', 'send'].forEach(method => {
+			if (!Utils.isFunction(socket[method])) {
+				logger.warn(`missing or invalid JsSIP.Socket method: ${method}`);
+				throw new Error(`Missing or invalid JsSIP.Socket method: ${method}`);
+			}
+		});
+	} catch (error) {
+		return false;
+	}

-  return true;
+	return true;
 };
diff --git a/src/Subscriber.d.ts b/src/Subscriber.d.ts
index 060f0ea..20bb039 100644
--- a/src/Subscriber.d.ts
+++ b/src/Subscriber.d.ts
@@ -1,42 +1,60 @@
-import {EventEmitter} from 'events'
-import {IncomingRequest} from './SIPMessage'
-import {UA} from './UA'
+import { EventEmitter } from 'events';
+import { IncomingRequest } from './SIPMessage';
+import { UA } from './UA';

 declare enum SubscriberTerminatedCode {
-  SUBSCRIBE_RESPONSE_TIMEOUT = 0,
-  SUBSCRIBE_TRANSPORT_ERROR = 1,
-  SUBSCRIBE_NON_OK_RESPONSE = 2,
-  SUBSCRIBE_WRONG_OK_RESPONSE = 3,
-  SUBSCRIBE_AUTHENTICATION_FAILED = 4,
-  UNSUBSCRIBE_TIMEOUT = 5,
-  FINAL_NOTIFY_RECEIVED = 6,
-  WRONG_NOTIFY_RECEIVED = 7
+	SUBSCRIBE_RESPONSE_TIMEOUT = 0,
+	SUBSCRIBE_TRANSPORT_ERROR = 1,
+	SUBSCRIBE_NON_OK_RESPONSE = 2,
+	SUBSCRIBE_WRONG_OK_RESPONSE = 3,
+	SUBSCRIBE_AUTHENTICATION_FAILED = 4,
+	UNSUBSCRIBE_TIMEOUT = 5,
+	FINAL_NOTIFY_RECEIVED = 6,
+	WRONG_NOTIFY_RECEIVED = 7,
 }

 export interface MessageEventMap {
-  pending: [];
-  accepted: [];
-  active: [];
-  terminated: [terminationCode: SubscriberTerminatedCode, reason: string | undefined, retryAfter: number | undefined];
-  notify: [isFinal: boolean, request: IncomingRequest, body: string | undefined, contentType: string | undefined];
+	pending: [];
+	accepted: [];
+	active: [];
+	terminated: [
+		terminationCode: SubscriberTerminatedCode,
+		reason: string | undefined,
+		retryAfter: number | undefined,
+	];
+	notify: [
+		isFinal: boolean,
+		request: IncomingRequest,
+		body: string | undefined,
+		contentType: string | undefined,
+	];
 }

 interface SubscriberOptions {
-  expires?: number;
-  contentType: string;
-  allowEvents?: string;
-  params?: Record<string, any>;
-  extraHeaders?: Array<string>;
+	expires?: number;
+	contentType: string;
+	allowEvents?: string;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	params?: Record<string, any>;
+	extraHeaders?: string[];
 }

 export class Subscriber extends EventEmitter<MessageEventMap> {
-  constructor(ua: UA, target: string, eventName: string, accept: string, options: SubscriberOptions)
-  subscribe(body?: string): void;
-  terminate(body?: string): void;
-  get state(): string;
-  get id(): string;
-  set data(_data: any);
-  get data(): any;
-  static get C(): typeof SubscriberTerminatedCode;
-  get C(): typeof SubscriberTerminatedCode;
+	constructor(
+		ua: UA,
+		target: string,
+		eventName: string,
+		accept: string,
+		options: SubscriberOptions
+	);
+	subscribe(body?: string): void;
+	terminate(body?: string): void;
+	get state(): string;
+	get id(): string;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	set data(_data: any);
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	get data(): any;
+	static get C(): typeof SubscriberTerminatedCode;
+	get C(): typeof SubscriberTerminatedCode;
 }
diff --git a/src/Subscriber.js b/src/Subscriber.js
index f8d5fe9..13f8bde 100644
--- a/src/Subscriber.js
+++ b/src/Subscriber.js
@@ -14,571 +14,515 @@ const logger = new Logger('Subscriber');
  * Termination codes.
  */
 const C = {
-  // Termination codes.
-  SUBSCRIBE_RESPONSE_TIMEOUT      : 0,
-  SUBSCRIBE_TRANSPORT_ERROR       : 1,
-  SUBSCRIBE_NON_OK_RESPONSE       : 2,
-  SUBSCRIBE_WRONG_OK_RESPONSE     : 3,
-  SUBSCRIBE_AUTHENTICATION_FAILED : 4,
-  UNSUBSCRIBE_TIMEOUT             : 5,
-  FINAL_NOTIFY_RECEIVED           : 6,
-  WRONG_NOTIFY_RECEIVED           : 7,
-
-  // Subscriber states.
-  STATE_PENDING        : 0,
-  STATE_ACTIVE         : 1,
-  STATE_TERMINATED     : 2,
-  STATE_INIT           : 3,
-  STATE_WAITING_NOTIFY : 4,
-
-  // RFC 6665 3.1.1, default expires value.
-  DEFAULT_EXPIRES_SEC : 900
+	// Termination codes.
+	SUBSCRIBE_RESPONSE_TIMEOUT: 0,
+	SUBSCRIBE_TRANSPORT_ERROR: 1,
+	SUBSCRIBE_NON_OK_RESPONSE: 2,
+	SUBSCRIBE_WRONG_OK_RESPONSE: 3,
+	SUBSCRIBE_AUTHENTICATION_FAILED: 4,
+	UNSUBSCRIBE_TIMEOUT: 5,
+	FINAL_NOTIFY_RECEIVED: 6,
+	WRONG_NOTIFY_RECEIVED: 7,
+
+	// Subscriber states.
+	STATE_PENDING: 0,
+	STATE_ACTIVE: 1,
+	STATE_TERMINATED: 2,
+	STATE_INIT: 3,
+	STATE_WAITING_NOTIFY: 4,
+
+	// RFC 6665 3.1.1, default expires value.
+	DEFAULT_EXPIRES_SEC: 900,
 };

 /**
  * RFC 6665 Subscriber implementation.
  */
-module.exports = class Subscriber extends EventEmitter
-{
-  /**
-   * Expose C object.
-   */
-  static get C()
-  {
-    return C;
-  }
-
-  /**
-   * @param {UA} ua - reference to JsSIP.UA
-   * @param {string} target
-   * @param {string} eventName - Event header value. May end with optional ;id=xxx
-   * @param {string} accept - Accept header value.
-   *
-   * @param {SubscriberOption} options - optional parameters.
-   *   @param {number} expires - Expires header value. Default is 900.
-   *   @param {string} contentType - Content-Type header value. Used for SUBSCRIBE with body
-   *   @param {string} allowEvents - Allow-Events header value.
-   *   @param {RequestParams} params - Will have priority over ua.configuration.
-   *      If set please define: to_uri, to_display_name, from_uri, from_display_name
-   *   @param {Array<string>} extraHeaders - Additional SIP headers.
-   */
-  constructor(ua, target, eventName, accept, { expires, contentType,
-    allowEvents, params, extraHeaders })
-  {
-    logger.debug('new');
-
-    super();
-
-    // Check that arguments are defined.
-    if (!target)
-    {
-      throw new TypeError('Not enough arguments: Missing target');
-    }
-
-    if (!eventName)
-    {
-      throw new TypeError('Not enough arguments: Missing eventName');
-    }
-
-    if (!accept)
-    {
-      throw new TypeError('Not enough arguments: Missing accept');
-    }
-
-    const event_header = Grammar.parse(eventName, 'Event');
-
-    if (event_header === -1)
-    {
-      throw new TypeError('Missing Event header field');
-    }
-
-    this._ua = ua;
-    this._target = target;
-
-    if (!Utils.isDecimal(expires) || expires <= 0)
-    {
-      expires = C.DEFAULT_EXPIRES_SEC;
-    }
-
-    this._expires = expires;
-
-    // Used to subscribe with body.
-    this._content_type = contentType;
-
-    // Set initial subscribe parameters.
-    this._params = Utils.cloneObject(params);
-
-    if (!this._params.from_uri)
-    {
-      this._params.from_uri = this._ua.configuration.uri;
-    }
-
-    this._params.from_tag = Utils.newTag();
-    this._params.to_tag = null;
-    this._params.call_id = Utils.createRandomToken(20);
-
-    // Create subscribe cseq if not defined custom cseq.
-    if (this._params.cseq === undefined)
-    {
-      this._params.cseq = Math.floor((Math.random() * 10000) + 1);
-    }
-
-    // Subscriber state.
-    this._state = C.STATE_INIT;
-
-    // Dialog.
-    this._dialog = null;
-
-    // To refresh subscription.
-    this._expires_timer = null;
-    this._expires_timestamp = null;
-
-    // To prevent duplicate terminated call.
-    this._terminated = false;
-
-    this._event_name = event_header.event;
-    this._event_id = event_header.params && event_header.params.id;
-
-    let eventValue = this._event_name;
-
-    if (this._event_id)
-    {
-      eventValue += `;id=${this._event_id}`;
-    }
-
-    this._headers = Utils.cloneArray(extraHeaders);
-    this._headers = this._headers.concat([
-      `Event: ${eventValue}`,
-      `Expires: ${this._expires}`,
-      `Accept: ${accept}`
-    ]);
-
-    if (!this._headers.find((header) => header.startsWith('Contact')))
-    {
-      const contact = `Contact: ${this._ua._contact.toString()}`;
-
-      this._headers.push(contact);
-    }
-
-    if (allowEvents)
-    {
-      this._headers.push(`Allow-Events: ${allowEvents}`);
-    }
-
-    // To enqueue SUBSCRIBE requests created before the reception of the initial subscribe OK response.
-    this._queue = [];
-
-    // Custom session empty object for high level use.
-    this._data = {};
-  }
-
-  // Expose Subscriber constants as a property of the Subscriber instance.
-  get C()
-  {
-    return C;
-  }
-
-  /**
-   * Get dialog state.
-   */
-  get state()
-  {
-    return this._state;
-  }
-
-  /**
-   * Get dialog id.
-   */
-  get id()
-  {
-    return this._dialog ? this._dialog.id : null;
-  }
-
-  get data()
-  {
-    return this._data;
-  }
-
-  set data(_data)
-  {
-    this._data = _data;
-  }
-
-  onRequestTimeout()
-  {
-    this._terminateDialog(C.SUBSCRIBE_RESPONSE_TIMEOUT);
-  }
-
-  onTransportError()
-  {
-    this._terminateDialog(C.SUBSCRIBE_TRANSPORT_ERROR);
-  }
-
-  /**
-   * Dialog callback.
-   */
-  receiveRequest(request)
-  {
-    if (request.method !== JsSIP_C.NOTIFY)
-    {
-      logger.warn('received non-NOTIFY request');
-      request.reply(405);
-
-      return;
-    }
-
-    // RFC 6665 8.2.1. Check if event header matches.
-    const event_header = request.parseHeader('Event');
-
-    if (!event_header)
-    {
-      logger.warn('missing Event header');
-      request.reply(400);
-      this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
-      return;
-    }
-
-    const event_name = event_header.event;
-    const event_id = event_header.params && event_header.params.id;
-
-    if (event_name !== this._event_name || event_id !== this._event_id)
-    {
-      logger.warn('Event header does not match the one in SUBSCRIBE request');
-      request.reply(489);
-      this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
-      return;
-    }
-
-    // Process Subscription-State header.
-    const subs_state = request.parseHeader('subscription-state');
-
-    if (!subs_state)
-    {
-      logger.warn('missing Subscription-State header');
-      request.reply(400);
-      this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
-      return;
-    }
-
-    const new_state = this._parseSubscriptionState(subs_state.state);
-
-    if (new_state === undefined)
-    {
-      logger.warn(`Invalid Subscription-State header value: ${subs_state.state}`);
-      request.reply(400);
-      this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
-      return;
-    }
-    request.reply(200);
-
-    const prev_state = this._state;
-
-    if (prev_state !== C.STATE_TERMINATED && new_state !== C.STATE_TERMINATED)
-    {
-      this._state = new_state;
-
-      if (subs_state.expires !== undefined)
-      {
-        const expires = subs_state.expires;
-        const expires_timestamp = new Date().getTime() + (expires * 1000);
-        const max_time_deviation = 2000;
-
-        // Expiration time is shorter and the difference is not too small.
-        if (this._expires_timestamp - expires_timestamp > max_time_deviation)
-        {
-          logger.debug('update sending re-SUBSCRIBE time');
-
-          this._scheduleSubscribe(expires);
-        }
-      }
-    }
-
-    if (prev_state !== C.STATE_PENDING && new_state === C.STATE_PENDING)
-    {
-      logger.debug('emit "pending"');
-
-      this.emit('pending');
-    }
-    else if (prev_state !== C.STATE_ACTIVE && new_state === C.STATE_ACTIVE)
-    {
-      logger.debug('emit "active"');
-
-      this.emit('active');
-    }
-
-    const body = request.body;
-
-    // Check if the notify is final.
-    const is_final = new_state === C.STATE_TERMINATED;
-
-    // Notify event fired only for notify with body.
-    if (body)
-    {
-      const content_type = request.getHeader('content-type');
-
-      logger.debug('emit "notify"');
-
-      this.emit('notify', is_final, request, body, content_type);
-    }
-
-    if (is_final)
-    {
-      const reason = subs_state.reason;
-      let retry_after = undefined;
-
-      if (subs_state.params && subs_state.params['retry-after'] !== undefined)
-      {
-        retry_after = parseInt(subs_state.params['retry-after']);
-      }
-
-      this._terminateDialog(C.FINAL_NOTIFY_RECEIVED, reason, retry_after);
-    }
-  }
-
-  /**
-   * User API
-   */
-
-  /**
-   * Send the initial (non-fetch)  and subsequent subscribe.
-   * @param {string} body - subscribe request body.
-   */
-  subscribe(body = null)
-  {
-    logger.debug('subscribe()');
-
-    if (this._state === C.STATE_INIT)
-    {
-      this._sendInitialSubscribe(body, this._headers);
-    }
-    else
-    {
-      this._sendSubsequentSubscribe(body, this._headers);
-    }
-  }
-
-  /**
-   * terminate.
-   * Send un-subscribe or fetch-subscribe (with Expires: 0).
-   * @param {string} body - un-subscribe request body
-   */
-  terminate(body = null)
-  {
-    logger.debug('terminate()');
-
-    if (this._state === C.STATE_INIT)
-    {
-      throw new Exceptions.InvalidStateError(this._state);
-    }
-
-    // Prevent duplication un-subscribe sending.
-    if (this._terminated)
-    {
-      return;
-    }
-    this._terminated = true;
-
-    // Set header Expires: 0.
-    const headers = this._headers.map((header) =>
-    {
-      return header.startsWith('Expires') ? 'Expires: 0' : header;
-    });
-
-    this._sendSubsequentSubscribe(body, headers);
-  }
-
-  /**
-   * Private API.
-   */
-  _terminateDialog(terminationCode, reason = undefined, retryAfter = undefined)
-  {
-    // To prevent duplicate emit terminated event.
-    if (this._state === C.STATE_TERMINATED)
-    {
-      return;
-    }
-
-    this._state = C.STATE_TERMINATED;
-
-    // Clear timers.
-    clearTimeout(this._expires_timer);
-
-    if (this._dialog)
-    {
-      this._dialog.terminate();
-      this._dialog = null;
-    }
-
-    logger.debug(`emit "terminated" code=${terminationCode}`);
-
-    this.emit('terminated', terminationCode, reason, retryAfter);
-  }
-
-  _sendInitialSubscribe(body, headers)
-  {
-    if (body)
-    {
-      if (!this._content_type)
-      {
-        throw new TypeError('content_type is undefined');
-      }
-
-      headers = Utils.cloneArray(headers);
-      headers.push(`Content-Type: ${this._content_type}`);
-    }
-
-    this._state = C.STATE_WAITING_NOTIFY;
-
-    const request = new SIPMessage.OutgoingRequest(JsSIP_C.SUBSCRIBE,
-      this._ua.normalizeTarget(this._target), this._ua, this._params, headers, body);
-
-    const request_sender = new RequestSender(this._ua, request, {
-      onRequestTimeout : () =>
-      {
-        this.onRequestTimeout();
-      },
-      onTransportError : () =>
-      {
-        this.onTransportError();
-      },
-      onReceiveResponse : (response) =>
-      {
-        this._receiveSubscribeResponse(response);
-      }
-    });
-
-    request_sender.send();
-  }
-
-  _sendSubsequentSubscribe(body, headers)
-  {
-    if (this._state === C.STATE_TERMINATED)
-    {
-      return;
-    }
-
-    if (!this._dialog)
-    {
-      logger.debug('enqueue subscribe');
-
-      this._queue.push({ body, headers: Utils.cloneArray(headers) });
-
-      return;
-    }
-
-    if (body)
-    {
-      if (!this._content_type)
-      {
-        throw new TypeError('content_type is undefined');
-      }
-
-      headers = Utils.cloneArray(headers);
-      headers.push(`Content-Type: ${this._content_type}`);
-    }
-
-    this._dialog.sendRequest(JsSIP_C.SUBSCRIBE, {
-      body,
-      extraHeaders  : headers,
-      eventHandlers : {
-        onRequestTimeout : () =>
-        {
-          this.onRequestTimeout();
-        },
-        onTransportError : () =>
-        {
-          this.onTransportError();
-        },
-        onSuccessResponse : (response) =>
-        {
-          this._receiveSubscribeResponse(response);
-        },
-        onErrorResponse : (response) =>
-        {
-          this._receiveSubscribeResponse(response);
-        },
-        onDialogError : (response) =>
-        {
-          this._receiveSubscribeResponse(response);
-        }
-      }
-    });
-  }
-
-  _receiveSubscribeResponse(response)
-  {
-    if (this._state === C.STATE_TERMINATED)
-    {
-      return;
-    }
-
-    if (response.status_code >= 200 && response.status_code < 300)
-    {
-      // Create dialog.
-      if (this._dialog === null)
-      {
-        const dialog = new Dialog(this, response, 'UAC');
-
-        if (dialog.error)
-        {
-          // OK response without Contact.
-          logger.warn(dialog.error);
-          this._terminateDialog(C.SUBSCRIBE_WRONG_OK_RESPONSE);
-
-          return;
-        }
-
-        this._dialog = dialog;
-
-        logger.debug('emit "accepted"');
-
-        this.emit('accepted');
-
-        // Subsequent subscribes saved in the queue until dialog created.
-        for (const subscribe of this._queue)
-        {
-          logger.debug('dequeue subscribe');
-
-          this._sendSubsequentSubscribe(subscribe.body, subscribe.headers);
-        }
-      }
-
-      // Check expires value.
-      const expires_value = response.getHeader('expires');
-
-      let expires = parseInt(expires_value);
-
-      if (!Utils.isDecimal(expires) || expires <= 0)
-      {
-        logger.warn(`response without Expires header, setting a default value of ${C.DEFAULT_EXPIRES_SEC}`);
-
-        // RFC 6665 3.1.1 subscribe OK response must contain Expires header.
-        // Use workaround expires value.
-        expires = C.DEFAULT_EXPIRES_SEC;
-      }
-
-      if (expires > 0)
-      {
-        this._scheduleSubscribe(expires);
-      }
-    }
-    else if (response.status_code === 401 || response.status_code === 407)
-    {
-      this._terminateDialog(C.SUBSCRIBE_AUTHENTICATION_FAILED);
-    }
-    else if (response.status_code >= 300)
-    {
-      this._terminateDialog(C.SUBSCRIBE_NON_OK_RESPONSE);
-    }
-  }
-
-  _scheduleSubscribe(expires)
-  {
-    /*
+module.exports = class Subscriber extends EventEmitter {
+	/**
+	 * Expose C object.
+	 */
+	static get C() {
+		return C;
+	}
+
+	/**
+	 * @param {UA} ua - reference to JsSIP.UA
+	 * @param {string} target
+	 * @param {string} eventName - Event header value. May end with optional ;id=xxx
+	 * @param {string} accept - Accept header value.
+	 *
+	 * @param {SubscriberOption} options - optional parameters.
+	 *   @param {number} expires - Expires header value. Default is 900.
+	 *   @param {string} contentType - Content-Type header value. Used for SUBSCRIBE with body
+	 *   @param {string} allowEvents - Allow-Events header value.
+	 *   @param {RequestParams} params - Will have priority over ua.configuration.
+	 *      If set please define: to_uri, to_display_name, from_uri, from_display_name
+	 *   @param {Array<string>} extraHeaders - Additional SIP headers.
+	 */
+	constructor(
+		ua,
+		target,
+		eventName,
+		accept,
+		{ expires, contentType, allowEvents, params, extraHeaders }
+	) {
+		logger.debug('new');
+
+		super();
+
+		// Check that arguments are defined.
+		if (!target) {
+			throw new TypeError('Not enough arguments: Missing target');
+		}
+
+		if (!eventName) {
+			throw new TypeError('Not enough arguments: Missing eventName');
+		}
+
+		if (!accept) {
+			throw new TypeError('Not enough arguments: Missing accept');
+		}
+
+		const event_header = Grammar.parse(eventName, 'Event');
+
+		if (event_header === -1) {
+			throw new TypeError('Missing Event header field');
+		}
+
+		this._ua = ua;
+		this._target = target;
+
+		if (!Utils.isDecimal(expires) || expires <= 0) {
+			expires = C.DEFAULT_EXPIRES_SEC;
+		}
+
+		this._expires = expires;
+
+		// Used to subscribe with body.
+		this._content_type = contentType;
+
+		// Set initial subscribe parameters.
+		this._params = Utils.cloneObject(params);
+
+		if (!this._params.from_uri) {
+			this._params.from_uri = this._ua.configuration.uri;
+		}
+
+		this._params.from_tag = Utils.newTag();
+		this._params.to_tag = null;
+		this._params.call_id = Utils.createRandomToken(20);
+
+		// Create subscribe cseq if not defined custom cseq.
+		if (this._params.cseq === undefined) {
+			this._params.cseq = Math.floor(Math.random() * 10000 + 1);
+		}
+
+		// Subscriber state.
+		this._state = C.STATE_INIT;
+
+		// Dialog.
+		this._dialog = null;
+
+		// To refresh subscription.
+		this._expires_timer = null;
+		this._expires_timestamp = null;
+
+		// To prevent duplicate terminated call.
+		this._terminated = false;
+
+		this._event_name = event_header.event;
+		this._event_id = event_header.params && event_header.params.id;
+
+		let eventValue = this._event_name;
+
+		if (this._event_id) {
+			eventValue += `;id=${this._event_id}`;
+		}
+
+		this._headers = Utils.cloneArray(extraHeaders);
+		this._headers = this._headers.concat([
+			`Event: ${eventValue}`,
+			`Expires: ${this._expires}`,
+			`Accept: ${accept}`,
+		]);
+
+		if (!this._headers.find(header => header.startsWith('Contact'))) {
+			const contact = `Contact: ${this._ua._contact.toString()}`;
+
+			this._headers.push(contact);
+		}
+
+		if (allowEvents) {
+			this._headers.push(`Allow-Events: ${allowEvents}`);
+		}
+
+		// To enqueue SUBSCRIBE requests created before the reception of the initial subscribe OK response.
+		this._queue = [];
+
+		// Custom session empty object for high level use.
+		this._data = {};
+	}
+
+	// Expose Subscriber constants as a property of the Subscriber instance.
+	get C() {
+		return C;
+	}
+
+	/**
+	 * Get dialog state.
+	 */
+	get state() {
+		return this._state;
+	}
+
+	/**
+	 * Get dialog id.
+	 */
+	get id() {
+		return this._dialog ? this._dialog.id : null;
+	}
+
+	get data() {
+		return this._data;
+	}
+
+	set data(_data) {
+		this._data = _data;
+	}
+
+	onRequestTimeout() {
+		this._terminateDialog(C.SUBSCRIBE_RESPONSE_TIMEOUT);
+	}
+
+	onTransportError() {
+		this._terminateDialog(C.SUBSCRIBE_TRANSPORT_ERROR);
+	}
+
+	/**
+	 * Dialog callback.
+	 */
+	receiveRequest(request) {
+		if (request.method !== JsSIP_C.NOTIFY) {
+			logger.warn('received non-NOTIFY request');
+			request.reply(405);
+
+			return;
+		}
+
+		// RFC 6665 8.2.1. Check if event header matches.
+		const event_header = request.parseHeader('Event');
+
+		if (!event_header) {
+			logger.warn('missing Event header');
+			request.reply(400);
+			this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+			return;
+		}
+
+		const event_name = event_header.event;
+		const event_id = event_header.params && event_header.params.id;
+
+		if (event_name !== this._event_name || event_id !== this._event_id) {
+			logger.warn('Event header does not match the one in SUBSCRIBE request');
+			request.reply(489);
+			this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+			return;
+		}
+
+		// Process Subscription-State header.
+		const subs_state = request.parseHeader('subscription-state');
+
+		if (!subs_state) {
+			logger.warn('missing Subscription-State header');
+			request.reply(400);
+			this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+			return;
+		}
+
+		const new_state = this._parseSubscriptionState(subs_state.state);
+
+		if (new_state === undefined) {
+			logger.warn(
+				`Invalid Subscription-State header value: ${subs_state.state}`
+			);
+			request.reply(400);
+			this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+			return;
+		}
+		request.reply(200);
+
+		const prev_state = this._state;
+
+		if (prev_state !== C.STATE_TERMINATED && new_state !== C.STATE_TERMINATED) {
+			this._state = new_state;
+
+			if (subs_state.expires !== undefined) {
+				const expires = subs_state.expires;
+				const expires_timestamp = new Date().getTime() + expires * 1000;
+				const max_time_deviation = 2000;
+
+				// Expiration time is shorter and the difference is not too small.
+				if (this._expires_timestamp - expires_timestamp > max_time_deviation) {
+					logger.debug('update sending re-SUBSCRIBE time');
+
+					this._scheduleSubscribe(expires);
+				}
+			}
+		}
+
+		if (prev_state !== C.STATE_PENDING && new_state === C.STATE_PENDING) {
+			logger.debug('emit "pending"');
+
+			this.emit('pending');
+		} else if (prev_state !== C.STATE_ACTIVE && new_state === C.STATE_ACTIVE) {
+			logger.debug('emit "active"');
+
+			this.emit('active');
+		}
+
+		const body = request.body;
+
+		// Check if the notify is final.
+		const is_final = new_state === C.STATE_TERMINATED;
+
+		// Notify event fired only for notify with body.
+		if (body) {
+			const content_type = request.getHeader('content-type');
+
+			logger.debug('emit "notify"');
+
+			this.emit('notify', is_final, request, body, content_type);
+		}
+
+		if (is_final) {
+			const reason = subs_state.reason;
+			let retry_after = undefined;
+
+			if (subs_state.params && subs_state.params['retry-after'] !== undefined) {
+				retry_after = parseInt(subs_state.params['retry-after']);
+			}
+
+			this._terminateDialog(C.FINAL_NOTIFY_RECEIVED, reason, retry_after);
+		}
+	}
+
+	/**
+	 * User API
+	 */
+
+	/**
+	 * Send the initial (non-fetch)  and subsequent subscribe.
+	 * @param {string} body - subscribe request body.
+	 */
+	subscribe(body = null) {
+		logger.debug('subscribe()');
+
+		if (this._state === C.STATE_INIT) {
+			this._sendInitialSubscribe(body, this._headers);
+		} else {
+			this._sendSubsequentSubscribe(body, this._headers);
+		}
+	}
+
+	/**
+	 * terminate.
+	 * Send un-subscribe or fetch-subscribe (with Expires: 0).
+	 * @param {string} body - un-subscribe request body
+	 */
+	terminate(body = null) {
+		logger.debug('terminate()');
+
+		if (this._state === C.STATE_INIT) {
+			throw new Exceptions.InvalidStateError(this._state);
+		}
+
+		// Prevent duplication un-subscribe sending.
+		if (this._terminated) {
+			return;
+		}
+		this._terminated = true;
+
+		// Set header Expires: 0.
+		const headers = this._headers.map(header => {
+			return header.startsWith('Expires') ? 'Expires: 0' : header;
+		});
+
+		this._sendSubsequentSubscribe(body, headers);
+	}
+
+	/**
+	 * Private API.
+	 */
+	_terminateDialog(
+		terminationCode,
+		reason = undefined,
+		retryAfter = undefined
+	) {
+		// To prevent duplicate emit terminated event.
+		if (this._state === C.STATE_TERMINATED) {
+			return;
+		}
+
+		this._state = C.STATE_TERMINATED;
+
+		// Clear timers.
+		clearTimeout(this._expires_timer);
+
+		if (this._dialog) {
+			this._dialog.terminate();
+			this._dialog = null;
+		}
+
+		logger.debug(`emit "terminated" code=${terminationCode}`);
+
+		this.emit('terminated', terminationCode, reason, retryAfter);
+	}
+
+	_sendInitialSubscribe(body, headers) {
+		if (body) {
+			if (!this._content_type) {
+				throw new TypeError('content_type is undefined');
+			}
+
+			headers = Utils.cloneArray(headers);
+			headers.push(`Content-Type: ${this._content_type}`);
+		}
+
+		this._state = C.STATE_WAITING_NOTIFY;
+
+		const request = new SIPMessage.OutgoingRequest(
+			JsSIP_C.SUBSCRIBE,
+			this._ua.normalizeTarget(this._target),
+			this._ua,
+			this._params,
+			headers,
+			body
+		);
+
+		const request_sender = new RequestSender(this._ua, request, {
+			onRequestTimeout: () => {
+				this.onRequestTimeout();
+			},
+			onTransportError: () => {
+				this.onTransportError();
+			},
+			onReceiveResponse: response => {
+				this._receiveSubscribeResponse(response);
+			},
+		});
+
+		request_sender.send();
+	}
+
+	_sendSubsequentSubscribe(body, headers) {
+		if (this._state === C.STATE_TERMINATED) {
+			return;
+		}
+
+		if (!this._dialog) {
+			logger.debug('enqueue subscribe');
+
+			this._queue.push({ body, headers: Utils.cloneArray(headers) });
+
+			return;
+		}
+
+		if (body) {
+			if (!this._content_type) {
+				throw new TypeError('content_type is undefined');
+			}
+
+			headers = Utils.cloneArray(headers);
+			headers.push(`Content-Type: ${this._content_type}`);
+		}
+
+		this._dialog.sendRequest(JsSIP_C.SUBSCRIBE, {
+			body,
+			extraHeaders: headers,
+			eventHandlers: {
+				onRequestTimeout: () => {
+					this.onRequestTimeout();
+				},
+				onTransportError: () => {
+					this.onTransportError();
+				},
+				onSuccessResponse: response => {
+					this._receiveSubscribeResponse(response);
+				},
+				onErrorResponse: response => {
+					this._receiveSubscribeResponse(response);
+				},
+				onDialogError: response => {
+					this._receiveSubscribeResponse(response);
+				},
+			},
+		});
+	}
+
+	_receiveSubscribeResponse(response) {
+		if (this._state === C.STATE_TERMINATED) {
+			return;
+		}
+
+		if (response.status_code >= 200 && response.status_code < 300) {
+			// Create dialog.
+			if (this._dialog === null) {
+				const dialog = new Dialog(this, response, 'UAC');
+
+				if (dialog.error) {
+					// OK response without Contact.
+					logger.warn(dialog.error);
+					this._terminateDialog(C.SUBSCRIBE_WRONG_OK_RESPONSE);
+
+					return;
+				}
+
+				this._dialog = dialog;
+
+				logger.debug('emit "accepted"');
+
+				this.emit('accepted');
+
+				// Subsequent subscribes saved in the queue until dialog created.
+				for (const subscribe of this._queue) {
+					logger.debug('dequeue subscribe');
+
+					this._sendSubsequentSubscribe(subscribe.body, subscribe.headers);
+				}
+			}
+
+			// Check expires value.
+			const expires_value = response.getHeader('expires');
+
+			let expires = parseInt(expires_value);
+
+			if (!Utils.isDecimal(expires) || expires <= 0) {
+				logger.warn(
+					`response without Expires header, setting a default value of ${C.DEFAULT_EXPIRES_SEC}`
+				);
+
+				// RFC 6665 3.1.1 subscribe OK response must contain Expires header.
+				// Use workaround expires value.
+				expires = C.DEFAULT_EXPIRES_SEC;
+			}
+
+			if (expires > 0) {
+				this._scheduleSubscribe(expires);
+			}
+		} else if (response.status_code === 401 || response.status_code === 407) {
+			this._terminateDialog(C.SUBSCRIBE_AUTHENTICATION_FAILED);
+		} else if (response.status_code >= 300) {
+			this._terminateDialog(C.SUBSCRIBE_NON_OK_RESPONSE);
+		}
+	}
+
+	_scheduleSubscribe(expires) {
+		/*
       If the expires time is less than 140 seconds we do not support Chrome intensive timer throttling mode.
       In this case, the re-subcribe is sent 5 seconds before the subscription expiration.

@@ -592,31 +536,45 @@ module.exports = class Subscriber extends EventEmitter
 	       expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds.
 	 */

-    const timeout = expires >= 140 ? (expires * 1000 / 2)
-     + Math.floor(((expires / 2) - 70) * 1000 * Math.random()) : (expires * 1000) - 5000;
-
-    this._expires_timestamp = new Date().getTime() + (expires * 1000);
-
-    logger.debug(`next SUBSCRIBE will be sent in ${Math.floor(timeout / 1000)} sec`);
-
-    clearTimeout(this._expires_timer);
-    this._expires_timer = setTimeout(() =>
-    {
-      this._expires_timer = null;
-      this._sendSubsequentSubscribe(null, this._headers);
-    }, timeout);
-  }
-
-  _parseSubscriptionState(strState)
-  {
-    switch (strState)
-    {
-      case 'pending': return C.STATE_PENDING;
-      case 'active': return C.STATE_ACTIVE;
-      case 'terminated': return C.STATE_TERMINATED;
-      case 'init': return C.STATE_INIT;
-      case 'notify_wait': return C.STATE_WAITING_NOTIFY;
-      default: return undefined;
-    }
-  }
+		const timeout =
+			expires >= 140
+				? (expires * 1000) / 2 +
+					Math.floor((expires / 2 - 70) * 1000 * Math.random())
+				: expires * 1000 - 5000;
+
+		this._expires_timestamp = new Date().getTime() + expires * 1000;
+
+		logger.debug(
+			`next SUBSCRIBE will be sent in ${Math.floor(timeout / 1000)} sec`
+		);
+
+		clearTimeout(this._expires_timer);
+		this._expires_timer = setTimeout(() => {
+			this._expires_timer = null;
+			this._sendSubsequentSubscribe(null, this._headers);
+		}, timeout);
+	}
+
+	_parseSubscriptionState(strState) {
+		switch (strState) {
+			case 'pending': {
+				return C.STATE_PENDING;
+			}
+			case 'active': {
+				return C.STATE_ACTIVE;
+			}
+			case 'terminated': {
+				return C.STATE_TERMINATED;
+			}
+			case 'init': {
+				return C.STATE_INIT;
+			}
+			case 'notify_wait': {
+				return C.STATE_WAITING_NOTIFY;
+			}
+			default: {
+				return undefined;
+			}
+		}
+	}
 };
diff --git a/src/Timers.js b/src/Timers.js
index 48fb254..2af8bfa 100644
--- a/src/Timers.js
+++ b/src/Timers.js
@@ -1,17 +1,19 @@
-const T1 = 500, T2 = 4000, T4 = 5000;
+const T1 = 500,
+	T2 = 4000,
+	T4 = 5000;

 module.exports = {
-  T1,
-  T2,
-  T4,
-  TIMER_B                       : 64 * T1,
-  TIMER_D                       : 0 * T1,
-  TIMER_F                       : 64 * T1,
-  TIMER_H                       : 64 * T1,
-  TIMER_I                       : 0 * T1,
-  TIMER_J                       : 0 * T1,
-  TIMER_K                       : 0 * T4,
-  TIMER_L                       : 64 * T1,
-  TIMER_M                       : 64 * T1,
-  PROVISIONAL_RESPONSE_INTERVAL : 60000 // See RFC 3261 Section 13.3.1.1
+	T1,
+	T2,
+	T4,
+	TIMER_B: 64 * T1,
+	TIMER_D: 0 * T1,
+	TIMER_F: 64 * T1,
+	TIMER_H: 64 * T1,
+	TIMER_I: 0 * T1,
+	TIMER_J: 0 * T1,
+	TIMER_K: 0 * T4,
+	TIMER_L: 64 * T1,
+	TIMER_M: 64 * T1,
+	PROVISIONAL_RESPONSE_INTERVAL: 60000, // See RFC 3261 Section 13.3.1.1
 };
diff --git a/src/Transactions.js b/src/Transactions.js
index 1f9ded3..4978bc9 100644
--- a/src/Transactions.js
+++ b/src/Transactions.js
@@ -11,678 +11,605 @@ 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'
+	// 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)
-  {
-    super();
-
-    this.type = C.NON_INVITE_CLIENT;
-    this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
-    this.ua = ua;
-    this.transport = transport;
-    this.request = request;
-    this.eventHandlers = eventHandlers;
-
-    let via = `SIP/2.0/${transport.via_transport}`;
-
-    via += ` ${ua.configuration.via_host};branch=${this.id}`;
-
-    this.request.setHeader('via', via);
-
-    this.ua.newTransaction(this);
-  }
-
-  get C()
-  {
-    return C;
-  }
-
-  stateChanged(state)
-  {
-    this.state = state;
-    this.emit('stateChanged');
-  }
-
-  send()
-  {
-    this.stateChanged(C.STATUS_TRYING);
-    this.F = setTimeout(() => { this.timer_F(); }, Timers.TIMER_F);
-
-    if (!this.transport.send(this.request))
-    {
-      this.onTransportError();
-    }
-  }
-
-  onTransportError()
-  {
-    loggernict.debug(`transport error occurred, deleting transaction ${this.id}`);
-    clearTimeout(this.F);
-    clearTimeout(this.K);
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-    this.eventHandlers.onTransportError();
-  }
-
-  timer_F()
-  {
-    loggernict.debug(`Timer F expired for transaction ${this.id}`);
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-    this.eventHandlers.onRequestTimeout();
-  }
-
-  timer_K()
-  {
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-  }
-
-  receiveResponse(response)
-  {
-    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);
-          this.eventHandlers.onReceiveResponse(response);
-          break;
-      }
-    }
-    else
-    {
-      switch (this.state)
-      {
-        case C.STATUS_TRYING:
-        case C.STATUS_PROCEEDING:
-          this.stateChanged(C.STATUS_COMPLETED);
-          clearTimeout(this.F);
-
-          if (status_code === 408)
-          {
-            this.eventHandlers.onRequestTimeout();
-          }
-          else
-          {
-            this.eventHandlers.onReceiveResponse(response);
-          }
-
-          this.K = setTimeout(() => { this.timer_K(); }, Timers.TIMER_K);
-          break;
-        case C.STATUS_COMPLETED:
-          break;
-      }
-    }
-  }
+class NonInviteClientTransaction extends EventEmitter {
+	constructor(ua, transport, request, eventHandlers) {
+		super();
+
+		this.type = C.NON_INVITE_CLIENT;
+		this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
+		this.ua = ua;
+		this.transport = transport;
+		this.request = request;
+		this.eventHandlers = eventHandlers;
+
+		let via = `SIP/2.0/${transport.via_transport}`;
+
+		via += ` ${ua.configuration.via_host};branch=${this.id}`;
+
+		this.request.setHeader('via', via);
+
+		this.ua.newTransaction(this);
+	}
+
+	get C() {
+		return C;
+	}
+
+	stateChanged(state) {
+		this.state = state;
+		this.emit('stateChanged');
+	}
+
+	send() {
+		this.stateChanged(C.STATUS_TRYING);
+		this.F = setTimeout(() => {
+			this.timer_F();
+		}, Timers.TIMER_F);
+
+		if (!this.transport.send(this.request)) {
+			this.onTransportError();
+		}
+	}
+
+	onTransportError() {
+		loggernict.debug(
+			`transport error occurred, deleting transaction ${this.id}`
+		);
+		clearTimeout(this.F);
+		clearTimeout(this.K);
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+		this.eventHandlers.onTransportError();
+	}
+
+	timer_F() {
+		loggernict.debug(`Timer F expired for transaction ${this.id}`);
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+		this.eventHandlers.onRequestTimeout();
+	}
+
+	timer_K() {
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+	}
+
+	receiveResponse(response) {
+		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);
+					this.eventHandlers.onReceiveResponse(response);
+					break;
+				}
+			}
+		} else {
+			switch (this.state) {
+				case C.STATUS_TRYING:
+				case C.STATUS_PROCEEDING: {
+					this.stateChanged(C.STATUS_COMPLETED);
+					clearTimeout(this.F);
+
+					if (status_code === 408) {
+						this.eventHandlers.onRequestTimeout();
+					} else {
+						this.eventHandlers.onReceiveResponse(response);
+					}
+
+					this.K = setTimeout(() => {
+						this.timer_K();
+					}, Timers.TIMER_K);
+					break;
+				}
+				case C.STATUS_COMPLETED: {
+					break;
+				}
+			}
+		}
+	}
 }

-class InviteClientTransaction extends EventEmitter
-{
-  constructor(ua, transport, request, eventHandlers)
-  {
-    super();
-
-    this.type = C.INVITE_CLIENT;
-    this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
-    this.ua = ua;
-    this.transport = transport;
-    this.request = request;
-    this.eventHandlers = eventHandlers;
-    request.transaction = this;
-
-    let via = `SIP/2.0/${transport.via_transport}`;
-
-    via += ` ${ua.configuration.via_host};branch=${this.id}`;
-
-    this.request.setHeader('via', via);
-
-    this.ua.newTransaction(this);
-  }
-
-  get C()
-  {
-    return C;
-  }
-
-  stateChanged(state)
-  {
-    this.state = state;
-    this.emit('stateChanged');
-  }
-
-  send()
-  {
-    this.stateChanged(C.STATUS_CALLING);
-    this.B = setTimeout(() =>
-    {
-      this.timer_B();
-    }, Timers.TIMER_B);
-
-    if (!this.transport.send(this.request))
-    {
-      this.onTransportError();
-    }
-  }
-
-  onTransportError()
-  {
-    clearTimeout(this.B);
-    clearTimeout(this.D);
-    clearTimeout(this.M);
-
-    if (this.state !== C.STATUS_ACCEPTED)
-    {
-      loggerict.debug(`transport error occurred, deleting transaction ${this.id}`);
-      this.eventHandlers.onTransportError();
-    }
-
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-  }
-
-  // RFC 6026 7.2.
-  timer_M()
-  {
-    loggerict.debug(`Timer M expired for transaction ${this.id}`);
-
-    if (this.state === C.STATUS_ACCEPTED)
-    {
-      clearTimeout(this.B);
-      this.stateChanged(C.STATUS_TERMINATED);
-      this.ua.destroyTransaction(this);
-    }
-  }
-
-  // RFC 3261 17.1.1.
-  timer_B()
-  {
-    loggerict.debug(`Timer B expired for transaction ${this.id}`);
-    if (this.state === C.STATUS_CALLING)
-    {
-      this.stateChanged(C.STATUS_TERMINATED);
-      this.ua.destroyTransaction(this);
-      this.eventHandlers.onRequestTimeout();
-    }
-  }
-
-  timer_D()
-  {
-    loggerict.debug(`Timer D expired for transaction ${this.id}`);
-    clearTimeout(this.B);
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-  }
-
-  sendACK(response)
-  {
-    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'),
-        'cseq'      : this.request.cseq
-      });
-
-    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(); }, Timers.TIMER_D);
-
-    this.transport.send(ack);
-  }
-
-  cancel(reason)
-  {
-    // Send only if a provisional response (>100) has been received.
-    if (this.state !== C.STATUS_PROCEEDING)
-    {
-      return;
-    }
-
-    const cancel = new SIPMessage.OutgoingRequest(JsSIP_C.CANCEL, this.request.ruri,
-      this.ua, {
-        'route_set' : this.request.getHeaders('route'),
-        'call_id'   : this.request.getHeader('call-id'),
-        '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'));
-
-    if (reason)
-    {
-      cancel.setHeader('reason', reason);
-    }
-
-    this.transport.send(cancel);
-  }
-
-  receiveResponse(response)
-  {
-    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);
-          this.eventHandlers.onReceiveResponse(response);
-          break;
-        case C.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);
-          this.M = setTimeout(() =>
-          {
-            this.timer_M();
-          }, Timers.TIMER_M);
-          this.eventHandlers.onReceiveResponse(response);
-          break;
-        case C.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);
-          this.sendACK(response);
-          this.eventHandlers.onReceiveResponse(response);
-          break;
-        case C.STATUS_COMPLETED:
-          this.sendACK(response);
-          break;
-      }
-    }
-  }
+class InviteClientTransaction extends EventEmitter {
+	constructor(ua, transport, request, eventHandlers) {
+		super();
+
+		this.type = C.INVITE_CLIENT;
+		this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
+		this.ua = ua;
+		this.transport = transport;
+		this.request = request;
+		this.eventHandlers = eventHandlers;
+		request.transaction = this;
+
+		let via = `SIP/2.0/${transport.via_transport}`;
+
+		via += ` ${ua.configuration.via_host};branch=${this.id}`;
+
+		this.request.setHeader('via', via);
+
+		this.ua.newTransaction(this);
+	}
+
+	get C() {
+		return C;
+	}
+
+	stateChanged(state) {
+		this.state = state;
+		this.emit('stateChanged');
+	}
+
+	send() {
+		this.stateChanged(C.STATUS_CALLING);
+		this.B = setTimeout(() => {
+			this.timer_B();
+		}, Timers.TIMER_B);
+
+		if (!this.transport.send(this.request)) {
+			this.onTransportError();
+		}
+	}
+
+	onTransportError() {
+		clearTimeout(this.B);
+		clearTimeout(this.D);
+		clearTimeout(this.M);
+
+		if (this.state !== C.STATUS_ACCEPTED) {
+			loggerict.debug(
+				`transport error occurred, deleting transaction ${this.id}`
+			);
+			this.eventHandlers.onTransportError();
+		}
+
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+	}
+
+	// RFC 6026 7.2.
+	timer_M() {
+		loggerict.debug(`Timer M expired for transaction ${this.id}`);
+
+		if (this.state === C.STATUS_ACCEPTED) {
+			clearTimeout(this.B);
+			this.stateChanged(C.STATUS_TERMINATED);
+			this.ua.destroyTransaction(this);
+		}
+	}
+
+	// RFC 3261 17.1.1.
+	timer_B() {
+		loggerict.debug(`Timer B expired for transaction ${this.id}`);
+		if (this.state === C.STATUS_CALLING) {
+			this.stateChanged(C.STATUS_TERMINATED);
+			this.ua.destroyTransaction(this);
+			this.eventHandlers.onRequestTimeout();
+		}
+	}
+
+	timer_D() {
+		loggerict.debug(`Timer D expired for transaction ${this.id}`);
+		clearTimeout(this.B);
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+	}
+
+	sendACK(response) {
+		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'),
+				cseq: this.request.cseq,
+			}
+		);
+
+		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();
+		}, Timers.TIMER_D);
+
+		this.transport.send(ack);
+	}
+
+	cancel(reason) {
+		// Send only if a provisional response (>100) has been received.
+		if (this.state !== C.STATUS_PROCEEDING) {
+			return;
+		}
+
+		const cancel = new SIPMessage.OutgoingRequest(
+			JsSIP_C.CANCEL,
+			this.request.ruri,
+			this.ua,
+			{
+				route_set: this.request.getHeaders('route'),
+				call_id: this.request.getHeader('call-id'),
+				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'));
+
+		if (reason) {
+			cancel.setHeader('reason', reason);
+		}
+
+		this.transport.send(cancel);
+	}
+
+	receiveResponse(response) {
+		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);
+					this.eventHandlers.onReceiveResponse(response);
+					break;
+				}
+				case C.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);
+					this.M = setTimeout(() => {
+						this.timer_M();
+					}, Timers.TIMER_M);
+					this.eventHandlers.onReceiveResponse(response);
+					break;
+				}
+				case C.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);
+					this.sendACK(response);
+					this.eventHandlers.onReceiveResponse(response);
+					break;
+				}
+				case C.STATUS_COMPLETED: {
+					this.sendACK(response);
+					break;
+				}
+			}
+		}
+	}
 }

-class AckClientTransaction extends EventEmitter
-{
-  constructor(ua, transport, request, eventHandlers)
-  {
-    super();
-
-    this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
-    this.transport = transport;
-    this.request = request;
-    this.eventHandlers = eventHandlers;
-
-    let via = `SIP/2.0/${transport.via_transport}`;
-
-    via += ` ${ua.configuration.via_host};branch=${this.id}`;
-
-    this.request.setHeader('via', via);
-  }
-
-  get C()
-  {
-    return C;
-  }
-
-  send()
-  {
-    if (!this.transport.send(this.request))
-    {
-      this.onTransportError();
-    }
-  }
-
-  onTransportError()
-  {
-    loggeract.debug(`transport error occurred for transaction ${this.id}`);
-    this.eventHandlers.onTransportError();
-  }
+class AckClientTransaction extends EventEmitter {
+	constructor(ua, transport, request, eventHandlers) {
+		super();
+
+		this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
+		this.transport = transport;
+		this.request = request;
+		this.eventHandlers = eventHandlers;
+
+		let via = `SIP/2.0/${transport.via_transport}`;
+
+		via += ` ${ua.configuration.via_host};branch=${this.id}`;
+
+		this.request.setHeader('via', via);
+	}
+
+	get C() {
+		return C;
+	}
+
+	send() {
+		if (!this.transport.send(this.request)) {
+			this.onTransportError();
+		}
+	}
+
+	onTransportError() {
+		loggeract.debug(`transport error occurred for transaction ${this.id}`);
+		this.eventHandlers.onTransportError();
+	}
 }

-class NonInviteServerTransaction extends EventEmitter
-{
-  constructor(ua, transport, request)
-  {
-    super();
-
-    this.type = C.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;
-
-    ua.newTransaction(this);
-  }
-
-  get C()
-  {
-    return C;
-  }
-
-  stateChanged(state)
-  {
-    this.state = state;
-    this.emit('stateChanged');
-  }
-
-  timer_J()
-  {
-    loggernist.debug(`Timer J expired for transaction ${this.id}`);
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-  }
-
-  onTransportError()
-  {
-    if (!this.transportError)
-    {
-      this.transportError = true;
-
-      loggernist.debug(`transport error occurred, deleting transaction ${this.id}`);
-
-      clearTimeout(this.J);
-      this.stateChanged(C.STATUS_TERMINATED);
-      this.ua.destroyTransaction(this);
-    }
-  }
-
-  receiveResponse(status_code, response, onSuccess, onFailure)
-  {
-    if (status_code === 100)
-    {
-      /* RFC 4320 4.1
-       * 'A SIP element MUST NOT
-       * send any provisional response with a
-       * Status-Code other than 100 to a non-INVITE request.'
-       */
-      switch (this.state)
-      {
-        case C.STATUS_TRYING:
-          this.stateChanged(C.STATUS_PROCEEDING);
-          if (!this.transport.send(response))
-          {
-            this.onTransportError();
-          }
-          break;
-        case C.STATUS_PROCEEDING:
-          this.last_response = response;
-          if (!this.transport.send(response))
-          {
-            this.onTransportError();
-            if (onFailure)
-            {
-              onFailure();
-            }
-          }
-          else if (onSuccess)
-          {
-            onSuccess();
-          }
-          break;
-      }
-    }
-    else if (status_code >= 200 && status_code <= 699)
-    {
-      switch (this.state)
-      {
-        case C.STATUS_TRYING:
-        case C.STATUS_PROCEEDING:
-          this.stateChanged(C.STATUS_COMPLETED);
-          this.last_response = response;
-          this.J = setTimeout(() =>
-          {
-            this.timer_J();
-          }, Timers.TIMER_J);
-          if (!this.transport.send(response))
-          {
-            this.onTransportError();
-            if (onFailure)
-            {
-              onFailure();
-            }
-          }
-          else if (onSuccess)
-          {
-            onSuccess();
-          }
-          break;
-        case C.STATUS_COMPLETED:
-          break;
-      }
-    }
-  }
+class NonInviteServerTransaction extends EventEmitter {
+	constructor(ua, transport, request) {
+		super();
+
+		this.type = C.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;
+
+		ua.newTransaction(this);
+	}
+
+	get C() {
+		return C;
+	}
+
+	stateChanged(state) {
+		this.state = state;
+		this.emit('stateChanged');
+	}
+
+	timer_J() {
+		loggernist.debug(`Timer J expired for transaction ${this.id}`);
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+	}
+
+	onTransportError() {
+		if (!this.transportError) {
+			this.transportError = true;
+
+			loggernist.debug(
+				`transport error occurred, deleting transaction ${this.id}`
+			);
+
+			clearTimeout(this.J);
+			this.stateChanged(C.STATUS_TERMINATED);
+			this.ua.destroyTransaction(this);
+		}
+	}
+
+	receiveResponse(status_code, response, onSuccess, onFailure) {
+		if (status_code === 100) {
+			/* RFC 4320 4.1
+			 * 'A SIP element MUST NOT
+			 * send any provisional response with a
+			 * Status-Code other than 100 to a non-INVITE request.'
+			 */
+			switch (this.state) {
+				case C.STATUS_TRYING: {
+					this.stateChanged(C.STATUS_PROCEEDING);
+					if (!this.transport.send(response)) {
+						this.onTransportError();
+					}
+					break;
+				}
+				case C.STATUS_PROCEEDING: {
+					this.last_response = response;
+					if (!this.transport.send(response)) {
+						this.onTransportError();
+						if (onFailure) {
+							onFailure();
+						}
+					} else if (onSuccess) {
+						onSuccess();
+					}
+					break;
+				}
+			}
+		} else if (status_code >= 200 && status_code <= 699) {
+			switch (this.state) {
+				case C.STATUS_TRYING:
+				case C.STATUS_PROCEEDING: {
+					this.stateChanged(C.STATUS_COMPLETED);
+					this.last_response = response;
+					this.J = setTimeout(() => {
+						this.timer_J();
+					}, Timers.TIMER_J);
+					if (!this.transport.send(response)) {
+						this.onTransportError();
+						if (onFailure) {
+							onFailure();
+						}
+					} else if (onSuccess) {
+						onSuccess();
+					}
+					break;
+				}
+				case C.STATUS_COMPLETED: {
+					break;
+				}
+			}
+		}
+	}
 }

-class InviteServerTransaction extends EventEmitter
-{
-  constructor(ua, transport, request)
-  {
-    super();
-
-    this.type = C.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;
-
-    ua.newTransaction(this);
-
-    this.resendProvisionalTimer = null;
-
-    request.reply(100);
-  }
-
-  get C()
-  {
-    return C;
-  }
-
-  stateChanged(state)
-  {
-    this.state = state;
-    this.emit('stateChanged');
-  }
-
-  timer_H()
-  {
-    loggerist.debug(`Timer H expired for transaction ${this.id}`);
-
-    if (this.state === C.STATUS_COMPLETED)
-    {
-      loggerist.debug('ACK not received, dialog will be terminated');
-    }
-
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-  }
-
-  timer_I()
-  {
-    this.stateChanged(C.STATUS_TERMINATED);
-    this.ua.destroyTransaction(this);
-  }
-
-  // RFC 6026 7.1.
-  timer_L()
-  {
-    loggerist.debug(`Timer L expired for transaction ${this.id}`);
-
-    if (this.state === C.STATUS_ACCEPTED)
-    {
-      this.stateChanged(C.STATUS_TERMINATED);
-      this.ua.destroyTransaction(this);
-    }
-  }
-
-  onTransportError()
-  {
-    if (!this.transportError)
-    {
-      this.transportError = true;
-
-      loggerist.debug(`transport error occurred, deleting transaction ${this.id}`);
-
-      if (this.resendProvisionalTimer !== null)
-      {
-        clearInterval(this.resendProvisionalTimer);
-        this.resendProvisionalTimer = null;
-      }
-
-      clearTimeout(this.L);
-      clearTimeout(this.H);
-      clearTimeout(this.I);
-
-      this.stateChanged(C.STATUS_TERMINATED);
-      this.ua.destroyTransaction(this);
-    }
-  }
-
-  resend_provisional()
-  {
-    if (!this.transport.send(this.last_response))
-    {
-      this.onTransportError();
-    }
-  }
-
-  // INVITE Server Transaction RFC 3261 17.2.1.
-  receiveResponse(status_code, response, onSuccess, onFailure)
-  {
-    if (status_code >= 100 && status_code <= 199)
-    {
-      switch (this.state)
-      {
-        case C.STATUS_PROCEEDING:
-          if (!this.transport.send(response))
-          {
-            this.onTransportError();
-          }
-          this.last_response = response;
-          break;
-      }
-    }
-
-    if (status_code > 100 && status_code <= 199 && this.state === C.STATUS_PROCEEDING)
-    {
-      // Trigger the resendProvisionalTimer only for the first non 100 provisional response.
-      if (this.resendProvisionalTimer === null)
-      {
-        this.resendProvisionalTimer = setInterval(() =>
-        {
-          this.resend_provisional();
-        }, 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);
-
-          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();
-            if (onFailure)
-            {
-              onFailure();
-            }
-          }
-          else if (onSuccess)
-          {
-            onSuccess();
-          }
-          break;
-      }
-    }
-    else if (status_code >= 300 && status_code <= 699)
-    {
-      switch (this.state)
-      {
-        case C.STATUS_PROCEEDING:
-          if (this.resendProvisionalTimer !== null)
-          {
-            clearInterval(this.resendProvisionalTimer);
-            this.resendProvisionalTimer = null;
-          }
-
-          if (!this.transport.send(response))
-          {
-            this.onTransportError();
-            if (onFailure)
-            {
-              onFailure();
-            }
-          }
-          else
-          {
-            this.stateChanged(C.STATUS_COMPLETED);
-            this.H = setTimeout(() =>
-            {
-              this.timer_H();
-            }, Timers.TIMER_H);
-            if (onSuccess)
-            {
-              onSuccess();
-            }
-          }
-          break;
-      }
-    }
-  }
+class InviteServerTransaction extends EventEmitter {
+	constructor(ua, transport, request) {
+		super();
+
+		this.type = C.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;
+
+		ua.newTransaction(this);
+
+		this.resendProvisionalTimer = null;
+
+		request.reply(100);
+	}
+
+	get C() {
+		return C;
+	}
+
+	stateChanged(state) {
+		this.state = state;
+		this.emit('stateChanged');
+	}
+
+	timer_H() {
+		loggerist.debug(`Timer H expired for transaction ${this.id}`);
+
+		if (this.state === C.STATUS_COMPLETED) {
+			loggerist.debug('ACK not received, dialog will be terminated');
+		}
+
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+	}
+
+	timer_I() {
+		this.stateChanged(C.STATUS_TERMINATED);
+		this.ua.destroyTransaction(this);
+	}
+
+	// RFC 6026 7.1.
+	timer_L() {
+		loggerist.debug(`Timer L expired for transaction ${this.id}`);
+
+		if (this.state === C.STATUS_ACCEPTED) {
+			this.stateChanged(C.STATUS_TERMINATED);
+			this.ua.destroyTransaction(this);
+		}
+	}
+
+	onTransportError() {
+		if (!this.transportError) {
+			this.transportError = true;
+
+			loggerist.debug(
+				`transport error occurred, deleting transaction ${this.id}`
+			);
+
+			if (this.resendProvisionalTimer !== null) {
+				clearInterval(this.resendProvisionalTimer);
+				this.resendProvisionalTimer = null;
+			}
+
+			clearTimeout(this.L);
+			clearTimeout(this.H);
+			clearTimeout(this.I);
+
+			this.stateChanged(C.STATUS_TERMINATED);
+			this.ua.destroyTransaction(this);
+		}
+	}
+
+	resend_provisional() {
+		if (!this.transport.send(this.last_response)) {
+			this.onTransportError();
+		}
+	}
+
+	// INVITE Server Transaction RFC 3261 17.2.1.
+	receiveResponse(status_code, response, onSuccess, onFailure) {
+		if (status_code >= 100 && status_code <= 199) {
+			switch (this.state) {
+				case C.STATUS_PROCEEDING: {
+					if (!this.transport.send(response)) {
+						this.onTransportError();
+					}
+					this.last_response = response;
+					break;
+				}
+			}
+		}
+
+		if (
+			status_code > 100 &&
+			status_code <= 199 &&
+			this.state === C.STATUS_PROCEEDING
+		) {
+			// Trigger the resendProvisionalTimer only for the first non 100 provisional response.
+			if (this.resendProvisionalTimer === null) {
+				this.resendProvisionalTimer = setInterval(() => {
+					this.resend_provisional();
+				}, 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);
+
+					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();
+						if (onFailure) {
+							onFailure();
+						}
+					} else if (onSuccess) {
+						onSuccess();
+					}
+					break;
+				}
+			}
+		} else if (status_code >= 300 && status_code <= 699) {
+			switch (this.state) {
+				case C.STATUS_PROCEEDING: {
+					if (this.resendProvisionalTimer !== null) {
+						clearInterval(this.resendProvisionalTimer);
+						this.resendProvisionalTimer = null;
+					}
+
+					if (!this.transport.send(response)) {
+						this.onTransportError();
+						if (onFailure) {
+							onFailure();
+						}
+					} else {
+						this.stateChanged(C.STATUS_COMPLETED);
+						this.H = setTimeout(() => {
+							this.timer_H();
+						}, Timers.TIMER_H);
+						if (onSuccess) {
+							onSuccess();
+						}
+					}
+					break;
+				}
+			}
+		}
+	}
 }

 /**
@@ -703,103 +630,95 @@ class InviteServerTransaction extends EventEmitter
  *  _true_  retransmission
  *  _false_ new request
  */
-function checkTransaction({ _transactions }, request)
-{
-  let tr;
-
-  switch (request.method)
-  {
-    case JsSIP_C.INVITE:
-      tr = _transactions.ist[request.via_branch];
-      if (tr)
-      {
-        switch (tr.state)
-        {
-          case C.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:
-            break;
-        }
-
-        return true;
-      }
-      break;
-    case JsSIP_C.ACK:
-      tr = _transactions.ist[request.via_branch];
-
-      // RFC 6026 7.1.
-      if (tr)
-      {
-        if (tr.state === C.STATUS_ACCEPTED)
-        {
-          return false;
-        }
-        else if (tr.state === C.STATUS_COMPLETED)
-        {
-          tr.state = C.STATUS_CONFIRMED;
-          tr.I = setTimeout(() => { tr.timer_I(); }, Timers.TIMER_I);
-
-          return true;
-        }
-      }
-      // ACK to 2XX Response.
-      else
-      {
-        return false;
-      }
-      break;
-    case JsSIP_C.CANCEL:
-      tr = _transactions.ist[request.via_branch];
-      if (tr)
-      {
-        request.reply_sl(200);
-        if (tr.state === C.STATUS_PROCEEDING)
-        {
-          return false;
-        }
-        else
-        {
-          return true;
-        }
-      }
-      else
-      {
-        request.reply_sl(481);
-
-        return true;
-      }
-    default:
-
-      // Non-INVITE Server Transaction RFC 3261 17.2.2.
-      tr = _transactions.nist[request.via_branch];
-      if (tr)
-      {
-        switch (tr.state)
-        {
-          case C.STATUS_TRYING:
-            break;
-          case C.STATUS_PROCEEDING:
-          case C.STATUS_COMPLETED:
-            tr.transport.send(tr.last_response);
-            break;
-        }
-
-        return true;
-      }
-      break;
-  }
+function checkTransaction({ _transactions }, request) {
+	let tr;
+
+	switch (request.method) {
+		case JsSIP_C.INVITE: {
+			tr = _transactions.ist[request.via_branch];
+			if (tr) {
+				switch (tr.state) {
+					case C.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: {
+						break;
+					}
+				}
+
+				return true;
+			}
+			break;
+		}
+		case JsSIP_C.ACK: {
+			tr = _transactions.ist[request.via_branch];
+
+			// RFC 6026 7.1.
+			if (tr) {
+				if (tr.state === C.STATUS_ACCEPTED) {
+					return false;
+				} else if (tr.state === C.STATUS_COMPLETED) {
+					tr.state = C.STATUS_CONFIRMED;
+					tr.I = setTimeout(() => {
+						tr.timer_I();
+					}, Timers.TIMER_I);
+
+					return true;
+				}
+			}
+			// ACK to 2XX Response.
+			else {
+				return false;
+			}
+			break;
+		}
+		case JsSIP_C.CANCEL: {
+			tr = _transactions.ist[request.via_branch];
+			if (tr) {
+				request.reply_sl(200);
+				if (tr.state === C.STATUS_PROCEEDING) {
+					return false;
+				} else {
+					return true;
+				}
+			} else {
+				request.reply_sl(481);
+
+				return true;
+			}
+		}
+		default: {
+			// Non-INVITE Server Transaction RFC 3261 17.2.2.
+			tr = _transactions.nist[request.via_branch];
+			if (tr) {
+				switch (tr.state) {
+					case C.STATUS_TRYING: {
+						break;
+					}
+					case C.STATUS_PROCEEDING:
+					case C.STATUS_COMPLETED: {
+						tr.transport.send(tr.last_response);
+						break;
+					}
+				}
+
+				return true;
+			}
+			break;
+		}
+	}
 }

 module.exports = {
-  C,
-  NonInviteClientTransaction,
-  InviteClientTransaction,
-  AckClientTransaction,
-  NonInviteServerTransaction,
-  InviteServerTransaction,
-  checkTransaction
+	C,
+	NonInviteClientTransaction,
+	InviteClientTransaction,
+	AckClientTransaction,
+	NonInviteServerTransaction,
+	InviteServerTransaction,
+	checkTransaction,
 };
diff --git a/src/Transport.d.ts b/src/Transport.d.ts
index 5d9e79a..0829a9b 100644
--- a/src/Transport.d.ts
+++ b/src/Transport.d.ts
@@ -1,10 +1,10 @@
-import {Socket} from './Socket'
+import { Socket } from './Socket';

 export interface RecoveryOptions {
-  min_interval: number;
-  max_interval: number;
+	min_interval: number;
+	max_interval: number;
 }

 export class Transport extends Socket {
-  constructor(sockets: Socket | Socket[], recovery_options?: RecoveryOptions)
+	constructor(sockets: Socket | Socket[], recovery_options?: RecoveryOptions);
 }
diff --git a/src/Transport.js b/src/Transport.js
index ca2d715..258382c 100644
--- a/src/Transport.js
+++ b/src/Transport.js
@@ -8,22 +8,22 @@ const logger = new Logger('Transport');
  * Constants
  */
 const C = {
-  // Transport status.
-  STATUS_CONNECTED    : 0,
-  STATUS_CONNECTING   : 1,
-  STATUS_DISCONNECTED : 2,
-
-  // Socket status.
-  SOCKET_STATUS_READY : 0,
-  SOCKET_STATUS_ERROR : 1,
-
-  // Recovery options.
-  recovery_options : {
-    // minimum interval in seconds between recover attempts.
-    min_interval : JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
-    // maximum interval in seconds between recover attempts.
-    max_interval : JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL
-  }
+	// Transport status.
+	STATUS_CONNECTED: 0,
+	STATUS_CONNECTING: 1,
+	STATUS_DISCONNECTED: 2,
+
+	// Socket status.
+	SOCKET_STATUS_READY: 0,
+	SOCKET_STATUS_ERROR: 1,
+
+	// Recovery options.
+	recovery_options: {
+		// minimum interval in seconds between recover attempts.
+		min_interval: JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
+		// maximum interval in seconds between recover attempts.
+		max_interval: JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL,
+	},
 };

 /*
@@ -32,366 +32,314 @@ const C = {
  *
  * @socket JsSIP::Socket instance
  */
-module.exports = class Transport
-{
-  constructor(sockets, recovery_options = C.recovery_options)
-  {
-    logger.debug('new()');
-
-    this.status = C.STATUS_DISCONNECTED;
-
-    // Current socket.
-    this.socket = null;
-
-    // Socket collection.
-    this.sockets = [];
-
-    this.recovery_options = recovery_options;
-    this.recover_attempts = 0;
-    this.recovery_timer = null;
-
-    this.close_requested = false;
-
-    // It seems that TextDecoder is not available in some versions of React-Native.
-    // See https://github.com/versatica/JsSIP/issues/695
-    try
-    {
-      this.textDecoder = new TextDecoder('utf8');
-    }
-    catch (error)
-    {
-      logger.warn(`cannot use TextDecoder: ${error}`);
-    }
-
-    if (typeof sockets === 'undefined')
-    {
-      throw new TypeError('Invalid argument.' +
-                          ' undefined \'sockets\' argument');
-    }
-
-    if (!(sockets instanceof Array))
-    {
-      sockets = [ sockets ];
-    }
-
-    sockets.forEach(function(socket)
-    {
-      if (!Socket.isSocket(socket.socket))
-      {
-        throw new TypeError('Invalid argument.' +
-                            ' invalid \'JsSIP.Socket\' instance');
-      }
-
-      if (socket.weight && !Number(socket.weight))
-      {
-        throw new TypeError('Invalid argument.' +
-                            ' \'weight\' attribute is not a number');
-      }
-
-      this.sockets.push({
-        socket : socket.socket,
-        weight : socket.weight || 0,
-        status : C.SOCKET_STATUS_READY
-      });
-    }, this);
-
-    // Get the socket with higher weight.
-    this._getSocket();
-  }
-
-  /**
-   * Instance Methods
-   */
-
-  get via_transport()
-  {
-    return this.socket.via_transport;
-  }
-
-  get url()
-  {
-    return this.socket.url;
-  }
-
-  get sip_uri()
-  {
-    return this.socket.sip_uri;
-  }
-
-  connect()
-  {
-    logger.debug('connect()');
-
-    if (this.isConnected())
-    {
-      logger.debug('Transport is already connected');
-
-      return;
-    }
-    else if (this.isConnecting())
-    {
-      logger.debug('Transport is connecting');
-
-      return;
-    }
-
-    this.close_requested = false;
-    this.status = C.STATUS_CONNECTING;
-    this.onconnecting({ socket: this.socket, attempts: this.recover_attempts });
-
-    if (!this.close_requested)
-    {
-      // Bind socket event callbacks.
-      this.socket.onconnect = this._onConnect.bind(this);
-      this.socket.ondisconnect = this._onDisconnect.bind(this);
-      this.socket.ondata = this._onData.bind(this);
-
-      this.socket.connect();
-    }
-
-    return;
-  }
-
-  disconnect()
-  {
-    logger.debug('close()');
-
-    this.close_requested = true;
-    this.recover_attempts = 0;
-    this.status = C.STATUS_DISCONNECTED;
-
-    // Clear recovery_timer.
-    if (this.recovery_timer !== null)
-    {
-      clearTimeout(this.recovery_timer);
-      this.recovery_timer = null;
-    }
-
-    // Unbind socket event callbacks.
-    this.socket.onconnect = () => {};
-    this.socket.ondisconnect = () => {};
-    this.socket.ondata = () => {};
-
-    this.socket.disconnect();
-    this.ondisconnect({
-      socket : this.socket,
-      error  : false
-    });
-  }
-
-  send(data)
-  {
-    logger.debug('send()');
-
-    if (!this.isConnected())
-    {
-      logger.warn('unable to send message, transport is not connected');
-
-      return false;
-    }
-
-    const message = data.toString();
-
-    logger.debug(`sending message:\n\n${message}\n`);
-
-    return this.socket.send(message);
-  }
-
-  isConnected()
-  {
-    return this.status === C.STATUS_CONNECTED;
-  }
-
-  isConnecting()
-  {
-    return this.status === C.STATUS_CONNECTING;
-  }
-
-  /**
-   * Private API.
-   */
-
-  _reconnect()
-  {
-    this.recover_attempts+=1;
-
-    let k = Math.floor((Math.random() * Math.pow(2, this.recover_attempts)) +1);
-
-    if (k < this.recovery_options.min_interval)
-    {
-      k = this.recovery_options.min_interval;
-    }
-
-    else if (k > this.recovery_options.max_interval)
-    {
-      k = this.recovery_options.max_interval;
-    }
-
-    logger.debug(`reconnection attempt: ${this.recover_attempts}. next connection attempt in ${k} seconds`);
-
-    this.recovery_timer = setTimeout(() =>
-    {
-      if (!this.close_requested && !(this.isConnected() || this.isConnecting()))
-      {
-        // Get the next available socket with higher weight.
-        this._getSocket();
-
-        // Connect the socket.
-        this.connect();
-      }
-    }, k * 1000);
-  }
-
-  /**
-   * get the next available socket with higher weight
-   */
-  _getSocket()
-  {
-
-    let candidates = [];
-
-    this.sockets.forEach((socket) =>
-    {
-      if (socket.status === C.SOCKET_STATUS_ERROR)
-      {
-        return; // continue the array iteration
-      }
-      else if (candidates.length === 0)
-      {
-        candidates.push(socket);
-      }
-      else if (socket.weight > candidates[0].weight)
-      {
-        candidates = [ socket ];
-      }
-      else if (socket.weight === candidates[0].weight)
-      {
-        candidates.push(socket);
-      }
-    });
-
-    if (candidates.length === 0)
-    {
-      // All sockets have failed. reset sockets status.
-      this.sockets.forEach((socket) =>
-      {
-        socket.status = C.SOCKET_STATUS_READY;
-      });
-
-      // Get next available socket.
-      this._getSocket();
-
-      return;
-    }
-
-    const idx = Math.floor((Math.random()* candidates.length));
-
-    this.socket = candidates[idx].socket;
-  }
-
-  /**
-   * Socket Event Handlers
-   */
-
-  _onConnect()
-  {
-    this.recover_attempts = 0;
-    this.status = C.STATUS_CONNECTED;
-
-    // Clear recovery_timer.
-    if (this.recovery_timer !== null)
-    {
-      clearTimeout(this.recovery_timer);
-      this.recovery_timer = null;
-    }
-
-    this.onconnect({ socket: this });
-  }
-
-  _onDisconnect(error, code, reason)
-  {
-    this.status = C.STATUS_DISCONNECTED;
-    this.ondisconnect({
-      socket : this.socket,
-      error,
-      code,
-      reason
-    });
-
-    if (this.close_requested)
-    {
-      return;
-    }
-
-    // Update socket status.
-    else
-    {
-      this.sockets.forEach(function(socket)
-      {
-        if (this.socket === socket.socket)
-        {
-          socket.status = C.SOCKET_STATUS_ERROR;
-        }
-      }, this);
-    }
-
-    this._reconnect(error);
-  }
-
-  _onData(data)
-  {
-    // CRLF Keep Alive request from server, reply.
-    if (data === '\r\n\r\n')
-    {
-      logger.debug('received message with double-CRLF Keep Alive request');
-
-      try
-      {
-        // Reply with single CRLF.
-        this.socket.send('\r\n');
-      }
-      catch (error)
-      {
-        logger.warn(`error sending Keep Alive response: ${error}`);
-      }
-
-      return;
-    }
-
-    // CRLF Keep Alive response from server, ignore it.
-    if (data === '\r\n')
-    {
-      logger.debug('received message with CRLF Keep Alive response');
-
-      return;
-    }
-
-    // Binary message.
-    else if (typeof data !== 'string')
-    {
-      try
-      {
-        if (this.textDecoder)
-          data = this.textDecoder.decode(data);
-        else
-          data = String.fromCharCode.apply(null, new Uint8Array(data));
-      }
-      catch (error)
-      {
-        logger.debug(`received binary message failed to be converted into string: ${error}`);
-
-        return;
-      }
-
-      logger.debug(`received binary message:\n\n${data}\n`);
-    }
-
-    // Text message.
-    else
-    {
-      logger.debug(`received text message:\n\n${data}\n`);
-    }
-
-    this.ondata({ transport: this, message: data });
-  }
+module.exports = class Transport {
+	constructor(sockets, recovery_options = C.recovery_options) {
+		logger.debug('new()');
+
+		this.status = C.STATUS_DISCONNECTED;
+
+		// Current socket.
+		this.socket = null;
+
+		// Socket collection.
+		this.sockets = [];
+
+		this.recovery_options = recovery_options;
+		this.recover_attempts = 0;
+		this.recovery_timer = null;
+
+		this.close_requested = false;
+
+		// It seems that TextDecoder is not available in some versions of React-Native.
+		// See https://github.com/versatica/JsSIP/issues/695
+		try {
+			this.textDecoder = new TextDecoder('utf8');
+		} catch (error) {
+			logger.warn(`cannot use TextDecoder: ${error}`);
+		}
+
+		if (typeof sockets === 'undefined') {
+			throw new TypeError("Invalid argument. undefined 'sockets' argument");
+		}
+
+		if (!(sockets instanceof Array)) {
+			sockets = [sockets];
+		}
+
+		sockets.forEach(function (socket) {
+			if (!Socket.isSocket(socket.socket)) {
+				throw new TypeError(
+					"Invalid argument. invalid 'JsSIP.Socket' instance"
+				);
+			}
+
+			if (socket.weight && !Number(socket.weight)) {
+				throw new TypeError(
+					"Invalid argument. 'weight' attribute is not a number"
+				);
+			}
+
+			this.sockets.push({
+				socket: socket.socket,
+				weight: socket.weight || 0,
+				status: C.SOCKET_STATUS_READY,
+			});
+		}, this);
+
+		// Get the socket with higher weight.
+		this._getSocket();
+	}
+
+	/**
+	 * Instance Methods
+	 */
+
+	get via_transport() {
+		return this.socket.via_transport;
+	}
+
+	get url() {
+		return this.socket.url;
+	}
+
+	get sip_uri() {
+		return this.socket.sip_uri;
+	}
+
+	connect() {
+		logger.debug('connect()');
+
+		if (this.isConnected()) {
+			logger.debug('Transport is already connected');
+
+			return;
+		} else if (this.isConnecting()) {
+			logger.debug('Transport is connecting');
+
+			return;
+		}
+
+		this.close_requested = false;
+		this.status = C.STATUS_CONNECTING;
+		this.onconnecting({ socket: this.socket, attempts: this.recover_attempts });
+
+		if (!this.close_requested) {
+			// Bind socket event callbacks.
+			this.socket.onconnect = this._onConnect.bind(this);
+			this.socket.ondisconnect = this._onDisconnect.bind(this);
+			this.socket.ondata = this._onData.bind(this);
+
+			this.socket.connect();
+		}
+
+		return;
+	}
+
+	disconnect() {
+		logger.debug('close()');
+
+		this.close_requested = true;
+		this.recover_attempts = 0;
+		this.status = C.STATUS_DISCONNECTED;
+
+		// Clear recovery_timer.
+		if (this.recovery_timer !== null) {
+			clearTimeout(this.recovery_timer);
+			this.recovery_timer = null;
+		}
+
+		// Unbind socket event callbacks.
+		this.socket.onconnect = () => {};
+		this.socket.ondisconnect = () => {};
+		this.socket.ondata = () => {};
+
+		this.socket.disconnect();
+		this.ondisconnect({
+			socket: this.socket,
+			error: false,
+		});
+	}
+
+	send(data) {
+		logger.debug('send()');
+
+		if (!this.isConnected()) {
+			logger.warn('unable to send message, transport is not connected');
+
+			return false;
+		}
+
+		const message = data.toString();
+
+		logger.debug(`sending message:\n\n${message}\n`);
+
+		return this.socket.send(message);
+	}
+
+	isConnected() {
+		return this.status === C.STATUS_CONNECTED;
+	}
+
+	isConnecting() {
+		return this.status === C.STATUS_CONNECTING;
+	}
+
+	/**
+	 * Private API.
+	 */
+
+	_reconnect() {
+		this.recover_attempts += 1;
+
+		let k = Math.floor(Math.random() * Math.pow(2, this.recover_attempts) + 1);
+
+		if (k < this.recovery_options.min_interval) {
+			k = this.recovery_options.min_interval;
+		} else if (k > this.recovery_options.max_interval) {
+			k = this.recovery_options.max_interval;
+		}
+
+		logger.debug(
+			`reconnection attempt: ${this.recover_attempts}. next connection attempt in ${k} seconds`
+		);
+
+		this.recovery_timer = setTimeout(() => {
+			if (
+				!this.close_requested &&
+				!(this.isConnected() || this.isConnecting())
+			) {
+				// Get the next available socket with higher weight.
+				this._getSocket();
+
+				// Connect the socket.
+				this.connect();
+			}
+		}, k * 1000);
+	}
+
+	/**
+	 * get the next available socket with higher weight
+	 */
+	_getSocket() {
+		let candidates = [];
+
+		this.sockets.forEach(socket => {
+			if (socket.status === C.SOCKET_STATUS_ERROR) {
+				return; // continue the array iteration
+			} else if (candidates.length === 0) {
+				candidates.push(socket);
+			} else if (socket.weight > candidates[0].weight) {
+				candidates = [socket];
+			} else if (socket.weight === candidates[0].weight) {
+				candidates.push(socket);
+			}
+		});
+
+		if (candidates.length === 0) {
+			// All sockets have failed. reset sockets status.
+			this.sockets.forEach(socket => {
+				socket.status = C.SOCKET_STATUS_READY;
+			});
+
+			// Get next available socket.
+			this._getSocket();
+
+			return;
+		}
+
+		const idx = Math.floor(Math.random() * candidates.length);
+
+		this.socket = candidates[idx].socket;
+	}
+
+	/**
+	 * Socket Event Handlers
+	 */
+
+	_onConnect() {
+		this.recover_attempts = 0;
+		this.status = C.STATUS_CONNECTED;
+
+		// Clear recovery_timer.
+		if (this.recovery_timer !== null) {
+			clearTimeout(this.recovery_timer);
+			this.recovery_timer = null;
+		}
+
+		this.onconnect({ socket: this });
+	}
+
+	_onDisconnect(error, code, reason) {
+		this.status = C.STATUS_DISCONNECTED;
+		this.ondisconnect({
+			socket: this.socket,
+			error,
+			code,
+			reason,
+		});
+
+		if (this.close_requested) {
+			return;
+		}
+
+		// Update socket status.
+		else {
+			this.sockets.forEach(function (socket) {
+				if (this.socket === socket.socket) {
+					socket.status = C.SOCKET_STATUS_ERROR;
+				}
+			}, this);
+		}
+
+		this._reconnect(error);
+	}
+
+	_onData(data) {
+		// CRLF Keep Alive request from server, reply.
+		if (data === '\r\n\r\n') {
+			logger.debug('received message with double-CRLF Keep Alive request');
+
+			try {
+				// Reply with single CRLF.
+				this.socket.send('\r\n');
+			} catch (error) {
+				logger.warn(`error sending Keep Alive response: ${error}`);
+			}
+
+			return;
+		}
+
+		// CRLF Keep Alive response from server, ignore it.
+		if (data === '\r\n') {
+			logger.debug('received message with CRLF Keep Alive response');
+
+			return;
+		}
+
+		// Binary message.
+		else if (typeof data !== 'string') {
+			try {
+				if (this.textDecoder) {
+					data = this.textDecoder.decode(data);
+				} else {
+					data = String.fromCharCode.apply(null, new Uint8Array(data));
+				}
+			} catch (error) {
+				logger.debug(
+					`received binary message failed to be converted into string: ${error}`
+				);
+
+				return;
+			}
+
+			logger.debug(`received binary message:\n\n${data}\n`);
+		}
+
+		// Text message.
+		else {
+			logger.debug(`received text message:\n\n${data}\n`);
+		}
+
+		this.ondata({ transport: this, message: data });
+	}
 };
diff --git a/src/UA.d.ts b/src/UA.d.ts
index 0fff768..ae562cc 100644
--- a/src/UA.d.ts
+++ b/src/UA.d.ts
@@ -1,113 +1,123 @@
-import {EventEmitter} from 'events'
-
-import {Socket, WeightedSocket} from './Socket'
-import {AnswerOptions, Originator, RTCSession, RTCSessionEventMap, TerminateOptions} from './RTCSession'
-import {IncomingRequest, IncomingResponse, OutgoingRequest} from './SIPMessage'
-import {Message, SendMessageOptions} from './Message'
-import {Registrator} from './Registrator'
-import {Notifier} from './Notifier'
-import {Subscriber} from './Subscriber'
-import {URI} from './URI'
-import {causes} from './Constants'
+import { EventEmitter } from 'events';
+
+import { Socket, WeightedSocket } from './Socket';
+import {
+	AnswerOptions,
+	Originator,
+	RTCSession,
+	RTCSessionEventMap,
+	TerminateOptions,
+} from './RTCSession';
+import {
+	IncomingRequest,
+	IncomingResponse,
+	OutgoingRequest,
+} from './SIPMessage';
+import { Message, SendMessageOptions } from './Message';
+import { Registrator } from './Registrator';
+import { Notifier } from './Notifier';
+import { Subscriber } from './Subscriber';
+import { URI } from './URI';
+import { causes } from './Constants';

 export interface UnRegisterOptions {
-  all?: boolean;
+	all?: boolean;
 }

 export interface CallOptions extends AnswerOptions {
-  eventHandlers?: Partial<RTCSessionEventMap>;
-  anonymous?: boolean;
-  fromUserName?: string;
-  fromDisplayName?: string;
+	eventHandlers?: Partial<RTCSessionEventMap>;
+	anonymous?: boolean;
+	fromUserName?: string;
+	fromDisplayName?: string;
 }

 export interface UAConfiguration {
-  // mandatory parameters
-  sockets: Socket | Socket[] | WeightedSocket[] ;
-  uri: string;
-  // optional parameters
-  authorization_jwt?: string;
-  authorization_user?: string;
-  connection_recovery_max_interval?: number;
-  connection_recovery_min_interval?: number;
-  contact_uri?: string;
-  display_name?: string;
-  instance_id?: string;
-  no_answer_timeout?: number;
-  session_timers?: boolean;
-  session_timers_refresh_method?: string;
-  session_timers_force_refresher?: boolean;
-  password?: string;
-  realm?: string;
-  ha1?: string;
-  register?: boolean;
-  register_expires?: number;
-  register_from_tag_trail?: string | (() => string);
-  registrar_server?: string;
-  use_preloaded_route?: boolean;
-  user_agent?: string;
-  extra_headers?: string[];
+	// mandatory parameters
+	sockets: Socket | Socket[] | WeightedSocket[];
+	uri: string;
+	// optional parameters
+	authorization_jwt?: string;
+	authorization_user?: string;
+	connection_recovery_max_interval?: number;
+	connection_recovery_min_interval?: number;
+	contact_uri?: string;
+	display_name?: string;
+	instance_id?: string;
+	no_answer_timeout?: number;
+	session_timers?: boolean;
+	session_timers_refresh_method?: string;
+	session_timers_force_refresher?: boolean;
+	password?: string;
+	realm?: string;
+	ha1?: string;
+	register?: boolean;
+	register_expires?: number;
+	register_from_tag_trail?: string | (() => string);
+	registrar_server?: string;
+	use_preloaded_route?: boolean;
+	user_agent?: string;
+	extra_headers?: string[];
 }

 export interface IncomingRTCSessionEvent {
-  originator: Originator.REMOTE;
-  session: RTCSession;
-  request: IncomingRequest;
+	originator: Originator.REMOTE;
+	session: RTCSession;
+	request: IncomingRequest;
 }

 export interface OutgoingRTCSessionEvent {
-  originator: Originator.LOCAL;
-  session: RTCSession;
-  request: OutgoingRequest;
+	originator: Originator.LOCAL;
+	session: RTCSession;
+	request: OutgoingRequest;
 }

 export type RTCSessionEvent = IncomingRTCSessionEvent | OutgoingRTCSessionEvent;

 export interface ConnectingEvent {
-  socket: Socket;
-  attempts: number
+	socket: Socket;
+	attempts: number;
 }

 export interface ConnectedEvent {
-  socket: Socket;
+	socket: Socket;
 }

 export interface DisconnectEvent {
-  socket: Socket;
-  error: boolean;
-  code?: number;
-  reason?: string;
+	socket: Socket;
+	error: boolean;
+	code?: number;
+	reason?: string;
 }

 export interface RegisteredEvent {
-  response: IncomingResponse;
+	response: IncomingResponse;
 }

 export interface UnRegisteredEvent {
-  response: IncomingResponse;
-  cause?: causes;
+	response: IncomingResponse;
+	cause?: causes;
 }

 export interface IncomingMessageEvent {
-  originator: Originator.REMOTE;
-  message: Message;
-  request: IncomingRequest;
+	originator: Originator.REMOTE;
+	message: Message;
+	request: IncomingRequest;
 }

 export interface OutgoingMessageEvent {
-  originator: Originator.LOCAL;
-  message: Message;
-  request: OutgoingRequest;
+	originator: Originator.LOCAL;
+	message: Message;
+	request: OutgoingRequest;
 }

 export interface IncomingOptionsEvent {
-  originator: Originator.REMOTE;
-  request: IncomingRequest;
+	originator: Originator.REMOTE;
+	request: IncomingRequest;
 }

 export interface OutgoingOptionsEvent {
-  originator: Originator.LOCAL;
-  request: OutgoingRequest;
+	originator: Originator.LOCAL;
+	request: OutgoingRequest;
 }

 export type ConnectingListener = (event: ConnectingEvent) => void;
@@ -117,127 +127,159 @@ export type RegisteredListener = (event: RegisteredEvent) => void;
 export type UnRegisteredListener = (event: UnRegisteredEvent) => void;
 export type RegistrationFailedListener = UnRegisteredListener;
 export type RegistrationExpiringListener = () => void;
-export type IncomingRTCSessionListener = (event: IncomingRTCSessionEvent) => void;
-export type OutgoingRTCSessionListener = (event: OutgoingRTCSessionEvent) => void;
-export type RTCSessionListener = IncomingRTCSessionListener | OutgoingRTCSessionListener;
+export type IncomingRTCSessionListener = (
+	event: IncomingRTCSessionEvent
+) => void;
+export type OutgoingRTCSessionListener = (
+	event: OutgoingRTCSessionEvent
+) => void;
+export type RTCSessionListener =
+	| IncomingRTCSessionListener
+	| OutgoingRTCSessionListener;
 export type IncomingMessageListener = (event: IncomingMessageEvent) => void;
 export type OutgoingMessageListener = (event: OutgoingMessageEvent) => void;
 export type MessageListener = IncomingMessageListener | OutgoingMessageListener;
 export type IncomingOptionsListener = (event: IncomingOptionsEvent) => void;
 export type OutgoingOptionsListener = (event: OutgoingOptionsEvent) => void;
 export type OptionsListener = IncomingOptionsListener | OutgoingOptionsListener;
-export type SipEventListener = <T = any>(event: { event: T; request: IncomingRequest; }) => void
-export type SipSubscribeListener = <T = any>(event: { event: T; request: IncomingRequest; }) => void
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type SipEventListener = <T = any>(event: {
+	event: T;
+	request: IncomingRequest;
+}) => void;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type SipSubscribeListener = <T = any>(event: {
+	event: T;
+	request: IncomingRequest;
+}) => void;

 export interface UAEventMap {
-  connecting: ConnectingListener;
-  connected: ConnectedListener;
-  disconnected: DisconnectedListener;
-  registered: RegisteredListener;
-  unregistered: UnRegisteredListener;
-  registrationFailed: RegistrationFailedListener;
-  registrationExpiring: RegistrationExpiringListener;
-  newRTCSession: RTCSessionListener;
-  newMessage: MessageListener;
-  sipEvent: SipEventListener;
-  newSubscribe: SipSubscribeListener;
-  newOptions: OptionsListener;
+	connecting: ConnectingListener;
+	connected: ConnectedListener;
+	disconnected: DisconnectedListener;
+	registered: RegisteredListener;
+	unregistered: UnRegisteredListener;
+	registrationFailed: RegistrationFailedListener;
+	registrationExpiring: RegistrationExpiringListener;
+	newRTCSession: RTCSessionListener;
+	newMessage: MessageListener;
+	sipEvent: SipEventListener;
+	newSubscribe: SipSubscribeListener;
+	newOptions: OptionsListener;
 }

 export interface UAContactOptions {
-  anonymous?: boolean;
-  outbound?: boolean;
+	anonymous?: boolean;
+	outbound?: boolean;
 }

 export interface UAContact {
-  pub_gruu?: string,
-  temp_gruu?: string,
-  uri?: string;
+	pub_gruu?: string;
+	temp_gruu?: string;
+	uri?: string;

-  toString(options?: UAContactOptions): string
+	toString(options?: UAContactOptions): string;
 }

 export interface RequestParams {
-  from_uri: URI;
-  from_display_name?: string;
-  from_tag: string;
-  to_uri: URI;
-  to_display_name?: string;
-  to_tag?: string;
-  call_id: string;
-  cseq: number;
+	from_uri: URI;
+	from_display_name?: string;
+	from_tag: string;
+	to_uri: URI;
+	to_display_name?: string;
+	to_tag?: string;
+	call_id: string;
+	cseq: number;
 }

 export interface SubscriberParams {
-  from_uri: URI;
-  from_display_name?: string;
-  to_uri: URI;
-  to_display_name?: string;
+	from_uri: URI;
+	from_display_name?: string;
+	to_uri: URI;
+	to_display_name?: string;
 }

 export interface SubscriberOptions {
-  expires?: number;
-  contentType?: string;
-  allowEvents?: string;
-  params?: SubscriberParams;
-  extraHeaders?: string[];
+	expires?: number;
+	contentType?: string;
+	allowEvents?: string;
+	params?: SubscriberParams;
+	extraHeaders?: string[];
 }

 export interface NotifierOptions {
-  allowEvents?: string;
-  extraHeaders?: string[];
-  pending?: boolean;
+	allowEvents?: string;
+	extraHeaders?: string[];
+	pending?: boolean;
 }

 declare enum UAStatus {
-  // UA status codes.
-  STATUS_INIT = 0,
-  STATUS_READY = 1,
-  STATUS_USER_CLOSED = 2,
-  STATUS_NOT_READY = 3,
-  // UA error codes.
-  CONFIGURATION_ERROR = 1,
-  NETWORK_ERROR = 2
+	// UA status codes.
+	STATUS_INIT = 0,
+	STATUS_READY = 1,
+	STATUS_USER_CLOSED = 2,
+	STATUS_NOT_READY = 3,
+	// UA error codes.
+	// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
+	CONFIGURATION_ERROR = 1,
+	// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
+	NETWORK_ERROR = 2,
 }

 export class UA extends EventEmitter {
-  static get C(): typeof UAStatus;
+	static get C(): typeof UAStatus;

-  constructor(configuration: UAConfiguration);
+	constructor(configuration: UAConfiguration);

-  get C(): typeof UAStatus;
+	get C(): typeof UAStatus;

-  get status(): UAStatus;
+	get status(): UAStatus;

-  get contact(): UAContact;
+	get contact(): UAContact;

-  start(): void;
+	start(): void;

-  stop(): void;
+	stop(): void;

-  register(): void;
+	register(): void;

-  unregister(options?: UnRegisterOptions): void;
+	unregister(options?: UnRegisterOptions): void;

-  registrator(): Registrator;
+	registrator(): Registrator;

-  call(target: string, options?: CallOptions): RTCSession;
+	call(target: string, options?: CallOptions): RTCSession;

-  sendMessage(target: string | URI, body: string, options?: SendMessageOptions): Message;
+	sendMessage(
+		target: string | URI,
+		body: string,
+		options?: SendMessageOptions
+	): Message;

-  subscribe(target: string, eventName: string, accept: string, options?: SubscriberOptions): Subscriber;
+	subscribe(
+		target: string,
+		eventName: string,
+		accept: string,
+		options?: SubscriberOptions
+	): Subscriber;

-  notify( subscribe: IncomingRequest, contentType: string, options?: NotifierOptions): Notifier;
+	notify(
+		subscribe: IncomingRequest,
+		contentType: string,
+		options?: NotifierOptions
+	): Notifier;

-  terminateSessions(options?: TerminateOptions): void;
+	terminateSessions(options?: TerminateOptions): void;

-  isRegistered(): boolean;
+	isRegistered(): boolean;

-  isConnected(): boolean;
+	isConnected(): boolean;

-  get<T extends keyof UAConfiguration>(parameter: T): UAConfiguration[T];
+	get<T extends keyof UAConfiguration>(parameter: T): UAConfiguration[T];

-  set<T extends keyof UAConfiguration>(parameter: T, value: UAConfiguration[T]): boolean;
+	set<T extends keyof UAConfiguration>(
+		parameter: T,
+		value: UAConfiguration[T]
+	): boolean;

-  on<T extends keyof UAEventMap>(type: T, listener: UAEventMap[T]): this;
+	on<T extends keyof UAEventMap>(type: T, listener: UAEventMap[T]): this;
 }
diff --git a/src/UA.js b/src/UA.js
index 066df6c..687ff37 100644
--- a/src/UA.js
+++ b/src/UA.js
@@ -1,3 +1,5 @@
+/* eslint-disable no-invalid-this */
+
 const EventEmitter = require('events').EventEmitter;
 const Logger = require('./Logger');
 const JsSIP_C = require('./Constants');
@@ -20,15 +22,15 @@ const config = require('./Config');
 const logger = new Logger('UA');

 const C = {
-  // UA status codes.
-  STATUS_INIT        : 0,
-  STATUS_READY       : 1,
-  STATUS_USER_CLOSED : 2,
-  STATUS_NOT_READY   : 3,
-
-  // UA error codes.
-  CONFIGURATION_ERROR : 1,
-  NETWORK_ERROR       : 2
+	// UA status codes.
+	STATUS_INIT: 0,
+	STATUS_READY: 1,
+	STATUS_USER_CLOSED: 2,
+	STATUS_NOT_READY: 3,
+
+	// UA error codes.
+	CONFIGURATION_ERROR: 1,
+	NETWORK_ERROR: 2,
 };

 /**
@@ -38,1004 +40,923 @@ const C = {
  * @throws {JsSIP.Exceptions.ConfigurationError} If a configuration parameter is invalid.
  * @throws {TypeError} If no configuration is given.
  */
-module.exports = class UA extends EventEmitter
-{
-  // Expose C object.
-  static get C()
-  {
-    return C;
-  }
-
-  constructor(configuration)
-  {
-    // Check configuration argument.
-    if (!configuration)
-    {
-      throw new TypeError('Not enough arguments');
-    }
-
-    // Hide sensitive information.
-    const sensitiveKeys = [ 'password', 'ha1', 'authorization_jwt' ];
-
-    logger.debug('new() [configuration:%o]',
-      Object.entries(configuration).filter(([ key ]) => !sensitiveKeys.includes(key))
-    );
-
-    super();
-
-    this._cache = {
-      credentials : {}
-    };
-
-    this._configuration = Object.assign({}, config.settings);
-    this._dynConfiguration = {};
-    this._dialogs = {};
-
-    // User actions outside any session/dialog (MESSAGE/OPTIONS).
-    this._applicants = {};
-
-    this._sessions = {};
-    this._transport = null;
-    this._contact = null;
-    this._status = C.STATUS_INIT;
-    this._error = null;
-    this._transactions = {
-      nist : {},
-      nict : {},
-      ist  : {},
-      ict  : {}
-    };
-
-    // Custom UA empty object for high level use.
-    this._data = {};
-
-    this._closeTimer = null;
-
-    // Load configuration.
-    try
-    {
-      this._loadConfig(configuration);
-    }
-    catch (error)
-    {
-      this._status = C.STATUS_NOT_READY;
-      this._error = C.CONFIGURATION_ERROR;
-      throw error;
-    }
-
-    // Initialize registrator.
-    this._registrator = new Registrator(this);
-  }
-
-  get C()
-  {
-    return C;
-  }
-
-  get status()
-  {
-    return this._status;
-  }
-
-  get contact()
-  {
-    return this._contact;
-  }
-
-  get configuration()
-  {
-    return this._configuration;
-  }
-
-  get transport()
-  {
-    return this._transport;
-  }
-
-  // =================
-  //  High Level API
-  // =================
-
-  /**
-   * Connect to the server if status = STATUS_INIT.
-   * Resume UA after being closed.
-   */
-  start()
-  {
-    logger.debug('start()');
-
-    if (this._status === C.STATUS_INIT)
-    {
-      this._transport.connect();
-    }
-    else if (this._status === C.STATUS_USER_CLOSED)
-    {
-      logger.debug('restarting UA');
-
-      // Disconnect.
-      if (this._closeTimer !== null)
-      {
-        clearTimeout(this._closeTimer);
-        this._closeTimer = null;
-        this._transport.disconnect();
-      }
-
-      // Reconnect.
-      this._status = C.STATUS_INIT;
-      this._transport.connect();
-    }
-    else if (this._status === C.STATUS_READY)
-    {
-      logger.debug('UA is in READY status, not restarted');
-    }
-    else
-    {
-      logger.debug('ERROR: connection is down, Auto-Recovery system is trying to reconnect');
-    }
-
-    // Set dynamic configuration.
-    this._dynConfiguration.register = this._configuration.register;
-  }
-
-  /**
-   * Register.
-   */
-  register()
-  {
-    logger.debug('register()');
-
-    this._dynConfiguration.register = true;
-    this._registrator.register();
-  }
-
-  /**
-   * Unregister.
-   */
-  unregister(options)
-  {
-    logger.debug('unregister()');
-
-    this._dynConfiguration.register = false;
-    this._registrator.unregister(options);
-  }
-
-  /**
-   * Get the Registrator instance.
-   */
-  registrator()
-  {
-    return this._registrator;
-  }
-
-  /**
-   * Registration state.
-   */
-  isRegistered()
-  {
-    return this._registrator.registered;
-  }
-
-  /**
-   * Connection state.
-   */
-  isConnected()
-  {
-    return this._transport.isConnected();
-  }
-
-  /**
-   * Make an outgoing call.
-   *
-   * -param {String} target
-   * -param {Object} [options]
-   *
-   * -throws {TypeError}
-   *
-   */
-  call(target, options)
-  {
-    logger.debug('call()');
-
-    const session = new RTCSession(this);
-
-    session.connect(target, options);
-
-    return session;
-  }
-
-  /**
-   * Send a message.
-   *
-   * -param {String} target
-   * -param {String} body
-   * -param {Object} [options]
-   *
-   * -throws {TypeError}
-   *
-   */
-  sendMessage(target, body, options)
-  {
-    logger.debug('sendMessage()');
-
-    const message = new Message(this);
-
-    message.send(target, body, options);
-
-    return message;
-  }
-
-  /**
-   * Create subscriber instance
-   */
-  subscribe(target, eventName, accept, options)
-  {
-    logger.debug('subscribe()');
-
-    return new Subscriber(this, target, eventName, accept, options);
-  }
-
-  /**
-   * Create notifier instance
-   */
-  notify(subscribe, contentType, options)
-  {
-    logger.debug('notify()');
-
-    return new Notifier(this, subscribe, contentType, options);
-  }
-
-  /**
-   * Send a SIP OPTIONS.
-   *
-   * -param {String} target
-   * -param {String} [body]
-   * -param {Object} [options]
-   *
-   * -throws {TypeError}
-   *
-   */
-  sendOptions(target, body, options)
-  {
-    logger.debug('sendOptions()');
-
-    const message = new Options(this);
-
-    message.send(target, body, options);
-
-    return message;
-  }
-
-  /**
-   * Terminate ongoing sessions.
-   */
-  terminateSessions(options)
-  {
-    logger.debug('terminateSessions()');
-
-    for (const idx in this._sessions)
-    {
-      if (!this._sessions[idx].isEnded())
-      {
-        this._sessions[idx].terminate(options);
-      }
-    }
-  }
-
-  /**
-   * Gracefully close.
-   *
-   */
-  stop()
-  {
-    logger.debug('stop()');
-
-    // Remove dynamic settings.
-    this._dynConfiguration = {};
-
-    if (this._status === C.STATUS_USER_CLOSED)
-    {
-      logger.debug('UA already closed');
-
-      return;
-    }
-
-    // Close registrator.
-    this._registrator.close();
-
-    // If there are session wait a bit so CANCEL/BYE can be sent and their responses received.
-    const num_sessions = Object.keys(this._sessions).length;
-
-    // Run  _terminate_ on every Session.
-    for (const session in this._sessions)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._sessions, session))
-      {
-        logger.debug(`closing session ${session}`);
-        try { this._sessions[session].terminate(); }
-        // eslint-disable-next-line no-unused-vars
-        catch (error) {}
-      }
-    }
-
-    // Run  _close_ on every applicant.
-    for (const applicant in this._applicants)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._applicants, applicant))
-        try { this._applicants[applicant].close(); }
-        // eslint-disable-next-line no-unused-vars
-        catch (error) {}
-    }
-
-    this._status = C.STATUS_USER_CLOSED;
-
-    const num_transactions =
-      Object.keys(this._transactions.nict).length +
-      Object.keys(this._transactions.nist).length +
-      Object.keys(this._transactions.ict).length +
-      Object.keys(this._transactions.ist).length;
-
-    if (num_transactions === 0 && num_sessions === 0)
-    {
-      this._transport.disconnect();
-    }
-    else
-    {
-      this._closeTimer = setTimeout(() =>
-      {
-        this._closeTimer = null;
-        this._transport.disconnect();
-      }, 2000);
-    }
-  }
-
-  /**
-   * Normalice a string into a valid SIP request URI
-   * -param {String} target
-   * -returns {JsSIP.URI|undefined}
-   */
-  normalizeTarget(target)
-  {
-    return Utils.normalizeTarget(target, this._configuration.hostport_params);
-  }
-
-  /**
-   * Allow retrieving configuration and autogenerated fields in runtime.
-   */
-  get(parameter)
-  {
-    switch (parameter)
-    {
-      case 'authorization_user':
-        return this._configuration.authorization_user;
-
-      case 'realm':
-        return this._configuration.realm;
-
-      case 'ha1':
-        return this._configuration.ha1;
-
-      case 'authorization_jwt':
-        return this._configuration.authorization_jwt;
-
-      default:
-        logger.warn('get() | cannot get "%s" parameter in runtime', parameter);
-
-        return undefined;
-    }
-  }
-
-  /**
-   * Allow configuration changes in runtime.
-   * Returns true if the parameter could be set.
-   */
-  set(parameter, value)
-  {
-    switch (parameter)
-    {
-      case 'authorization_user': {
-        this._configuration.authorization_user = String(value);
-        break;
-      }
-
-      case 'password': {
-        this._configuration.password = String(value);
-        break;
-      }
-
-      case 'realm': {
-        this._configuration.realm = String(value);
-        break;
-      }
-
-      case 'ha1': {
-        this._configuration.ha1 = String(value);
-        // Delete the plain SIP password.
-        this._configuration.password = null;
-        break;
-      }
-
-      case 'authorization_jwt': {
-        this._configuration.authorization_jwt = String(value);
-        break;
-      }
-
-      case 'display_name': {
-        this._configuration.display_name = value;
-        break;
-      }
-
-      case 'extra_headers': {
-        this._configuration.extra_headers = value;
-        break;
-      }
-
-      default:
-        logger.warn('set() | cannot set "%s" parameter in runtime', parameter);
-
-        return false;
-    }
-
-    return true;
-  }
-
-  // ==========================
-  // Event Handlers.
-  // ==========================
-
-  /**
-   * new Transaction
-   */
-  newTransaction(transaction)
-  {
-    this._transactions[transaction.type][transaction.id] = transaction;
-    this.emit('newTransaction', {
-      transaction
-    });
-  }
-
-  /**
-   * Transaction destroyed.
-   */
-  destroyTransaction(transaction)
-  {
-    delete this._transactions[transaction.type][transaction.id];
-    this.emit('transactionDestroyed', {
-      transaction
-    });
-  }
-
-  /**
-   * new Dialog
-   */
-  newDialog(dialog)
-  {
-    this._dialogs[dialog.id] = dialog;
-  }
-
-  /**
-   * Dialog destroyed.
-   */
-  destroyDialog(dialog)
-  {
-    delete this._dialogs[dialog.id];
-  }
-
-  /**
-   *  new Message
-   */
-  newMessage(message, data)
-  {
-    this._applicants[message] = message;
-    this.emit('newMessage', data);
-  }
-
-  /**
-   *  new Options
-   */
-  newOptions(message, data)
-  {
-    this._applicants[message] = message;
-    this.emit('newOptions', data);
-  }
-
-  /**
-   *  Message destroyed.
-   */
-  destroyMessage(message)
-  {
-    delete this._applicants[message];
-  }
-
-  /**
-   * new RTCSession
-   */
-  newRTCSession(session, data)
-  {
-    this._sessions[session.id] = session;
-    this.emit('newRTCSession', data);
-  }
-
-  /**
-   * RTCSession destroyed.
-   */
-  destroyRTCSession(session)
-  {
-    delete this._sessions[session.id];
-  }
-
-  /**
-   * Registered
-   */
-  registered(data)
-  {
-    this.emit('registered', data);
-  }
-
-  /**
-   * Unregistered
-   */
-  unregistered(data)
-  {
-    this.emit('unregistered', data);
-  }
-
-  /**
-   * Registration Failed
-   */
-  registrationFailed(data)
-  {
-    this.emit('registrationFailed', data);
-  }
-
-  // =========================
-  // ReceiveRequest.
-  // =========================
-
-  /**
-   * Request reception
-   */
-  receiveRequest(request)
-  {
-    const method = request.method;
-
-    // Check that request URI points to us.
-    if (request.ruri.user !== this._configuration.uri.user &&
-        request.ruri.user !== this._contact.uri.user)
-    {
-      logger.debug('Request-URI does not point to us');
-      if (request.method !== JsSIP_C.ACK)
-      {
-        request.reply_sl(404);
-      }
-
-      return;
-    }
-
-    // Check request URI scheme.
-    if (request.ruri.scheme === JsSIP_C.SIPS)
-    {
-      request.reply_sl(416);
-
-      return;
-    }
-
-    // Check transaction.
-    if (Transactions.checkTransaction(this, request))
-    {
-      return;
-    }
-
-    // Create the server transaction.
-    if (method === JsSIP_C.INVITE)
-    {
-      /* eslint-disable no-new */
-      new Transactions.InviteServerTransaction(this, this._transport, request);
-      /* eslint-enable no-new */
-    }
-    else if (method !== JsSIP_C.ACK && method !== JsSIP_C.CANCEL)
-    {
-      /* eslint-disable no-new */
-      new Transactions.NonInviteServerTransaction(this, this._transport, request);
-      /* eslint-enable no-new */
-    }
-
-    /* RFC3261 12.2.2
-     * Requests that do not change in any way the state of a dialog may be
-     * received within a dialog (for example, an OPTIONS request).
-     * They are processed as if they had been received outside the dialog.
-     */
-    if (method === JsSIP_C.OPTIONS)
-    {
-      if (this.listeners('newOptions').length === 0)
-      {
-        request.reply(200);
-
-        return;
-      }
-
-      const message = new Options(this);
-
-      message.init_incoming(request);
-    }
-    else if (method === JsSIP_C.MESSAGE)
-    {
-      if (this.listeners('newMessage').length === 0)
-      {
-        request.reply(405);
-
-        return;
-      }
-      const message = new Message(this);
-
-      message.init_incoming(request);
-    }
-    else if (method === JsSIP_C.SUBSCRIBE)
-    {
-      if (this.listeners('newSubscribe').length === 0)
-      {
-        request.reply(405);
-
-        return;
-      }
-    }
-    else if (method === JsSIP_C.INVITE)
-    {
-      // Initial INVITE.
-      if (!request.to_tag && this.listeners('newRTCSession').length === 0)
-      {
-        request.reply(405);
-
-        return;
-      }
-    }
-
-    let dialog;
-    let session;
-
-    // Initial Request.
-    if (!request.to_tag)
-    {
-      switch (method)
-      {
-        case JsSIP_C.INVITE:
-          if (window.RTCPeerConnection)
-          { // TODO
-            if (request.hasHeader('replaces'))
-            {
-              const replaces = request.replaces;
-
-              dialog = this._findDialog(
-                replaces.call_id, replaces.from_tag, replaces.to_tag);
-              if (dialog)
-              {
-                session = dialog.owner;
-                if (!session.isEnded())
-                {
-                  session.receiveRequest(request);
-                }
-                else
-                {
-                  request.reply(603);
-                }
-              }
-              else
-              {
-                request.reply(481);
-              }
-            }
-            else
-            {
-              session = new RTCSession(this);
-              session.init_incoming(request);
-            }
-          }
-          else
-          {
-            logger.warn('INVITE received but WebRTC is not supported');
-            request.reply(488);
-          }
-          break;
-        case JsSIP_C.BYE:
-          // Out of dialog BYE received.
-          request.reply(481);
-          break;
-        case JsSIP_C.CANCEL:
-          session = this._findSession(request);
-          if (session)
-          {
-            session.receiveRequest(request);
-          }
-          else
-          {
-            logger.debug('received CANCEL request for a non existent session');
-          }
-          break;
-        case JsSIP_C.ACK:
-          /* Absorb it.
-           * ACK request without a corresponding Invite Transaction
-           * and without To tag.
-           */
-          break;
-        case JsSIP_C.NOTIFY:
-          // Receive new sip event.
-          this.emit('sipEvent', {
-            event : request.event,
-            request
-          });
-          request.reply(200);
-          break;
-        case JsSIP_C.SUBSCRIBE:
-          Notifier.init_incoming(request, () =>
-          {
-            this.emit('newSubscribe', {
-              event : request.event,
-              request
-            });
-          });
-          break;
-        default:
-          request.reply(405);
-          break;
-      }
-    }
-    // In-dialog request.
-    else
-    {
-      dialog = this._findDialog(request.call_id, request.from_tag, request.to_tag);
-
-      if (dialog)
-      {
-        dialog.receiveRequest(request);
-      }
-      else if (method === JsSIP_C.NOTIFY)
-      {
-        session = this._findSession(request);
-        if (session)
-        {
-          session.receiveRequest(request);
-        }
-        else
-        {
-          logger.debug('received NOTIFY request for a non existent subscription');
-          request.reply(481, 'Subscription does not exist');
-        }
-      }
-
-      /* RFC3261 12.2.2
-       * Request with to tag, but no matching dialog found.
-       * Exception: ACK for an Invite request for which a dialog has not
-       * been created.
-       */
-      else if (method !== JsSIP_C.ACK)
-      {
-        request.reply(481);
-      }
-    }
-  }
-
-  // =================
-  // Utils.
-  // =================
-
-  /**
-   * Get the session to which the request belongs to, if any.
-   */
-  _findSession({ call_id, from_tag, to_tag })
-  {
-    const sessionIDa = call_id + from_tag;
-    const sessionA = this._sessions[sessionIDa];
-    const sessionIDb = call_id + to_tag;
-    const sessionB = this._sessions[sessionIDb];
-
-    if (sessionA)
-    {
-      return sessionA;
-    }
-    else if (sessionB)
-    {
-      return sessionB;
-    }
-    else
-    {
-      return null;
-    }
-  }
-
-  /**
-   * Get the dialog to which the request belongs to, if any.
-   */
-  _findDialog(call_id, from_tag, to_tag)
-  {
-    let id = call_id + from_tag + to_tag;
-    let dialog = this._dialogs[id];
-
-    if (dialog)
-    {
-      return dialog;
-    }
-    else
-    {
-      id = call_id + to_tag + from_tag;
-      dialog = this._dialogs[id];
-      if (dialog)
-      {
-        return dialog;
-      }
-      else
-      {
-        return null;
-      }
-    }
-  }
-
-  _loadConfig(configuration)
-  {
-    // Check and load the given configuration.
-    // This can throw.
-    config.load(this._configuration, configuration);
-
-    // Post Configuration Process.
-
-    // Allow passing 0 number as display_name.
-    if (this._configuration.display_name === 0)
-    {
-      this._configuration.display_name = '0';
-    }
-
-    // Instance-id for GRUU.
-    if (!this._configuration.instance_id)
-    {
-      this._configuration.instance_id = Utils.newUUID();
-    }
-
-    // Jssip_id instance parameter. Static random tag of length 5.
-    this._configuration.jssip_id = Utils.createRandomToken(5);
-
-    // String containing this._configuration.uri without scheme and user.
-    const hostport_params = this._configuration.uri.clone();
-
-    hostport_params.user = null;
-    this._configuration.hostport_params = hostport_params.toString().replace(/^sip:/i, '');
-
-    // Transport.
-    try
-    {
-      this._transport = new Transport(this._configuration.sockets, {
-        // Recovery options.
-        max_interval : this._configuration.connection_recovery_max_interval,
-        min_interval : this._configuration.connection_recovery_min_interval
-      });
-
-      // Transport event callbacks.
-      this._transport.onconnecting = onTransportConnecting.bind(this);
-      this._transport.onconnect = onTransportConnect.bind(this);
-      this._transport.ondisconnect = onTransportDisconnect.bind(this);
-      this._transport.ondata = onTransportData.bind(this);
-    }
-    catch (error)
-    {
-      logger.warn(error);
-      throw new Exceptions.ConfigurationError('sockets', this._configuration.sockets);
-    }
-
-    // Remove sockets instance from configuration object.
-    delete this._configuration.sockets;
-
-    // Check whether authorization_user is explicitly defined.
-    // Take 'this._configuration.uri.user' value if not.
-    if (!this._configuration.authorization_user)
-    {
-      this._configuration.authorization_user = this._configuration.uri.user;
-    }
-
-    // If no 'registrar_server' is set use the 'uri' value without user portion and
-    // without URI params/headers.
-    if (!this._configuration.registrar_server)
-    {
-      const registrar_server = this._configuration.uri.clone();
-
-      registrar_server.user = null;
-      registrar_server.clearParams();
-      registrar_server.clearHeaders();
-      this._configuration.registrar_server = registrar_server;
-    }
-
-    // User no_answer_timeout.
-    this._configuration.no_answer_timeout *= 1000;
-
-    // Via Host.
-    if (this._configuration.contact_uri)
-    {
-      this._configuration.via_host = this._configuration.contact_uri.host;
-    }
-
-    // Contact URI.
-    else
-    {
-      this._configuration.contact_uri = new URI('sip', Utils.createRandomToken(8), this._configuration.via_host, null, { transport: 'ws' });
-    }
-
-    this._contact = {
-      pub_gruu  : null,
-      temp_gruu : null,
-      uri       : this._configuration.contact_uri,
-      toString(options = {})
-      {
-        const anonymous = options.anonymous || null;
-        const outbound = options.outbound || null;
-        let contact = '<';
-
-        if (anonymous)
-        {
-          contact += this.temp_gruu || 'sip:anonymous@anonymous.invalid;transport=ws';
-        }
-        else
-        {
-          contact += this.pub_gruu || this.uri.toString();
-        }
-
-        if (outbound && (anonymous ? !this.temp_gruu : !this.pub_gruu))
-        {
-          contact += ';ob';
-        }
-
-        contact += '>';
-
-        return contact;
-      }
-    };
-
-    // Seal the configuration.
-    const writable_parameters = [
-      'authorization_user', 'password', 'realm', 'ha1', 'authorization_jwt', 'display_name', 'register', 'extra_headers'
-    ];
-
-    for (const parameter in this._configuration)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._configuration, parameter))
-      {
-        if (writable_parameters.indexOf(parameter) !== -1)
-        {
-          Object.defineProperty(this._configuration, parameter, {
-            writable     : true,
-            configurable : false
-          });
-        }
-        else
-        {
-          Object.defineProperty(this._configuration, parameter, {
-            writable     : false,
-            configurable : false
-          });
-        }
-      }
-    }
-
-    logger.debug('configuration parameters after validation:');
-    for (const parameter in this._configuration)
-    {
-      // Only show the user user configurable parameters.
-      if (Object.prototype.hasOwnProperty.call(config.settings, parameter))
-      {
-        switch (parameter)
-        {
-          case 'uri':
-          case 'registrar_server':
-            logger.debug(`- ${parameter}: ${this._configuration[parameter]}`);
-            break;
-          case 'password':
-          case 'ha1':
-          case 'authorization_jwt':
-            logger.debug(`- ${parameter}: NOT SHOWN`);
-            break;
-          default:
-            logger.debug(`- ${parameter}: ${JSON.stringify(this._configuration[parameter])}`);
-        }
-      }
-    }
-
-    return;
-  }
+module.exports = class UA extends EventEmitter {
+	// Expose C object.
+	static get C() {
+		return C;
+	}
+
+	constructor(configuration) {
+		// Check configuration argument.
+		if (!configuration) {
+			throw new TypeError('Not enough arguments');
+		}
+
+		// Hide sensitive information.
+		const sensitiveKeys = ['password', 'ha1', 'authorization_jwt'];
+
+		logger.debug(
+			'new() [configuration:%o]',
+			Object.entries(configuration).filter(
+				([key]) => !sensitiveKeys.includes(key)
+			)
+		);
+
+		super();
+
+		this._cache = {
+			credentials: {},
+		};
+
+		this._configuration = Object.assign({}, config.settings);
+		this._dynConfiguration = {};
+		this._dialogs = {};
+
+		// User actions outside any session/dialog (MESSAGE/OPTIONS).
+		this._applicants = {};
+
+		this._sessions = {};
+		this._transport = null;
+		this._contact = null;
+		this._status = C.STATUS_INIT;
+		this._error = null;
+		this._transactions = {
+			nist: {},
+			nict: {},
+			ist: {},
+			ict: {},
+		};
+
+		// Custom UA empty object for high level use.
+		this._data = {};
+
+		this._closeTimer = null;
+
+		// Load configuration.
+		try {
+			this._loadConfig(configuration);
+		} catch (error) {
+			this._status = C.STATUS_NOT_READY;
+			this._error = C.CONFIGURATION_ERROR;
+			throw error;
+		}
+
+		// Initialize registrator.
+		this._registrator = new Registrator(this);
+	}
+
+	get C() {
+		return C;
+	}
+
+	get status() {
+		return this._status;
+	}
+
+	get contact() {
+		return this._contact;
+	}
+
+	get configuration() {
+		return this._configuration;
+	}
+
+	get transport() {
+		return this._transport;
+	}
+
+	// =================
+	//  High Level API
+	// =================
+
+	/**
+	 * Connect to the server if status = STATUS_INIT.
+	 * Resume UA after being closed.
+	 */
+	start() {
+		logger.debug('start()');
+
+		if (this._status === C.STATUS_INIT) {
+			this._transport.connect();
+		} else if (this._status === C.STATUS_USER_CLOSED) {
+			logger.debug('restarting UA');
+
+			// Disconnect.
+			if (this._closeTimer !== null) {
+				clearTimeout(this._closeTimer);
+				this._closeTimer = null;
+				this._transport.disconnect();
+			}
+
+			// Reconnect.
+			this._status = C.STATUS_INIT;
+			this._transport.connect();
+		} else if (this._status === C.STATUS_READY) {
+			logger.debug('UA is in READY status, not restarted');
+		} else {
+			logger.debug(
+				'ERROR: connection is down, Auto-Recovery system is trying to reconnect'
+			);
+		}
+
+		// Set dynamic configuration.
+		this._dynConfiguration.register = this._configuration.register;
+	}
+
+	/**
+	 * Register.
+	 */
+	register() {
+		logger.debug('register()');
+
+		this._dynConfiguration.register = true;
+		this._registrator.register();
+	}
+
+	/**
+	 * Unregister.
+	 */
+	unregister(options) {
+		logger.debug('unregister()');
+
+		this._dynConfiguration.register = false;
+		this._registrator.unregister(options);
+	}
+
+	/**
+	 * Get the Registrator instance.
+	 */
+	registrator() {
+		return this._registrator;
+	}
+
+	/**
+	 * Registration state.
+	 */
+	isRegistered() {
+		return this._registrator.registered;
+	}
+
+	/**
+	 * Connection state.
+	 */
+	isConnected() {
+		return this._transport.isConnected();
+	}
+
+	/**
+	 * Make an outgoing call.
+	 *
+	 * -param {String} target
+	 * -param {Object} [options]
+	 *
+	 * -throws {TypeError}
+	 *
+	 */
+	call(target, options) {
+		logger.debug('call()');
+
+		const session = new RTCSession(this);
+
+		session.connect(target, options);
+
+		return session;
+	}
+
+	/**
+	 * Send a message.
+	 *
+	 * -param {String} target
+	 * -param {String} body
+	 * -param {Object} [options]
+	 *
+	 * -throws {TypeError}
+	 *
+	 */
+	sendMessage(target, body, options) {
+		logger.debug('sendMessage()');
+
+		const message = new Message(this);
+
+		message.send(target, body, options);
+
+		return message;
+	}
+
+	/**
+	 * Create subscriber instance
+	 */
+	subscribe(target, eventName, accept, options) {
+		logger.debug('subscribe()');
+
+		return new Subscriber(this, target, eventName, accept, options);
+	}
+
+	/**
+	 * Create notifier instance
+	 */
+	notify(subscribe, contentType, options) {
+		logger.debug('notify()');
+
+		return new Notifier(this, subscribe, contentType, options);
+	}
+
+	/**
+	 * Send a SIP OPTIONS.
+	 *
+	 * -param {String} target
+	 * -param {String} [body]
+	 * -param {Object} [options]
+	 *
+	 * -throws {TypeError}
+	 *
+	 */
+	sendOptions(target, body, options) {
+		logger.debug('sendOptions()');
+
+		const message = new Options(this);
+
+		message.send(target, body, options);
+
+		return message;
+	}
+
+	/**
+	 * Terminate ongoing sessions.
+	 */
+	terminateSessions(options) {
+		logger.debug('terminateSessions()');
+
+		for (const idx in this._sessions) {
+			if (!this._sessions[idx].isEnded()) {
+				this._sessions[idx].terminate(options);
+			}
+		}
+	}
+
+	/**
+	 * Gracefully close.
+	 *
+	 */
+	stop() {
+		logger.debug('stop()');
+
+		// Remove dynamic settings.
+		this._dynConfiguration = {};
+
+		if (this._status === C.STATUS_USER_CLOSED) {
+			logger.debug('UA already closed');
+
+			return;
+		}
+
+		// Close registrator.
+		this._registrator.close();
+
+		// If there are session wait a bit so CANCEL/BYE can be sent and their responses received.
+		const num_sessions = Object.keys(this._sessions).length;
+
+		// Run  _terminate_ on every Session.
+		for (const session in this._sessions) {
+			if (Object.prototype.hasOwnProperty.call(this._sessions, session)) {
+				logger.debug(`closing session ${session}`);
+				try {
+					this._sessions[session].terminate();
+				} catch (error) {}
+			}
+		}
+
+		// Run  _close_ on every applicant.
+		for (const applicant in this._applicants) {
+			if (Object.prototype.hasOwnProperty.call(this._applicants, applicant)) {
+				try {
+					this._applicants[applicant].close();
+				} catch (error) {}
+			}
+		}
+
+		this._status = C.STATUS_USER_CLOSED;
+
+		const num_transactions =
+			Object.keys(this._transactions.nict).length +
+			Object.keys(this._transactions.nist).length +
+			Object.keys(this._transactions.ict).length +
+			Object.keys(this._transactions.ist).length;
+
+		if (num_transactions === 0 && num_sessions === 0) {
+			this._transport.disconnect();
+		} else {
+			this._closeTimer = setTimeout(() => {
+				this._closeTimer = null;
+				this._transport.disconnect();
+			}, 2000);
+		}
+	}
+
+	/**
+	 * Normalice a string into a valid SIP request URI
+	 * -param {String} target
+	 * -returns {JsSIP.URI|undefined}
+	 */
+	normalizeTarget(target) {
+		return Utils.normalizeTarget(target, this._configuration.hostport_params);
+	}
+
+	/**
+	 * Allow retrieving configuration and autogenerated fields in runtime.
+	 */
+	get(parameter) {
+		switch (parameter) {
+			case 'authorization_user': {
+				return this._configuration.authorization_user;
+			}
+
+			case 'realm': {
+				return this._configuration.realm;
+			}
+
+			case 'ha1': {
+				return this._configuration.ha1;
+			}
+
+			case 'authorization_jwt': {
+				return this._configuration.authorization_jwt;
+			}
+
+			default: {
+				logger.warn('get() | cannot get "%s" parameter in runtime', parameter);
+
+				return undefined;
+			}
+		}
+	}
+
+	/**
+	 * Allow configuration changes in runtime.
+	 * Returns true if the parameter could be set.
+	 */
+	set(parameter, value) {
+		switch (parameter) {
+			case 'authorization_user': {
+				this._configuration.authorization_user = String(value);
+				break;
+			}
+
+			case 'password': {
+				this._configuration.password = String(value);
+				break;
+			}
+
+			case 'realm': {
+				this._configuration.realm = String(value);
+				break;
+			}
+
+			case 'ha1': {
+				this._configuration.ha1 = String(value);
+				// Delete the plain SIP password.
+				this._configuration.password = null;
+				break;
+			}
+
+			case 'authorization_jwt': {
+				this._configuration.authorization_jwt = String(value);
+				break;
+			}
+
+			case 'display_name': {
+				this._configuration.display_name = value;
+				break;
+			}
+
+			case 'extra_headers': {
+				this._configuration.extra_headers = value;
+				break;
+			}
+
+			default: {
+				logger.warn('set() | cannot set "%s" parameter in runtime', parameter);
+
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	// ==========================
+	// Event Handlers.
+	// ==========================
+
+	/**
+	 * new Transaction
+	 */
+	newTransaction(transaction) {
+		this._transactions[transaction.type][transaction.id] = transaction;
+		this.emit('newTransaction', {
+			transaction,
+		});
+	}
+
+	/**
+	 * Transaction destroyed.
+	 */
+	destroyTransaction(transaction) {
+		delete this._transactions[transaction.type][transaction.id];
+		this.emit('transactionDestroyed', {
+			transaction,
+		});
+	}
+
+	/**
+	 * new Dialog
+	 */
+	newDialog(dialog) {
+		this._dialogs[dialog.id] = dialog;
+	}
+
+	/**
+	 * Dialog destroyed.
+	 */
+	destroyDialog(dialog) {
+		delete this._dialogs[dialog.id];
+	}
+
+	/**
+	 *  new Message
+	 */
+	newMessage(message, data) {
+		this._applicants[message] = message;
+		this.emit('newMessage', data);
+	}
+
+	/**
+	 *  new Options
+	 */
+	newOptions(message, data) {
+		this._applicants[message] = message;
+		this.emit('newOptions', data);
+	}
+
+	/**
+	 *  Message destroyed.
+	 */
+	destroyMessage(message) {
+		delete this._applicants[message];
+	}
+
+	/**
+	 * new RTCSession
+	 */
+	newRTCSession(session, data) {
+		this._sessions[session.id] = session;
+		this.emit('newRTCSession', data);
+	}
+
+	/**
+	 * RTCSession destroyed.
+	 */
+	destroyRTCSession(session) {
+		delete this._sessions[session.id];
+	}
+
+	/**
+	 * Registered
+	 */
+	registered(data) {
+		this.emit('registered', data);
+	}
+
+	/**
+	 * Unregistered
+	 */
+	unregistered(data) {
+		this.emit('unregistered', data);
+	}
+
+	/**
+	 * Registration Failed
+	 */
+	registrationFailed(data) {
+		this.emit('registrationFailed', data);
+	}
+
+	// =========================
+	// ReceiveRequest.
+	// =========================
+
+	/**
+	 * Request reception
+	 */
+	receiveRequest(request) {
+		const method = request.method;
+
+		// Check that request URI points to us.
+		if (
+			request.ruri.user !== this._configuration.uri.user &&
+			request.ruri.user !== this._contact.uri.user
+		) {
+			logger.debug('Request-URI does not point to us');
+			if (request.method !== JsSIP_C.ACK) {
+				request.reply_sl(404);
+			}
+
+			return;
+		}
+
+		// Check request URI scheme.
+		if (request.ruri.scheme === JsSIP_C.SIPS) {
+			request.reply_sl(416);
+
+			return;
+		}
+
+		// Check transaction.
+		if (Transactions.checkTransaction(this, request)) {
+			return;
+		}
+
+		// Create the server transaction.
+		if (method === JsSIP_C.INVITE) {
+			/* eslint-disable no-new */
+			new Transactions.InviteServerTransaction(this, this._transport, request);
+			/* eslint-enable no-new */
+		} else if (method !== JsSIP_C.ACK && method !== JsSIP_C.CANCEL) {
+			/* eslint-disable no-new */
+			new Transactions.NonInviteServerTransaction(
+				this,
+				this._transport,
+				request
+			);
+			/* eslint-enable no-new */
+		}
+
+		/* RFC3261 12.2.2
+		 * Requests that do not change in any way the state of a dialog may be
+		 * received within a dialog (for example, an OPTIONS request).
+		 * They are processed as if they had been received outside the dialog.
+		 */
+		if (method === JsSIP_C.OPTIONS) {
+			if (this.listeners('newOptions').length === 0) {
+				request.reply(200);
+
+				return;
+			}
+
+			const message = new Options(this);
+
+			message.init_incoming(request);
+		} else if (method === JsSIP_C.MESSAGE) {
+			if (this.listeners('newMessage').length === 0) {
+				request.reply(405);
+
+				return;
+			}
+			const message = new Message(this);
+
+			message.init_incoming(request);
+		} else if (method === JsSIP_C.SUBSCRIBE) {
+			if (this.listeners('newSubscribe').length === 0) {
+				request.reply(405);
+
+				return;
+			}
+		} else if (method === JsSIP_C.INVITE) {
+			// Initial INVITE.
+			if (!request.to_tag && this.listeners('newRTCSession').length === 0) {
+				request.reply(405);
+
+				return;
+			}
+		}
+
+		let dialog;
+		let session;
+
+		// Initial Request.
+		if (!request.to_tag) {
+			switch (method) {
+				case JsSIP_C.INVITE: {
+					// eslint-disable-next-line no-undef
+					if (window.RTCPeerConnection) {
+						// TODO
+						if (request.hasHeader('replaces')) {
+							const replaces = request.replaces;
+
+							dialog = this._findDialog(
+								replaces.call_id,
+								replaces.from_tag,
+								replaces.to_tag
+							);
+							if (dialog) {
+								session = dialog.owner;
+								if (!session.isEnded()) {
+									session.receiveRequest(request);
+								} else {
+									request.reply(603);
+								}
+							} else {
+								request.reply(481);
+							}
+						} else {
+							session = new RTCSession(this);
+							session.init_incoming(request);
+						}
+					} else {
+						logger.warn('INVITE received but WebRTC is not supported');
+						request.reply(488);
+					}
+					break;
+				}
+				case JsSIP_C.BYE: {
+					// Out of dialog BYE received.
+					request.reply(481);
+					break;
+				}
+				case JsSIP_C.CANCEL: {
+					session = this._findSession(request);
+					if (session) {
+						session.receiveRequest(request);
+					} else {
+						logger.debug('received CANCEL request for a non existent session');
+					}
+					break;
+				}
+				case JsSIP_C.ACK: {
+					/* Absorb it.
+					 * ACK request without a corresponding Invite Transaction
+					 * and without To tag.
+					 */
+					break;
+				}
+				case JsSIP_C.NOTIFY: {
+					// Receive new sip event.
+					this.emit('sipEvent', {
+						event: request.event,
+						request,
+					});
+					request.reply(200);
+					break;
+				}
+				case JsSIP_C.SUBSCRIBE: {
+					Notifier.init_incoming(request, () => {
+						this.emit('newSubscribe', {
+							event: request.event,
+							request,
+						});
+					});
+					break;
+				}
+				default: {
+					request.reply(405);
+					break;
+				}
+			}
+		}
+		// In-dialog request.
+		else {
+			dialog = this._findDialog(
+				request.call_id,
+				request.from_tag,
+				request.to_tag
+			);
+
+			if (dialog) {
+				dialog.receiveRequest(request);
+			} else if (method === JsSIP_C.NOTIFY) {
+				session = this._findSession(request);
+				if (session) {
+					session.receiveRequest(request);
+				} else {
+					logger.debug(
+						'received NOTIFY request for a non existent subscription'
+					);
+					request.reply(481, 'Subscription does not exist');
+				}
+			} else if (method !== JsSIP_C.ACK) {
+				/* RFC3261 12.2.2
+				 * Request with to tag, but no matching dialog found.
+				 * Exception: ACK for an Invite request for which a dialog has not
+				 * been created.
+				 */
+				request.reply(481);
+			}
+		}
+	}
+
+	// =================
+	// Utils.
+	// =================
+
+	/**
+	 * Get the session to which the request belongs to, if any.
+	 */
+	_findSession({ call_id, from_tag, to_tag }) {
+		const sessionIDa = call_id + from_tag;
+		const sessionA = this._sessions[sessionIDa];
+		const sessionIDb = call_id + to_tag;
+		const sessionB = this._sessions[sessionIDb];
+
+		if (sessionA) {
+			return sessionA;
+		} else if (sessionB) {
+			return sessionB;
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Get the dialog to which the request belongs to, if any.
+	 */
+	_findDialog(call_id, from_tag, to_tag) {
+		let id = call_id + from_tag + to_tag;
+		let dialog = this._dialogs[id];
+
+		if (dialog) {
+			return dialog;
+		} else {
+			id = call_id + to_tag + from_tag;
+			dialog = this._dialogs[id];
+			if (dialog) {
+				return dialog;
+			} else {
+				return null;
+			}
+		}
+	}
+
+	_loadConfig(configuration) {
+		// Check and load the given configuration.
+		// This can throw.
+		config.load(this._configuration, configuration);
+
+		// Post Configuration Process.
+
+		// Allow passing 0 number as display_name.
+		if (this._configuration.display_name === 0) {
+			this._configuration.display_name = '0';
+		}
+
+		// Instance-id for GRUU.
+		if (!this._configuration.instance_id) {
+			this._configuration.instance_id = Utils.newUUID();
+		}
+
+		// Jssip_id instance parameter. Static random tag of length 5.
+		this._configuration.jssip_id = Utils.createRandomToken(5);
+
+		// String containing this._configuration.uri without scheme and user.
+		const hostport_params = this._configuration.uri.clone();
+
+		hostport_params.user = null;
+		this._configuration.hostport_params = hostport_params
+			.toString()
+			.replace(/^sip:/i, '');
+
+		// Transport.
+		try {
+			this._transport = new Transport(this._configuration.sockets, {
+				// Recovery options.
+				max_interval: this._configuration.connection_recovery_max_interval,
+				min_interval: this._configuration.connection_recovery_min_interval,
+			});
+
+			// Transport event callbacks.
+			this._transport.onconnecting = onTransportConnecting.bind(this);
+			this._transport.onconnect = onTransportConnect.bind(this);
+			this._transport.ondisconnect = onTransportDisconnect.bind(this);
+			this._transport.ondata = onTransportData.bind(this);
+		} catch (error) {
+			logger.warn(error);
+			throw new Exceptions.ConfigurationError(
+				'sockets',
+				this._configuration.sockets
+			);
+		}
+
+		// Remove sockets instance from configuration object.
+		delete this._configuration.sockets;
+
+		// Check whether authorization_user is explicitly defined.
+		// Take 'this._configuration.uri.user' value if not.
+		if (!this._configuration.authorization_user) {
+			this._configuration.authorization_user = this._configuration.uri.user;
+		}
+
+		// If no 'registrar_server' is set use the 'uri' value without user portion and
+		// without URI params/headers.
+		if (!this._configuration.registrar_server) {
+			const registrar_server = this._configuration.uri.clone();
+
+			registrar_server.user = null;
+			registrar_server.clearParams();
+			registrar_server.clearHeaders();
+			this._configuration.registrar_server = registrar_server;
+		}
+
+		// User no_answer_timeout.
+		this._configuration.no_answer_timeout *= 1000;
+
+		// Via Host.
+		if (this._configuration.contact_uri) {
+			this._configuration.via_host = this._configuration.contact_uri.host;
+		}
+
+		// Contact URI.
+		else {
+			this._configuration.contact_uri = new URI(
+				'sip',
+				Utils.createRandomToken(8),
+				this._configuration.via_host,
+				null,
+				{ transport: 'ws' }
+			);
+		}
+
+		this._contact = {
+			pub_gruu: null,
+			temp_gruu: null,
+			uri: this._configuration.contact_uri,
+			toString(options = {}) {
+				const anonymous = options.anonymous || null;
+				const outbound = options.outbound || null;
+				let contact = '<';
+
+				if (anonymous) {
+					contact +=
+						this.temp_gruu || 'sip:anonymous@anonymous.invalid;transport=ws';
+				} else {
+					contact += this.pub_gruu || this.uri.toString();
+				}
+
+				if (outbound && (anonymous ? !this.temp_gruu : !this.pub_gruu)) {
+					contact += ';ob';
+				}
+
+				contact += '>';
+
+				return contact;
+			},
+		};
+
+		// Seal the configuration.
+		const writable_parameters = [
+			'authorization_user',
+			'password',
+			'realm',
+			'ha1',
+			'authorization_jwt',
+			'display_name',
+			'register',
+			'extra_headers',
+		];
+
+		for (const parameter in this._configuration) {
+			if (
+				Object.prototype.hasOwnProperty.call(this._configuration, parameter)
+			) {
+				if (writable_parameters.indexOf(parameter) !== -1) {
+					Object.defineProperty(this._configuration, parameter, {
+						writable: true,
+						configurable: false,
+					});
+				} else {
+					Object.defineProperty(this._configuration, parameter, {
+						writable: false,
+						configurable: false,
+					});
+				}
+			}
+		}
+
+		logger.debug('configuration parameters after validation:');
+		for (const parameter in this._configuration) {
+			// Only show the user user configurable parameters.
+			if (Object.prototype.hasOwnProperty.call(config.settings, parameter)) {
+				switch (parameter) {
+					case 'uri':
+					case 'registrar_server': {
+						logger.debug(`- ${parameter}: ${this._configuration[parameter]}`);
+						break;
+					}
+					case 'password':
+					case 'ha1':
+					case 'authorization_jwt': {
+						logger.debug(`- ${parameter}: NOT SHOWN`);
+						break;
+					}
+					default: {
+						logger.debug(
+							`- ${parameter}: ${JSON.stringify(this._configuration[parameter])}`
+						);
+					}
+				}
+			}
+		}
+
+		return;
+	}
 };

 /**
@@ -1043,117 +964,103 @@ module.exports = class UA extends EventEmitter
  */

 // Transport connecting event.
-function onTransportConnecting(data)
-{
-  this.emit('connecting', data);
+function onTransportConnecting(data) {
+	this.emit('connecting', data);
 }

 // Transport connected event.
-function onTransportConnect(data)
-{
-  if (this._status === C.STATUS_USER_CLOSED)
-  {
-    return;
-  }
-
-  this._status = C.STATUS_READY;
-  this._error = null;
-
-  this.emit('connected', data);
-
-  if (this._dynConfiguration.register)
-  {
-    this._registrator.register();
-  }
+function onTransportConnect(data) {
+	if (this._status === C.STATUS_USER_CLOSED) {
+		return;
+	}
+
+	this._status = C.STATUS_READY;
+	this._error = null;
+
+	this.emit('connected', data);
+
+	if (this._dynConfiguration.register) {
+		this._registrator.register();
+	}
 }

 // Transport disconnected event.
-function onTransportDisconnect(data)
-{
-  // Run _onTransportError_ callback on every client transaction using _transport_.
-  const client_transactions = [ 'nict', 'ict', 'nist', 'ist' ];
-
-  for (const type of client_transactions)
-  {
-    for (const id in this._transactions[type])
-    {
-      if (Object.prototype.hasOwnProperty.call(this._transactions[type], id))
-      {
-        this._transactions[type][id].onTransportError();
-      }
-    }
-  }
-
-  this.emit('disconnected', data);
-
-  // Call registrator _onTransportClosed_.
-  this._registrator.onTransportClosed();
-
-  if (this._status !== C.STATUS_USER_CLOSED)
-  {
-    this._status = C.STATUS_NOT_READY;
-    this._error = C.NETWORK_ERROR;
-  }
+function onTransportDisconnect(data) {
+	// Run _onTransportError_ callback on every client transaction using _transport_.
+	const client_transactions = ['nict', 'ict', 'nist', 'ist'];
+
+	for (const type of client_transactions) {
+		for (const id in this._transactions[type]) {
+			if (Object.prototype.hasOwnProperty.call(this._transactions[type], id)) {
+				this._transactions[type][id].onTransportError();
+			}
+		}
+	}
+
+	this.emit('disconnected', data);
+
+	// Call registrator _onTransportClosed_.
+	this._registrator.onTransportClosed();
+
+	if (this._status !== C.STATUS_USER_CLOSED) {
+		this._status = C.STATUS_NOT_READY;
+		this._error = C.NETWORK_ERROR;
+	}
 }

 // Transport data event.
-function onTransportData(data)
-{
-  const transport = data.transport;
-  let message = data.message;
-
-  message = Parser.parseMessage(message, this);
-
-  if (!message)
-  {
-    return;
-  }
-
-  if (this._status === C.STATUS_USER_CLOSED &&
-      message instanceof SIPMessage.IncomingRequest)
-  {
-    return;
-  }
-
-  // Do some sanity check.
-  if (!sanityCheck(message, this, transport))
-  {
-    return;
-  }
-
-  if (message instanceof SIPMessage.IncomingRequest)
-  {
-    message.transport = transport;
-    this.receiveRequest(message);
-  }
-  else if (message instanceof SIPMessage.IncomingResponse)
-  {
-    /* Unike stated in 18.1.2, if a response does not match
-    * any transaction, it is discarded here and no passed to the core
-    * in order to be discarded there.
-    */
-
-    let transaction;
-
-    switch (message.method)
-    {
-      case JsSIP_C.INVITE:
-        transaction = this._transactions.ict[message.via_branch];
-        if (transaction)
-        {
-          transaction.receiveResponse(message);
-        }
-        break;
-      case JsSIP_C.ACK:
-        // Just in case ;-).
-        break;
-      default:
-        transaction = this._transactions.nict[message.via_branch];
-        if (transaction)
-        {
-          transaction.receiveResponse(message);
-        }
-        break;
-    }
-  }
+function onTransportData(data) {
+	const transport = data.transport;
+	let message = data.message;
+
+	message = Parser.parseMessage(message, this);
+
+	if (!message) {
+		return;
+	}
+
+	if (
+		this._status === C.STATUS_USER_CLOSED &&
+		message instanceof SIPMessage.IncomingRequest
+	) {
+		return;
+	}
+
+	// Do some sanity check.
+	if (!sanityCheck(message, this, transport)) {
+		return;
+	}
+
+	if (message instanceof SIPMessage.IncomingRequest) {
+		message.transport = transport;
+		this.receiveRequest(message);
+	} else if (message instanceof SIPMessage.IncomingResponse) {
+		/* Unike stated in 18.1.2, if a response does not match
+		 * any transaction, it is discarded here and no passed to the core
+		 * in order to be discarded there.
+		 */
+
+		let transaction;
+
+		switch (message.method) {
+			case JsSIP_C.INVITE: {
+				transaction = this._transactions.ict[message.via_branch];
+				if (transaction) {
+					transaction.receiveResponse(message);
+				}
+				break;
+			}
+			case JsSIP_C.ACK: {
+				// Just in case ;-).
+				break;
+			}
+			default: {
+				transaction = this._transactions.nict[message.via_branch];
+				if (transaction) {
+					transaction.receiveResponse(message);
+				}
+				break;
+			}
+		}
+	}
 }
diff --git a/src/URI.d.ts b/src/URI.d.ts
index 66d3a09..05fcd0d 100644
--- a/src/URI.d.ts
+++ b/src/URI.d.ts
@@ -1,4 +1,4 @@
-import {Grammar} from './Grammar'
+import { Grammar } from './Grammar';

 export type URIScheme = 'sip' | string;

@@ -7,38 +7,45 @@ export type Parameters = Record<string, string | null>;
 export type Headers = Record<string, string | string[]>;

 export class URI {
-  scheme: URIScheme
-  user: string
-  host: string
-  port: number
+	scheme: URIScheme;
+	user: string;
+	host: string;
+	port: number;

-  constructor(scheme: URIScheme, user: string, host: string, port?: number, parameters?: Parameters, headers?: Headers);
+	constructor(
+		scheme: URIScheme,
+		user: string,
+		host: string,
+		port?: number,
+		parameters?: Parameters,
+		headers?: Headers
+	);

-  setParam(key: string, value?: string): void;
+	setParam(key: string, value?: string): void;

-  getParam<T = unknown>(key: string): T;
+	getParam<T = unknown>(key: string): T;

-  hasParam(key: string): boolean;
+	hasParam(key: string): boolean;

-  deleteParam(key: string): void;
+	deleteParam(key: string): void;

-  clearParams(): void;
+	clearParams(): void;

-  setHeader(key: string, value: string | string[]): void;
+	setHeader(key: string, value: string | string[]): void;

-  getHeader(key: string): string[];
+	getHeader(key: string): string[];

-  hasHeader(key: string): boolean;
+	hasHeader(key: string): boolean;

-  deleteHeader(key: string): void;
+	deleteHeader(key: string): void;

-  clearHeaders(): void;
+	clearHeaders(): void;

-  clone(): this;
+	clone(): this;

-  toString(): string;
+	toString(): string;

-  toAor(): string;
+	toAor(): string;

-  static parse(uri: string): Grammar | undefined;
+	static parse(uri: string): Grammar | undefined;
 }
diff --git a/src/URI.js b/src/URI.js
index b8ac6fc..8e629a9 100644
--- a/src/URI.js
+++ b/src/URI.js
@@ -11,254 +11,214 @@ const Grammar = require('./Grammar');
  * -param {Object} [headers]
  *
  */
-module.exports = class URI
-{
-  /**
-    * Parse the given string and returns a JsSIP.URI instance or undefined if
-    * it is an invalid URI.
-    */
-  static parse(uri)
-  {
-    uri = Grammar.parse(uri, 'SIP_URI');
-
-    if (uri !== -1)
-    {
-      return uri;
-    }
-    else
-    {
-      return undefined;
-    }
-  }
-
-  constructor(scheme, user, host, port, parameters = {}, headers = {})
-  {
-    // Checks.
-    if (!host)
-    {
-      throw new TypeError('missing or invalid "host" parameter');
-    }
-
-    // Initialize parameters.
-    this._parameters = {};
-    this._headers = {};
-
-    this._scheme = scheme || JsSIP_C.SIP;
-    this._user = user;
-    this._host = host;
-    this._port = port;
-
-    for (const param in parameters)
-    {
-      if (Object.prototype.hasOwnProperty.call(parameters, param))
-      {
-        this.setParam(param, parameters[param]);
-      }
-    }
-
-    for (const header in headers)
-    {
-      if (Object.prototype.hasOwnProperty.call(headers, header))
-      {
-        this.setHeader(header, headers[header]);
-      }
-    }
-  }
-
-  get scheme()
-  {
-    return this._scheme;
-  }
-
-  set scheme(value)
-  {
-    this._scheme = value.toLowerCase();
-  }
-
-  get user()
-  {
-    return this._user;
-  }
-
-  set user(value)
-  {
-    this._user = value;
-  }
-
-  get host()
-  {
-    return this._host;
-  }
-
-  set host(value)
-  {
-    this._host = value.toLowerCase();
-  }
-
-  get port()
-  {
-    return this._port;
-  }
-
-  set port(value)
-  {
-    this._port = value === 0 ? value : (parseInt(value, 10) || null);
-  }
-
-  setParam(key, value)
-  {
-    if (key)
-    {
-      this._parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString();
-    }
-  }
-
-  getParam(key)
-  {
-    if (key)
-    {
-      return this._parameters[key.toLowerCase()];
-    }
-  }
-
-  hasParam(key)
-  {
-    if (key)
-    {
-      return (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false;
-    }
-  }
-
-  deleteParam(parameter)
-  {
-    parameter = parameter.toLowerCase();
-    if (this._parameters.hasOwnProperty(parameter))
-    {
-      const value = this._parameters[parameter];
-
-      delete this._parameters[parameter];
-
-      return value;
-    }
-  }
-
-  clearParams()
-  {
-    this._parameters = {};
-  }
-
-  setHeader(name, value)
-  {
-    this._headers[Utils.headerize(name)] = (Array.isArray(value)) ? value : [ value ];
-  }
-
-  getHeader(name)
-  {
-    if (name)
-    {
-      return this._headers[Utils.headerize(name)];
-    }
-  }
-
-  hasHeader(name)
-  {
-    if (name)
-    {
-      return (this._headers.hasOwnProperty(Utils.headerize(name)) && true) || false;
-    }
-  }
-
-  deleteHeader(header)
-  {
-    header = Utils.headerize(header);
-    if (this._headers.hasOwnProperty(header))
-    {
-      const value = this._headers[header];
-
-      delete this._headers[header];
-
-      return value;
-    }
-  }
-
-  clearHeaders()
-  {
-    this._headers = {};
-  }
-
-  clone()
-  {
-    return new URI(
-      this._scheme,
-      this._user,
-      this._host,
-      this._port,
-      JSON.parse(JSON.stringify(this._parameters)),
-      JSON.parse(JSON.stringify(this._headers)));
-  }
-
-  toString()
-  {
-    const headers = [];
-
-    let uri = `${this._scheme}:`;
-
-    if (this._user)
-    {
-      uri += `${Utils.escapeUser(this._user)}@`;
-    }
-    uri += this._host;
-    if (this._port || this._port === 0)
-    {
-      uri += `:${this._port}`;
-    }
-
-    for (const parameter in this._parameters)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._parameters, parameter))
-      {
-        uri += `;${parameter}`;
-
-        if (this._parameters[parameter] !== null)
-        {
-          uri += `=${this._parameters[parameter]}`;
-        }
-      }
-    }
-
-    for (const header in this._headers)
-    {
-      if (Object.prototype.hasOwnProperty.call(this._headers, header))
-      {
-        for (const item of this._headers[header])
-        {
-          headers.push(`${header}=${item}`);
-        }
-      }
-    }
-
-    if (headers.length > 0)
-    {
-      uri += `?${headers.join('&')}`;
-    }
-
-    return uri;
-  }
-
-  toAor(show_port)
-  {
-    let aor = `${this._scheme}:`;
-
-    if (this._user)
-    {
-      aor += `${Utils.escapeUser(this._user)}@`;
-    }
-    aor += this._host;
-    if (show_port && (this._port || this._port === 0))
-    {
-      aor += `:${this._port}`;
-    }
-
-    return aor;
-  }
+module.exports = class URI {
+	/**
+	 * Parse the given string and returns a JsSIP.URI instance or undefined if
+	 * it is an invalid URI.
+	 */
+	static parse(uri) {
+		uri = Grammar.parse(uri, 'SIP_URI');
+
+		if (uri !== -1) {
+			return uri;
+		} else {
+			return undefined;
+		}
+	}
+
+	constructor(scheme, user, host, port, parameters = {}, headers = {}) {
+		// Checks.
+		if (!host) {
+			throw new TypeError('missing or invalid "host" parameter');
+		}
+
+		// Initialize parameters.
+		this._parameters = {};
+		this._headers = {};
+
+		this._scheme = scheme || JsSIP_C.SIP;
+		this._user = user;
+		this._host = host;
+		this._port = port;
+
+		for (const param in parameters) {
+			if (Object.prototype.hasOwnProperty.call(parameters, param)) {
+				this.setParam(param, parameters[param]);
+			}
+		}
+
+		for (const header in headers) {
+			if (Object.prototype.hasOwnProperty.call(headers, header)) {
+				this.setHeader(header, headers[header]);
+			}
+		}
+	}
+
+	get scheme() {
+		return this._scheme;
+	}
+
+	set scheme(value) {
+		this._scheme = value.toLowerCase();
+	}
+
+	get user() {
+		return this._user;
+	}
+
+	set user(value) {
+		this._user = value;
+	}
+
+	get host() {
+		return this._host;
+	}
+
+	set host(value) {
+		this._host = value.toLowerCase();
+	}
+
+	get port() {
+		return this._port;
+	}
+
+	set port(value) {
+		this._port = value === 0 ? value : parseInt(value, 10) || null;
+	}
+
+	setParam(key, value) {
+		if (key) {
+			this._parameters[key.toLowerCase()] =
+				typeof value === 'undefined' || value === null
+					? null
+					: value.toString();
+		}
+	}
+
+	getParam(key) {
+		if (key) {
+			return this._parameters[key.toLowerCase()];
+		}
+	}
+
+	hasParam(key) {
+		if (key) {
+			return (
+				(this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false
+			);
+		}
+	}
+
+	deleteParam(parameter) {
+		parameter = parameter.toLowerCase();
+		if (this._parameters.hasOwnProperty(parameter)) {
+			const value = this._parameters[parameter];
+
+			delete this._parameters[parameter];
+
+			return value;
+		}
+	}
+
+	clearParams() {
+		this._parameters = {};
+	}
+
+	setHeader(name, value) {
+		this._headers[Utils.headerize(name)] = Array.isArray(value)
+			? value
+			: [value];
+	}
+
+	getHeader(name) {
+		if (name) {
+			return this._headers[Utils.headerize(name)];
+		}
+	}
+
+	hasHeader(name) {
+		if (name) {
+			return (
+				(this._headers.hasOwnProperty(Utils.headerize(name)) && true) || false
+			);
+		}
+	}
+
+	deleteHeader(header) {
+		header = Utils.headerize(header);
+		if (this._headers.hasOwnProperty(header)) {
+			const value = this._headers[header];
+
+			delete this._headers[header];
+
+			return value;
+		}
+	}
+
+	clearHeaders() {
+		this._headers = {};
+	}
+
+	clone() {
+		return new URI(
+			this._scheme,
+			this._user,
+			this._host,
+			this._port,
+			JSON.parse(JSON.stringify(this._parameters)),
+			JSON.parse(JSON.stringify(this._headers))
+		);
+	}
+
+	toString() {
+		const headers = [];
+
+		let uri = `${this._scheme}:`;
+
+		if (this._user) {
+			uri += `${Utils.escapeUser(this._user)}@`;
+		}
+		uri += this._host;
+		if (this._port || this._port === 0) {
+			uri += `:${this._port}`;
+		}
+
+		for (const parameter in this._parameters) {
+			if (Object.prototype.hasOwnProperty.call(this._parameters, parameter)) {
+				uri += `;${parameter}`;
+
+				if (this._parameters[parameter] !== null) {
+					uri += `=${this._parameters[parameter]}`;
+				}
+			}
+		}
+
+		for (const header in this._headers) {
+			if (Object.prototype.hasOwnProperty.call(this._headers, header)) {
+				for (const item of this._headers[header]) {
+					headers.push(`${header}=${item}`);
+				}
+			}
+		}
+
+		if (headers.length > 0) {
+			uri += `?${headers.join('&')}`;
+		}
+
+		return uri;
+	}
+
+	toAor(show_port) {
+		let aor = `${this._scheme}:`;
+
+		if (this._user) {
+			aor += `${Utils.escapeUser(this._user)}@`;
+		}
+		aor += this._host;
+		if (show_port && (this._port || this._port === 0)) {
+			aor += `:${this._port}`;
+		}
+
+		return aor;
+	}
 };
diff --git a/src/Utils.d.ts b/src/Utils.d.ts
index 3e73f88..051a04c 100644
--- a/src/Utils.d.ts
+++ b/src/Utils.d.ts
@@ -1,5 +1,5 @@
-import {URI} from './URI'
-import {causes} from './Constants'
+import { URI } from './URI';
+import { causes } from './Constants';

 export function str_utf8_length(str: string): number;

@@ -9,6 +9,7 @@ export function isDecimal(num: unknown): num is number;

 export function isEmpty(value: unknown): boolean;

+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export function hasMethods(obj: any, ...methodNames: string[]): boolean;

 export function newTag(): string;
@@ -19,7 +20,10 @@ export function hostType(host: string): string;

 export function escapeUser(user: string): string;

-export function normalizeTarget(target: URI | string, domain?: string): URI | undefined;
+export function normalizeTarget(
+	target: URI | string,
+	domain?: string
+): URI | undefined;

 export function headerize(str: string): string;

diff --git a/src/Utils.js b/src/Utils.js
index 065873e..1ed5aac 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -2,551 +2,494 @@ const JsSIP_C = require('./Constants');
 const URI = require('./URI');
 const Grammar = require('./Grammar');

-exports.str_utf8_length = (string) => unescape(encodeURIComponent(string)).length;
+exports.str_utf8_length = string => unescape(encodeURIComponent(string)).length;

 // Used by 'hasMethods'.
-const isFunction = exports.isFunction = (fn) =>
-{
-  if (fn !== undefined)
-  {
-    return (Object.prototype.toString.call(fn) === '[object Function]')? true : false;
-  }
-  else
-  {
-    return false;
-  }
+const isFunction = (exports.isFunction = fn => {
+	if (fn !== undefined) {
+		return Object.prototype.toString.call(fn) === '[object Function]'
+			? true
+			: false;
+	} else {
+		return false;
+	}
+});
+
+exports.isString = str => {
+	if (str !== undefined) {
+		return Object.prototype.toString.call(str) === '[object String]'
+			? true
+			: false;
+	} else {
+		return false;
+	}
 };

-exports.isString = (str) =>
-{
-  if (str !== undefined)
-  {
-    return (Object.prototype.toString.call(str) === '[object String]')? true : false;
-  }
-  else
-  {
-    return false;
-  }
-};
-
-exports.isDecimal = (num) => !isNaN(num) && (parseFloat(num) === parseInt(num, 10));
+exports.isDecimal = num => !isNaN(num) && parseFloat(num) === parseInt(num, 10);

-exports.isEmpty = (value) =>
-{
-  return (value === null ||
-      value === '' ||
-      value === undefined ||
-      (Array.isArray(value) && value.length === 0) ||
-      (typeof(value) === 'number' && isNaN(value)));
+exports.isEmpty = value => {
+	return (
+		value === null ||
+		value === '' ||
+		value === undefined ||
+		(Array.isArray(value) && value.length === 0) ||
+		(typeof value === 'number' && isNaN(value))
+	);
 };

-exports.hasMethods = function(obj, ...methodNames)
-{
-  for (const methodName of methodNames)
-  {
-    if (isFunction(obj[methodName]))
-    {
-      return false;
-    }
-  }
-
-  return true;
+exports.hasMethods = function (obj, ...methodNames) {
+	for (const methodName of methodNames) {
+		if (isFunction(obj[methodName])) {
+			return false;
+		}
+	}
+
+	return true;
 };

 // Used by 'newTag'.
-const createRandomToken = exports.createRandomToken = (size, base = 32) =>
-{
-  let i, r, token = '';
+const createRandomToken = (exports.createRandomToken = (size, base = 32) => {
+	let i,
+		r,
+		token = '';

-  for (i=0; i < size; i++)
-  {
-    r = (Math.random() * base) | 0;
-    token += r.toString(base);
-  }
+	for (i = 0; i < size; i++) {
+		r = (Math.random() * base) | 0;
+		token += r.toString(base);
+	}

-  return token;
-};
+	return token;
+});

 exports.newTag = () => createRandomToken(10);

 // https://stackoverflow.com/users/109538/broofa.
-exports.newUUID = () =>
-{
-  const UUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) =>
-  {
-    const r = Math.random()*16|0, v = c === 'x' ? r : ((r&0x3)|0x8);
-
+exports.newUUID = () => {
+	const UUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+		const r = (Math.random() * 16) | 0,
+			v = c === 'x' ? r : (r & 0x3) | 0x8;

-    return v.toString(16);
-  });
+		return v.toString(16);
+	});

-  return UUID;
+	return UUID;
 };

-exports.hostType = (host) =>
-{
-  if (!host)
-  {
-    return;
-  }
-  else
-  {
-    host = Grammar.parse(host, 'host');
-    if (host !== -1)
-    {
-      return host.host_type;
-    }
-  }
+exports.hostType = host => {
+	if (!host) {
+		return;
+	} else {
+		host = Grammar.parse(host, 'host');
+		if (host !== -1) {
+			return host.host_type;
+		}
+	}
 };

 /**
-* Hex-escape a SIP URI user.
-* Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F).
-*
-* Used by 'normalizeTarget'.
-*/
-const escapeUser = exports.escapeUser = (user) =>
-  encodeURIComponent(decodeURIComponent(user))
-    .replace(/%3A/ig, ':')
-    .replace(/%2B/ig, '+')
-    .replace(/%3F/ig, '?')
-    .replace(/%2F/ig, '/');
+ * Hex-escape a SIP URI user.
+ * Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F).
+ *
+ * Used by 'normalizeTarget'.
+ */
+const escapeUser = (exports.escapeUser = user =>
+	encodeURIComponent(decodeURIComponent(user))
+		.replace(/%3A/gi, ':')
+		.replace(/%2B/gi, '+')
+		.replace(/%3F/gi, '?')
+		.replace(/%2F/gi, '/'));

 /**
-* Normalize SIP URI.
-* NOTE: It does not allow a SIP URI without username.
-* Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'.
-* Detects the domain part (if given) and properly hex-escapes the user portion.
-* If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators.
-*/
-exports.normalizeTarget = (target, domain) =>
-{
-  // If no target is given then raise an error.
-  if (!target)
-  {
-    return;
-  // If a URI instance is given then return it.
-  }
-  else if (target instanceof URI)
-  {
-    return target;
-
-  // If a string is given split it by '@':
-  // - Last fragment is the desired domain.
-  // - Otherwise append the given domain argument.
-  }
-  else if (typeof target === 'string')
-  {
-    const target_array = target.split('@');
-    let target_user;
-    let target_domain;
-
-    switch (target_array.length)
-    {
-      case 1:
-        if (!domain)
-        {
-          return;
-        }
-        target_user = target;
-        target_domain = domain;
-        break;
-      case 2:
-        target_user = target_array[0];
-        target_domain = target_array[1];
-        break;
-      default:
-        target_user = target_array.slice(0, target_array.length-1).join('@');
-        target_domain = target_array[target_array.length-1];
-    }
-
-    // Remove the URI scheme (if present).
-    target_user = target_user.replace(/^(sips?|tel):/i, '');
-
-    // Remove 'tel' visual separators if the user portion just contains 'tel' number symbols.
-    if (/^[-.()]*\+?[0-9\-.()]+$/.test(target_user))
-    {
-      target_user = target_user.replace(/[-.()]/g, '');
-    }
-
-    // Build the complete SIP URI.
-    target = `${JsSIP_C.SIP}:${escapeUser(target_user)}@${target_domain}`;
-
-    // Finally parse the resulting URI.
-    let uri;
-
-    if ((uri = URI.parse(target)))
-    {
-      return uri;
-    }
-    else
-    {
-      return;
-    }
-  }
-  else
-  {
-    return;
-  }
+ * Normalize SIP URI.
+ * NOTE: It does not allow a SIP URI without username.
+ * Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'.
+ * Detects the domain part (if given) and properly hex-escapes the user portion.
+ * If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators.
+ */
+exports.normalizeTarget = (target, domain) => {
+	// If no target is given then raise an error.
+	if (!target) {
+		return;
+		// If a URI instance is given then return it.
+	} else if (target instanceof URI) {
+		return target;
+
+		// If a string is given split it by '@':
+		// - Last fragment is the desired domain.
+		// - Otherwise append the given domain argument.
+	} else if (typeof target === 'string') {
+		const target_array = target.split('@');
+		let target_user;
+		let target_domain;
+
+		switch (target_array.length) {
+			case 1: {
+				if (!domain) {
+					return;
+				}
+				target_user = target;
+				target_domain = domain;
+				break;
+			}
+			case 2: {
+				target_user = target_array[0];
+				target_domain = target_array[1];
+				break;
+			}
+			default: {
+				target_user = target_array.slice(0, target_array.length - 1).join('@');
+				target_domain = target_array[target_array.length - 1];
+			}
+		}
+
+		// Remove the URI scheme (if present).
+		target_user = target_user.replace(/^(sips?|tel):/i, '');
+
+		// Remove 'tel' visual separators if the user portion just contains 'tel' number symbols.
+		if (/^[-.()]*\+?[0-9\-.()]+$/.test(target_user)) {
+			target_user = target_user.replace(/[-.()]/g, '');
+		}
+
+		// Build the complete SIP URI.
+		target = `${JsSIP_C.SIP}:${escapeUser(target_user)}@${target_domain}`;
+
+		// Finally parse the resulting URI.
+		let uri;
+
+		if ((uri = URI.parse(target))) {
+			return uri;
+		} else {
+			return;
+		}
+	} else {
+		return;
+	}
 };

-exports.headerize = (string) =>
-{
-  const exceptions = {
-    'Call-Id'          : 'Call-ID',
-    'Cseq'             : 'CSeq',
-    'Www-Authenticate' : 'WWW-Authenticate'
-  };
-
-  const name = string.toLowerCase()
-    .replace(/_/g, '-')
-    .split('-');
-  let hname = '';
-  const parts = name.length;
-  let part;
-
-  for (part = 0; part < parts; part++)
-  {
-    if (part !== 0)
-    {
-      hname +='-';
-    }
-    hname += name[part].charAt(0).toUpperCase()+name[part].substring(1);
-  }
-  if (exceptions[hname])
-  {
-    hname = exceptions[hname];
-  }
-
-  return hname;
+exports.headerize = string => {
+	const exceptions = {
+		'Call-Id': 'Call-ID',
+		Cseq: 'CSeq',
+		'Www-Authenticate': 'WWW-Authenticate',
+	};
+
+	const name = string.toLowerCase().replace(/_/g, '-').split('-');
+	let hname = '';
+	const parts = name.length;
+	let part;
+
+	for (part = 0; part < parts; part++) {
+		if (part !== 0) {
+			hname += '-';
+		}
+		hname += name[part].charAt(0).toUpperCase() + name[part].substring(1);
+	}
+	if (exceptions[hname]) {
+		hname = exceptions[hname];
+	}
+
+	return hname;
 };

-exports.sipErrorCause = (status_code) =>
-{
-  for (const cause in JsSIP_C.SIP_ERROR_CAUSES)
-  {
-    if (JsSIP_C.SIP_ERROR_CAUSES[cause].indexOf(status_code) !== -1)
-    {
-      return JsSIP_C.causes[cause];
-    }
-  }
-
-  return JsSIP_C.causes.SIP_FAILURE_CODE;
+exports.sipErrorCause = status_code => {
+	for (const cause in JsSIP_C.SIP_ERROR_CAUSES) {
+		if (JsSIP_C.SIP_ERROR_CAUSES[cause].indexOf(status_code) !== -1) {
+			return JsSIP_C.causes[cause];
+		}
+	}
+
+	return JsSIP_C.causes.SIP_FAILURE_CODE;
 };

 /**
-* Generate a random Test-Net IP (https://tools.ietf.org/html/rfc5735)
-*/
-exports.getRandomTestNetIP = () =>
-{
-  function getOctet(from, to)
-  {
-    return Math.floor((Math.random() * (to-from+1)) + from);
-  }
-
-  return `192.0.2.${getOctet(1, 254)}`;
+ * Generate a random Test-Net IP (https://tools.ietf.org/html/rfc5735)
+ */
+exports.getRandomTestNetIP = () => {
+	function getOctet(from, to) {
+		return Math.floor(Math.random() * (to - from + 1) + from);
+	}
+
+	return `192.0.2.${getOctet(1, 254)}`;
 };

 // MD5 (Message-Digest Algorithm) https://www.webtoolkit.info.
-exports.calculateMD5 = (string) =>
-{
-  function rotateLeft(lValue, iShiftBits)
-  {
-    return (lValue<<iShiftBits) | (lValue>>>(32-iShiftBits));
-  }
-
-  function addUnsigned(lX, lY)
-  {
-    const lX8 = (lX & 0x80000000);
-    const lY8 = (lY & 0x80000000);
-    const lX4 = (lX & 0x40000000);
-    const lY4 = (lY & 0x40000000);
-    const lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
-
-    if (lX4 & lY4)
-    {
-      return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
-    }
-    if (lX4 | lY4)
-    {
-      if (lResult & 0x40000000)
-      {
-        return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
-      }
-      else
-      {
-        return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
-      }
-    }
-    else
-    {
-      return (lResult ^ lX8 ^ lY8);
-    }
-  }
-
-  function doF(x, y, z)
-  {
-    return (x & y) | ((~x) & z);
-  }
-
-  function doG(x, y, z)
-  {
-    return (x & z) | (y & (~z));
-  }
-
-  function doH(x, y, z)
-  {
-    return (x ^ y ^ z);
-  }
-
-  function doI(x, y, z)
-  {
-    return (y ^ (x | (~z)));
-  }
-
-  function doFF(a, b, c, d, x, s, ac)
-  {
-    a = addUnsigned(a, addUnsigned(addUnsigned(doF(b, c, d), x), ac));
-
-    return addUnsigned(rotateLeft(a, s), b);
-  }
-
-  function doGG(a, b, c, d, x, s, ac)
-  {
-    a = addUnsigned(a, addUnsigned(addUnsigned(doG(b, c, d), x), ac));
-
-    return addUnsigned(rotateLeft(a, s), b);
-  }
-
-  function doHH(a, b, c, d, x, s, ac)
-  {
-    a = addUnsigned(a, addUnsigned(addUnsigned(doH(b, c, d), x), ac));
-
-    return addUnsigned(rotateLeft(a, s), b);
-  }
-
-  function doII(a, b, c, d, x, s, ac)
-  {
-    a = addUnsigned(a, addUnsigned(addUnsigned(doI(b, c, d), x), ac));
-
-    return addUnsigned(rotateLeft(a, s), b);
-  }
-
-  function convertToWordArray(str)
-  {
-    let lWordCount;
-    const lMessageLength = str.length;
-    const lNumberOfWords_temp1=lMessageLength + 8;
-    const lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
-    const lNumberOfWords = (lNumberOfWords_temp2+1)*16;
-    const lWordArray = new Array(lNumberOfWords-1);
-    let lBytePosition = 0;
-    let lByteCount = 0;
-
-    while (lByteCount < lMessageLength)
-    {
-      lWordCount = (lByteCount-(lByteCount % 4))/4;
-      lBytePosition = (lByteCount % 4)*8;
-      lWordArray[lWordCount] = (lWordArray[lWordCount] |
-          (str.charCodeAt(lByteCount)<<lBytePosition));
-      lByteCount++;
-    }
-    lWordCount = (lByteCount-(lByteCount % 4))/4;
-    lBytePosition = (lByteCount % 4)*8;
-    lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<<lBytePosition);
-    lWordArray[lNumberOfWords-2] = lMessageLength<<3;
-    lWordArray[lNumberOfWords-1] = lMessageLength>>>29;
-
-    return lWordArray;
-  }
-
-  function wordToHex(lValue)
-  {
-    let wordToHexValue='', wordToHexValue_temp='', lByte, lCount;
-
-    for (lCount = 0; lCount<=3; lCount++)
-    {
-      lByte = (lValue>>>(lCount*8)) & 255;
-      wordToHexValue_temp = `0${lByte.toString(16)}`;
-      wordToHexValue = wordToHexValue +
-        wordToHexValue_temp.substr(wordToHexValue_temp.length-2, 2);
-    }
-
-    return wordToHexValue;
-  }
-
-  function utf8Encode(str)
-  {
-    let utftext = '';
-
-    for (let n = 0; n < str.length; n++)
-    {
-      const c = str.charCodeAt(n);
-
-      if (c < 128)
-      {
-        utftext += String.fromCharCode(c);
-      }
-      else if ((c > 127) && (c < 2048))
-      {
-        utftext += String.fromCharCode((c >> 6) | 192);
-        utftext += String.fromCharCode((c & 63) | 128);
-      }
-      else
-      {
-        utftext += String.fromCharCode((c >> 12) | 224);
-        utftext += String.fromCharCode(((c >> 6) & 63) | 128);
-        utftext += String.fromCharCode((c & 63) | 128);
-      }
-    }
-
-    return utftext;
-  }
-
-  let x=[];
-  let k, AA, BB, CC, DD, a, b, c, d;
-  const S11=7, S12=12, S13=17, S14=22;
-  const S21=5, S22=9, S23=14, S24=20;
-  const S31=4, S32=11, S33=16, S34=23;
-  const S41=6, S42=10, S43=15, S44=21;
-
-  string = utf8Encode(string);
-
-  x = convertToWordArray(string);
-
-  a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
-
-  for (k=0; k<x.length; k+=16)
-  {
-    AA=a; BB=b; CC=c; DD=d;
-    a=doFF(a, b, c, d, x[k+0], S11, 0xD76AA478);
-    d=doFF(d, a, b, c, x[k+1], S12, 0xE8C7B756);
-    c=doFF(c, d, a, b, x[k+2], S13, 0x242070DB);
-    b=doFF(b, c, d, a, x[k+3], S14, 0xC1BDCEEE);
-    a=doFF(a, b, c, d, x[k+4], S11, 0xF57C0FAF);
-    d=doFF(d, a, b, c, x[k+5], S12, 0x4787C62A);
-    c=doFF(c, d, a, b, x[k+6], S13, 0xA8304613);
-    b=doFF(b, c, d, a, x[k+7], S14, 0xFD469501);
-    a=doFF(a, b, c, d, x[k+8], S11, 0x698098D8);
-    d=doFF(d, a, b, c, x[k+9], S12, 0x8B44F7AF);
-    c=doFF(c, d, a, b, x[k+10], S13, 0xFFFF5BB1);
-    b=doFF(b, c, d, a, x[k+11], S14, 0x895CD7BE);
-    a=doFF(a, b, c, d, x[k+12], S11, 0x6B901122);
-    d=doFF(d, a, b, c, x[k+13], S12, 0xFD987193);
-    c=doFF(c, d, a, b, x[k+14], S13, 0xA679438E);
-    b=doFF(b, c, d, a, x[k+15], S14, 0x49B40821);
-    a=doGG(a, b, c, d, x[k+1], S21, 0xF61E2562);
-    d=doGG(d, a, b, c, x[k+6], S22, 0xC040B340);
-    c=doGG(c, d, a, b, x[k+11], S23, 0x265E5A51);
-    b=doGG(b, c, d, a, x[k+0], S24, 0xE9B6C7AA);
-    a=doGG(a, b, c, d, x[k+5], S21, 0xD62F105D);
-    d=doGG(d, a, b, c, x[k+10], S22, 0x2441453);
-    c=doGG(c, d, a, b, x[k+15], S23, 0xD8A1E681);
-    b=doGG(b, c, d, a, x[k+4], S24, 0xE7D3FBC8);
-    a=doGG(a, b, c, d, x[k+9], S21, 0x21E1CDE6);
-    d=doGG(d, a, b, c, x[k+14], S22, 0xC33707D6);
-    c=doGG(c, d, a, b, x[k+3], S23, 0xF4D50D87);
-    b=doGG(b, c, d, a, x[k+8], S24, 0x455A14ED);
-    a=doGG(a, b, c, d, x[k+13], S21, 0xA9E3E905);
-    d=doGG(d, a, b, c, x[k+2], S22, 0xFCEFA3F8);
-    c=doGG(c, d, a, b, x[k+7], S23, 0x676F02D9);
-    b=doGG(b, c, d, a, x[k+12], S24, 0x8D2A4C8A);
-    a=doHH(a, b, c, d, x[k+5], S31, 0xFFFA3942);
-    d=doHH(d, a, b, c, x[k+8], S32, 0x8771F681);
-    c=doHH(c, d, a, b, x[k+11], S33, 0x6D9D6122);
-    b=doHH(b, c, d, a, x[k+14], S34, 0xFDE5380C);
-    a=doHH(a, b, c, d, x[k+1], S31, 0xA4BEEA44);
-    d=doHH(d, a, b, c, x[k+4], S32, 0x4BDECFA9);
-    c=doHH(c, d, a, b, x[k+7], S33, 0xF6BB4B60);
-    b=doHH(b, c, d, a, x[k+10], S34, 0xBEBFBC70);
-    a=doHH(a, b, c, d, x[k+13], S31, 0x289B7EC6);
-    d=doHH(d, a, b, c, x[k+0], S32, 0xEAA127FA);
-    c=doHH(c, d, a, b, x[k+3], S33, 0xD4EF3085);
-    b=doHH(b, c, d, a, x[k+6], S34, 0x4881D05);
-    a=doHH(a, b, c, d, x[k+9], S31, 0xD9D4D039);
-    d=doHH(d, a, b, c, x[k+12], S32, 0xE6DB99E5);
-    c=doHH(c, d, a, b, x[k+15], S33, 0x1FA27CF8);
-    b=doHH(b, c, d, a, x[k+2], S34, 0xC4AC5665);
-    a=doII(a, b, c, d, x[k+0], S41, 0xF4292244);
-    d=doII(d, a, b, c, x[k+7], S42, 0x432AFF97);
-    c=doII(c, d, a, b, x[k+14], S43, 0xAB9423A7);
-    b=doII(b, c, d, a, x[k+5], S44, 0xFC93A039);
-    a=doII(a, b, c, d, x[k+12], S41, 0x655B59C3);
-    d=doII(d, a, b, c, x[k+3], S42, 0x8F0CCC92);
-    c=doII(c, d, a, b, x[k+10], S43, 0xFFEFF47D);
-    b=doII(b, c, d, a, x[k+1], S44, 0x85845DD1);
-    a=doII(a, b, c, d, x[k+8], S41, 0x6FA87E4F);
-    d=doII(d, a, b, c, x[k+15], S42, 0xFE2CE6E0);
-    c=doII(c, d, a, b, x[k+6], S43, 0xA3014314);
-    b=doII(b, c, d, a, x[k+13], S44, 0x4E0811A1);
-    a=doII(a, b, c, d, x[k+4], S41, 0xF7537E82);
-    d=doII(d, a, b, c, x[k+11], S42, 0xBD3AF235);
-    c=doII(c, d, a, b, x[k+2], S43, 0x2AD7D2BB);
-    b=doII(b, c, d, a, x[k+9], S44, 0xEB86D391);
-    a=addUnsigned(a, AA);
-    b=addUnsigned(b, BB);
-    c=addUnsigned(c, CC);
-    d=addUnsigned(d, DD);
-  }
-
-  const temp = wordToHex(a)+wordToHex(b)+wordToHex(c)+wordToHex(d);
-
-  return temp.toLowerCase();
+exports.calculateMD5 = string => {
+	function rotateLeft(lValue, iShiftBits) {
+		return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
+	}
+
+	function addUnsigned(lX, lY) {
+		const lX8 = lX & 0x80000000;
+		const lY8 = lY & 0x80000000;
+		const lX4 = lX & 0x40000000;
+		const lY4 = lY & 0x40000000;
+		const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
+
+		if (lX4 & lY4) {
+			return lResult ^ 0x80000000 ^ lX8 ^ lY8;
+		}
+		if (lX4 | lY4) {
+			if (lResult & 0x40000000) {
+				return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
+			} else {
+				return lResult ^ 0x40000000 ^ lX8 ^ lY8;
+			}
+		} else {
+			return lResult ^ lX8 ^ lY8;
+		}
+	}
+
+	function doF(x, y, z) {
+		return (x & y) | (~x & z);
+	}
+
+	function doG(x, y, z) {
+		return (x & z) | (y & ~z);
+	}
+
+	function doH(x, y, z) {
+		return x ^ y ^ z;
+	}
+
+	function doI(x, y, z) {
+		return y ^ (x | ~z);
+	}
+
+	function doFF(a, b, c, d, x, s, ac) {
+		a = addUnsigned(a, addUnsigned(addUnsigned(doF(b, c, d), x), ac));
+
+		return addUnsigned(rotateLeft(a, s), b);
+	}
+
+	function doGG(a, b, c, d, x, s, ac) {
+		a = addUnsigned(a, addUnsigned(addUnsigned(doG(b, c, d), x), ac));
+
+		return addUnsigned(rotateLeft(a, s), b);
+	}
+
+	function doHH(a, b, c, d, x, s, ac) {
+		a = addUnsigned(a, addUnsigned(addUnsigned(doH(b, c, d), x), ac));
+
+		return addUnsigned(rotateLeft(a, s), b);
+	}
+
+	function doII(a, b, c, d, x, s, ac) {
+		a = addUnsigned(a, addUnsigned(addUnsigned(doI(b, c, d), x), ac));
+
+		return addUnsigned(rotateLeft(a, s), b);
+	}
+
+	function convertToWordArray(str) {
+		let lWordCount;
+		const lMessageLength = str.length;
+		const lNumberOfWords_temp1 = lMessageLength + 8;
+		const lNumberOfWords_temp2 =
+			(lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
+		const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
+		const lWordArray = new Array(lNumberOfWords - 1);
+		let lBytePosition = 0;
+		let lByteCount = 0;
+
+		while (lByteCount < lMessageLength) {
+			lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+			lBytePosition = (lByteCount % 4) * 8;
+			lWordArray[lWordCount] =
+				lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition);
+			lByteCount++;
+		}
+		lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+		lBytePosition = (lByteCount % 4) * 8;
+		lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
+		lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
+		lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
+
+		return lWordArray;
+	}
+
+	function wordToHex(lValue) {
+		let wordToHexValue = '',
+			wordToHexValue_temp = '',
+			lByte,
+			lCount;
+
+		for (lCount = 0; lCount <= 3; lCount++) {
+			lByte = (lValue >>> (lCount * 8)) & 255;
+			wordToHexValue_temp = `0${lByte.toString(16)}`;
+			wordToHexValue =
+				wordToHexValue +
+				wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
+		}
+
+		return wordToHexValue;
+	}
+
+	function utf8Encode(str) {
+		let utftext = '';
+
+		for (let n = 0; n < str.length; n++) {
+			const c = str.charCodeAt(n);
+
+			if (c < 128) {
+				utftext += String.fromCharCode(c);
+			} else if (c > 127 && c < 2048) {
+				utftext += String.fromCharCode((c >> 6) | 192);
+				utftext += String.fromCharCode((c & 63) | 128);
+			} else {
+				utftext += String.fromCharCode((c >> 12) | 224);
+				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+				utftext += String.fromCharCode((c & 63) | 128);
+			}
+		}
+
+		return utftext;
+	}
+
+	let x = [];
+	let k, AA, BB, CC, DD, a, b, c, d;
+	const S11 = 7,
+		S12 = 12,
+		S13 = 17,
+		S14 = 22;
+	const S21 = 5,
+		S22 = 9,
+		S23 = 14,
+		S24 = 20;
+	const S31 = 4,
+		S32 = 11,
+		S33 = 16,
+		S34 = 23;
+	const S41 = 6,
+		S42 = 10,
+		S43 = 15,
+		S44 = 21;
+
+	string = utf8Encode(string);
+
+	x = convertToWordArray(string);
+
+	a = 0x67452301;
+	b = 0xefcdab89;
+	c = 0x98badcfe;
+	d = 0x10325476;
+
+	for (k = 0; k < x.length; k += 16) {
+		AA = a;
+		BB = b;
+		CC = c;
+		DD = d;
+		a = doFF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
+		d = doFF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
+		c = doFF(c, d, a, b, x[k + 2], S13, 0x242070db);
+		b = doFF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
+		a = doFF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
+		d = doFF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
+		c = doFF(c, d, a, b, x[k + 6], S13, 0xa8304613);
+		b = doFF(b, c, d, a, x[k + 7], S14, 0xfd469501);
+		a = doFF(a, b, c, d, x[k + 8], S11, 0x698098d8);
+		d = doFF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
+		c = doFF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
+		b = doFF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
+		a = doFF(a, b, c, d, x[k + 12], S11, 0x6b901122);
+		d = doFF(d, a, b, c, x[k + 13], S12, 0xfd987193);
+		c = doFF(c, d, a, b, x[k + 14], S13, 0xa679438e);
+		b = doFF(b, c, d, a, x[k + 15], S14, 0x49b40821);
+		a = doGG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
+		d = doGG(d, a, b, c, x[k + 6], S22, 0xc040b340);
+		c = doGG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
+		b = doGG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
+		a = doGG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
+		d = doGG(d, a, b, c, x[k + 10], S22, 0x2441453);
+		c = doGG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
+		b = doGG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
+		a = doGG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
+		d = doGG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
+		c = doGG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
+		b = doGG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
+		a = doGG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
+		d = doGG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
+		c = doGG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
+		b = doGG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
+		a = doHH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
+		d = doHH(d, a, b, c, x[k + 8], S32, 0x8771f681);
+		c = doHH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
+		b = doHH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
+		a = doHH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
+		d = doHH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
+		c = doHH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
+		b = doHH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
+		a = doHH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
+		d = doHH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
+		c = doHH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
+		b = doHH(b, c, d, a, x[k + 6], S34, 0x4881d05);
+		a = doHH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
+		d = doHH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
+		c = doHH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
+		b = doHH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
+		a = doII(a, b, c, d, x[k + 0], S41, 0xf4292244);
+		d = doII(d, a, b, c, x[k + 7], S42, 0x432aff97);
+		c = doII(c, d, a, b, x[k + 14], S43, 0xab9423a7);
+		b = doII(b, c, d, a, x[k + 5], S44, 0xfc93a039);
+		a = doII(a, b, c, d, x[k + 12], S41, 0x655b59c3);
+		d = doII(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
+		c = doII(c, d, a, b, x[k + 10], S43, 0xffeff47d);
+		b = doII(b, c, d, a, x[k + 1], S44, 0x85845dd1);
+		a = doII(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
+		d = doII(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
+		c = doII(c, d, a, b, x[k + 6], S43, 0xa3014314);
+		b = doII(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
+		a = doII(a, b, c, d, x[k + 4], S41, 0xf7537e82);
+		d = doII(d, a, b, c, x[k + 11], S42, 0xbd3af235);
+		c = doII(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
+		b = doII(b, c, d, a, x[k + 9], S44, 0xeb86d391);
+		a = addUnsigned(a, AA);
+		b = addUnsigned(b, BB);
+		c = addUnsigned(c, CC);
+		d = addUnsigned(d, DD);
+	}
+
+	const temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
+
+	return temp.toLowerCase();
 };

-exports.closeMediaStream = (stream) =>
-{
-  if (!stream)
-  {
-    return;
-  }
-
-  // Latest spec states that MediaStream has no stop() method and instead must
-  // call stop() on every MediaStreamTrack.
-  try
-  {
-    let tracks;
-
-    if (stream.getTracks)
-    {
-      tracks = stream.getTracks();
-      for (const track of tracks)
-      {
-        track.stop();
-      }
-    }
-    else
-    {
-      tracks = stream.getAudioTracks();
-      for (const track of tracks)
-      {
-        track.stop();
-      }
-      tracks = stream.getVideoTracks();
-      for (const track of tracks)
-      {
-        track.stop();
-      }
-    }
-  }
-  // eslint-disable-next-line no-unused-vars
-  catch (error)
-  {
-    // Deprecated by the spec, but still in use.
-    // NOTE: In Temasys IE plugin stream.stop is a callable 'object'.
-    if (typeof stream.stop === 'function' || typeof stream.stop === 'object')
-    {
-      stream.stop();
-    }
-  }
+exports.closeMediaStream = stream => {
+	if (!stream) {
+		return;
+	}
+
+	// Latest spec states that MediaStream has no stop() method and instead must
+	// call stop() on every MediaStreamTrack.
+	try {
+		let tracks;
+
+		if (stream.getTracks) {
+			tracks = stream.getTracks();
+			for (const track of tracks) {
+				track.stop();
+			}
+		} else {
+			tracks = stream.getAudioTracks();
+			for (const track of tracks) {
+				track.stop();
+			}
+			tracks = stream.getVideoTracks();
+			for (const track of tracks) {
+				track.stop();
+			}
+		}
+	} catch (error) {
+		// Deprecated by the spec, but still in use.
+		// NOTE: In Temasys IE plugin stream.stop is a callable 'object'.
+		if (typeof stream.stop === 'function' || typeof stream.stop === 'object') {
+			stream.stop();
+		}
+	}
 };

-exports.cloneArray = (array) =>
-{
-  return (array && array.slice()) || [];
+exports.cloneArray = array => {
+	return (array && array.slice()) || [];
 };

-exports.cloneObject = (obj, fallback = {}) =>
-{
-  return (obj && Object.assign({}, obj)) || fallback;
+exports.cloneObject = (obj, fallback = {}) => {
+	return (obj && Object.assign({}, obj)) || fallback;
 };
diff --git a/src/WebSocketInterface.d.ts b/src/WebSocketInterface.d.ts
index 581b712..4e37a16 100644
--- a/src/WebSocketInterface.d.ts
+++ b/src/WebSocketInterface.d.ts
@@ -1,5 +1,5 @@
 import { Socket } from './Socket';

 export class WebSocketInterface extends Socket {
-  constructor(url: string)
+	constructor(url: string);
 }
diff --git a/src/WebSocketInterface.js b/src/WebSocketInterface.js
index 8d9de81..9dc582e 100644
--- a/src/WebSocketInterface.js
+++ b/src/WebSocketInterface.js
@@ -3,175 +3,142 @@ const Grammar = require('./Grammar');

 const logger = new Logger('WebSocketInterface');

-module.exports = class WebSocketInterface
-{
-  constructor(url)
-  {
-    logger.debug('new() [url:"%s"]', url);
-
-    this._url = url;
-    this._sip_uri = null;
-    this._via_transport = null;
-    this._ws = null;
-
-    const parsed_url = Grammar.parse(url, 'absoluteURI');
-
-    if (parsed_url === -1)
-    {
-      logger.warn(`invalid WebSocket URI: ${url}`);
-      throw new TypeError(`Invalid argument: ${url}`);
-    }
-    else if (parsed_url.scheme !== 'wss' && parsed_url.scheme !== 'ws')
-    {
-      logger.warn(`invalid WebSocket URI scheme: ${parsed_url.scheme}`);
-      throw new TypeError(`Invalid argument: ${url}`);
-    }
-    else
-    {
-      this._sip_uri = `sip:${parsed_url.host}${parsed_url.port ? `:${parsed_url.port}` : ''};transport=ws`;
-      this._via_transport = parsed_url.scheme.toUpperCase();
-    }
-  }
-
-  get via_transport()
-  {
-    return this._via_transport;
-  }
-
-  set via_transport(value)
-  {
-    this._via_transport = value.toUpperCase();
-  }
-
-  get sip_uri()
-  {
-    return this._sip_uri;
-  }
-
-  get url()
-  {
-    return this._url;
-  }
-
-  connect()
-  {
-    logger.debug('connect()');
-
-    if (this.isConnected())
-    {
-      logger.debug(`WebSocket ${this._url} is already connected`);
-
-      return;
-    }
-    else if (this.isConnecting())
-    {
-      logger.debug(`WebSocket ${this._url} is connecting`);
-
-      return;
-    }
-
-    if (this._ws)
-    {
-      this.disconnect();
-    }
-
-    logger.debug(`connecting to WebSocket ${this._url}`);
-
-    try
-    {
-      this._ws = new WebSocket(this._url, 'sip');
-
-      this._ws.binaryType = 'arraybuffer';
-
-      this._ws.onopen = this._onOpen.bind(this);
-      this._ws.onclose = this._onClose.bind(this);
-      this._ws.onmessage = this._onMessage.bind(this);
-      this._ws.onerror = this._onError.bind(this);
-    }
-    catch (error)
-    {
-      this._onError(error);
-    }
-  }
-
-  disconnect()
-  {
-    logger.debug('disconnect()');
-
-    if (this._ws)
-    {
-      // Unbind websocket event callbacks.
-      this._ws.onopen = () => {};
-      this._ws.onclose = () => {};
-      this._ws.onmessage = () => {};
-      this._ws.onerror = () => {};
-
-      this._ws.close();
-      this._ws = null;
-    }
-  }
-
-  send(message)
-  {
-    logger.debug('send()');
-
-    if (this.isConnected())
-    {
-      this._ws.send(message);
-
-      return true;
-    }
-    else
-    {
-      logger.warn('unable to send message, WebSocket is not open');
-
-      return false;
-    }
-  }
-
-  isConnected()
-  {
-    return this._ws && this._ws.readyState === this._ws.OPEN;
-  }
-
-  isConnecting()
-  {
-    return this._ws && this._ws.readyState === this._ws.CONNECTING;
-  }
-
-
-  /**
-   * WebSocket Event Handlers
-   */
-
-  _onOpen()
-  {
-    logger.debug(`WebSocket ${this._url} connected`);
-
-    this.onconnect();
-  }
-
-  _onClose({ wasClean, code, reason })
-  {
-    logger.debug(`WebSocket ${this._url} closed`);
-
-    if (wasClean === false)
-    {
-      logger.debug('WebSocket abrupt disconnection');
-    }
-
-    this.ondisconnect(!wasClean, code, reason);
-  }
-
-  _onMessage({ data })
-  {
-    logger.debug('received WebSocket message');
-
-    this.ondata(data);
-  }
-
-  _onError(e)
-  {
-    logger.warn(`WebSocket ${this._url} error: `, e);
-  }
+module.exports = class WebSocketInterface {
+	constructor(url) {
+		logger.debug('new() [url:"%s"]', url);
+
+		this._url = url;
+		this._sip_uri = null;
+		this._via_transport = null;
+		this._ws = null;
+
+		const parsed_url = Grammar.parse(url, 'absoluteURI');
+
+		if (parsed_url === -1) {
+			logger.warn(`invalid WebSocket URI: ${url}`);
+			throw new TypeError(`Invalid argument: ${url}`);
+		} else if (parsed_url.scheme !== 'wss' && parsed_url.scheme !== 'ws') {
+			logger.warn(`invalid WebSocket URI scheme: ${parsed_url.scheme}`);
+			throw new TypeError(`Invalid argument: ${url}`);
+		} else {
+			this._sip_uri = `sip:${parsed_url.host}${parsed_url.port ? `:${parsed_url.port}` : ''};transport=ws`;
+			this._via_transport = parsed_url.scheme.toUpperCase();
+		}
+	}
+
+	get via_transport() {
+		return this._via_transport;
+	}
+
+	set via_transport(value) {
+		this._via_transport = value.toUpperCase();
+	}
+
+	get sip_uri() {
+		return this._sip_uri;
+	}
+
+	get url() {
+		return this._url;
+	}
+
+	connect() {
+		logger.debug('connect()');
+
+		if (this.isConnected()) {
+			logger.debug(`WebSocket ${this._url} is already connected`);
+
+			return;
+		} else if (this.isConnecting()) {
+			logger.debug(`WebSocket ${this._url} is connecting`);
+
+			return;
+		}
+
+		if (this._ws) {
+			this.disconnect();
+		}
+
+		logger.debug(`connecting to WebSocket ${this._url}`);
+
+		try {
+			this._ws = new WebSocket(this._url, 'sip');
+
+			this._ws.binaryType = 'arraybuffer';
+
+			this._ws.onopen = this._onOpen.bind(this);
+			this._ws.onclose = this._onClose.bind(this);
+			this._ws.onmessage = this._onMessage.bind(this);
+			this._ws.onerror = this._onError.bind(this);
+		} catch (error) {
+			this._onError(error);
+		}
+	}
+
+	disconnect() {
+		logger.debug('disconnect()');
+
+		if (this._ws) {
+			// Unbind websocket event callbacks.
+			this._ws.onopen = () => {};
+			this._ws.onclose = () => {};
+			this._ws.onmessage = () => {};
+			this._ws.onerror = () => {};
+
+			this._ws.close();
+			this._ws = null;
+		}
+	}
+
+	send(message) {
+		logger.debug('send()');
+
+		if (this.isConnected()) {
+			this._ws.send(message);
+
+			return true;
+		} else {
+			logger.warn('unable to send message, WebSocket is not open');
+
+			return false;
+		}
+	}
+
+	isConnected() {
+		return this._ws && this._ws.readyState === this._ws.OPEN;
+	}
+
+	isConnecting() {
+		return this._ws && this._ws.readyState === this._ws.CONNECTING;
+	}
+
+	/**
+	 * WebSocket Event Handlers
+	 */
+
+	_onOpen() {
+		logger.debug(`WebSocket ${this._url} connected`);
+
+		this.onconnect();
+	}
+
+	_onClose({ wasClean, code, reason }) {
+		logger.debug(`WebSocket ${this._url} closed`);
+
+		if (wasClean === false) {
+			logger.debug('WebSocket abrupt disconnection');
+		}
+
+		this.ondisconnect(!wasClean, code, reason);
+	}
+
+	_onMessage({ data }) {
+		logger.debug('received WebSocket message');
+
+		this.ondata(data);
+	}
+
+	_onError(e) {
+		logger.warn(`WebSocket ${this._url} error: `, e);
+	}
 };
diff --git a/src/sanityCheck.js b/src/sanityCheck.js
index b843048..ef3d661 100644
--- a/src/sanityCheck.js
+++ b/src/sanityCheck.js
@@ -6,68 +6,53 @@ const Utils = require('./Utils');
 const logger = new Logger('sanityCheck');

 // Checks for requests and responses.
-const all = [ minimumHeaders ];
+const all = [minimumHeaders];

 // Checks for requests.
 const requests = [
-  rfc3261_8_2_2_1,
-  rfc3261_16_3_4,
-  rfc3261_18_3_request,
-  rfc3261_8_2_2_2
+	rfc3261_8_2_2_1,
+	rfc3261_16_3_4,
+	rfc3261_18_3_request,
+	rfc3261_8_2_2_2,
 ];

 // Checks for responses.
-const responses = [
-  rfc3261_8_1_3_3,
-  rfc3261_18_3_response
-];
+const responses = [rfc3261_8_1_3_3, rfc3261_18_3_response];

 // local variables.
 let message;
 let ua;
 let transport;

-module.exports = (m, u, t) =>
-{
-  message = m;
-  ua = u;
-  transport = t;
-
-  for (const check of all)
-  {
-    if (check() === false)
-    {
-      return false;
-    }
-  }
-
-  if (message instanceof SIPMessage.IncomingRequest)
-  {
-    for (const check of requests)
-    {
-      if (check() === false)
-      {
-        return false;
-      }
-    }
-  }
-
-  else if (message instanceof SIPMessage.IncomingResponse)
-  {
-    for (const check of responses)
-    {
-      if (check() === false)
-      {
-        return false;
-      }
-    }
-  }
-
-  // Everything is OK.
-  return true;
+module.exports = (m, u, t) => {
+	message = m;
+	ua = u;
+	transport = t;
+
+	for (const check of all) {
+		if (check() === false) {
+			return false;
+		}
+	}
+
+	if (message instanceof SIPMessage.IncomingRequest) {
+		for (const check of requests) {
+			if (check() === false) {
+				return false;
+			}
+		}
+	} else if (message instanceof SIPMessage.IncomingResponse) {
+		for (const check of responses) {
+			if (check() === false) {
+				return false;
+			}
+		}
+	}
+
+	// Everything is OK.
+	return true;
 };

-
 /*
  * Sanity Check for incoming Messages
  *
@@ -88,181 +73,169 @@ module.exports = (m, u, t) =>
  */

 // Sanity Check functions for requests.
-function rfc3261_8_2_2_1()
-{
-  if (message.s('to').uri.scheme !== 'sip')
-  {
-    reply(416);
-
-    return false;
-  }
+function rfc3261_8_2_2_1() {
+	if (message.s('to').uri.scheme !== 'sip') {
+		reply(416);
+
+		return false;
+	}
 }

-function rfc3261_16_3_4()
-{
-  if (!message.to_tag)
-  {
-    if (message.call_id.substr(0, 5) === ua.configuration.jssip_id)
-    {
-      reply(482);
-
-      return false;
-    }
-  }
+function rfc3261_16_3_4() {
+	if (!message.to_tag) {
+		if (message.call_id.substr(0, 5) === ua.configuration.jssip_id) {
+			reply(482);
+
+			return false;
+		}
+	}
 }

-function rfc3261_18_3_request()
-{
-  const len = Utils.str_utf8_length(message.body);
-  const contentLength = message.getHeader('content-length');
+function rfc3261_18_3_request() {
+	const len = Utils.str_utf8_length(message.body);
+	const contentLength = message.getHeader('content-length');

-  if (len < contentLength)
-  {
-    reply(400);
+	if (len < contentLength) {
+		reply(400);

-    return false;
-  }
+		return false;
+	}
 }

-function rfc3261_8_2_2_2()
-{
-  const fromTag = message.from_tag;
-  const call_id = message.call_id;
-  const cseq = message.cseq;
-  let tr;
-
-  // Accept any in-dialog request.
-  if (message.to_tag)
-  {
-    return;
-  }
-
-  // INVITE request.
-  if (message.method === JsSIP_C.INVITE)
-  {
-    // If the branch matches the key of any IST then assume it is a retransmission
-    // and ignore the INVITE.
-    // TODO: we should reply the last response.
-    if (ua._transactions.ist[message.via_branch])
-    {
-      return false;
-    }
-    // Otherwise check whether it is a merged request.
-    else
-    {
-      for (const transaction in ua._transactions.ist)
-      {
-        if (Object.prototype.hasOwnProperty.call(ua._transactions.ist, transaction))
-        {
-          tr = ua._transactions.ist[transaction];
-          if (tr.request.from_tag === fromTag &&
-              tr.request.call_id === call_id &&
-              tr.request.cseq === cseq)
-          {
-            reply(482);
-
-            return false;
-          }
-        }
-      }
-    }
-  }
-
-  // Non INVITE request.
-
-  // If the branch matches the key of any NIST then assume it is a retransmission
-  // and ignore the request.
-  // TODO: we should reply the last response.
-  else if (ua._transactions.nist[message.via_branch])
-  {
-    return false;
-  }
-
-  // Otherwise check whether it is a merged request.
-  else
-  {
-    for (const transaction in ua._transactions.nist)
-    {
-      if (Object.prototype.hasOwnProperty.call(ua._transactions.nist, transaction))
-      {
-        tr = ua._transactions.nist[transaction];
-        if (tr.request.from_tag === fromTag &&
-            tr.request.call_id === call_id &&
-            tr.request.cseq === cseq)
-        {
-          reply(482);
-
-          return false;
-        }
-      }
-    }
-  }
+function rfc3261_8_2_2_2() {
+	const fromTag = message.from_tag;
+	const call_id = message.call_id;
+	const cseq = message.cseq;
+	let tr;
+
+	// Accept any in-dialog request.
+	if (message.to_tag) {
+		return;
+	}
+
+	// INVITE request.
+	if (message.method === JsSIP_C.INVITE) {
+		// If the branch matches the key of any IST then assume it is a retransmission
+		// and ignore the INVITE.
+		// TODO: we should reply the last response.
+		if (ua._transactions.ist[message.via_branch]) {
+			return false;
+		}
+		// Otherwise check whether it is a merged request.
+		else {
+			for (const transaction in ua._transactions.ist) {
+				if (
+					Object.prototype.hasOwnProperty.call(
+						ua._transactions.ist,
+						transaction
+					)
+				) {
+					tr = ua._transactions.ist[transaction];
+					if (
+						tr.request.from_tag === fromTag &&
+						tr.request.call_id === call_id &&
+						tr.request.cseq === cseq
+					) {
+						reply(482);
+
+						return false;
+					}
+				}
+			}
+		}
+	}
+
+	// Non INVITE request.
+
+	// If the branch matches the key of any NIST then assume it is a retransmission
+	// and ignore the request.
+	// TODO: we should reply the last response.
+	else if (ua._transactions.nist[message.via_branch]) {
+		return false;
+	}
+
+	// Otherwise check whether it is a merged request.
+	else {
+		for (const transaction in ua._transactions.nist) {
+			if (
+				Object.prototype.hasOwnProperty.call(ua._transactions.nist, transaction)
+			) {
+				tr = ua._transactions.nist[transaction];
+				if (
+					tr.request.from_tag === fromTag &&
+					tr.request.call_id === call_id &&
+					tr.request.cseq === cseq
+				) {
+					reply(482);
+
+					return false;
+				}
+			}
+		}
+	}
 }

 // Sanity Check functions for responses.
-function rfc3261_8_1_3_3()
-{
-  if (message.getHeaders('via').length > 1)
-  {
-    logger.debug('more than one Via header field present in the response, dropping the response');
-
-    return false;
-  }
+function rfc3261_8_1_3_3() {
+	if (message.getHeaders('via').length > 1) {
+		logger.debug(
+			'more than one Via header field present in the response, dropping the response'
+		);
+
+		return false;
+	}
 }

-function rfc3261_18_3_response()
-{
-  const len = Utils.str_utf8_length(message.body), contentLength = message.getHeader('content-length');
+function rfc3261_18_3_response() {
+	const len = Utils.str_utf8_length(message.body),
+		contentLength = message.getHeader('content-length');

-  if (len < contentLength)
-  {
-    logger.debug('message body length is lower than the value in Content-Length header field, dropping the response');
+	if (len < contentLength) {
+		logger.debug(
+			'message body length is lower than the value in Content-Length header field, dropping the response'
+		);

-    return false;
-  }
+		return false;
+	}
 }

 // Sanity Check functions for requests and responses.
-function minimumHeaders()
-{
-  const mandatoryHeaders = [ 'from', 'to', 'call_id', 'cseq', 'via' ];
-
-  for (const header of mandatoryHeaders)
-  {
-    if (!message.hasHeader(header))
-    {
-      logger.debug(`missing mandatory header field : ${header}, dropping the response`);
-
-      return false;
-    }
-  }
+function minimumHeaders() {
+	const mandatoryHeaders = ['from', 'to', 'call_id', 'cseq', 'via'];
+
+	for (const header of mandatoryHeaders) {
+		if (!message.hasHeader(header)) {
+			logger.debug(
+				`missing mandatory header field : ${header}, dropping the response`
+			);
+
+			return false;
+		}
+	}
 }

 // Reply.
-function reply(status_code)
-{
-  const vias = message.getHeaders('via');
+function reply(status_code) {
+	const vias = message.getHeaders('via');

-  let to;
-  let response = `SIP/2.0 ${status_code} ${JsSIP_C.REASON_PHRASE[status_code]}\r\n`;
+	let to;
+	let response = `SIP/2.0 ${status_code} ${JsSIP_C.REASON_PHRASE[status_code]}\r\n`;

-  for (const via of vias)
-  {
-    response += `Via: ${via}\r\n`;
-  }
+	for (const via of vias) {
+		response += `Via: ${via}\r\n`;
+	}

-  to = message.getHeader('To');
+	to = message.getHeader('To');

-  if (!message.to_tag)
-  {
-    to += `;tag=${Utils.newTag()}`;
-  }
+	if (!message.to_tag) {
+		to += `;tag=${Utils.newTag()}`;
+	}

-  response += `To: ${to}\r\n`;
-  response += `From: ${message.getHeader('From')}\r\n`;
-  response += `Call-ID: ${message.call_id}\r\n`;
-  response += `CSeq: ${message.cseq} ${message.method}\r\n`;
-  response += '\r\n';
+	response += `To: ${to}\r\n`;
+	response += `From: ${message.getHeader('From')}\r\n`;
+	response += `Call-ID: ${message.call_id}\r\n`;
+	response += `CSeq: ${message.cseq} ${message.method}\r\n`;
+	response += '\r\n';

-  transport.send(response);
+	transport.send(response);
 }
diff --git a/test/include/common.js b/test/include/common.js
index 1033686..85a9094 100644
--- a/test/include/common.js
+++ b/test/include/common.js
@@ -1,20 +1,18 @@
 /* eslint no-console: 0*/

 // Show uncaught errors.
-process.on('uncaughtException', function(error)
-{
-  console.error('uncaught exception:');
-  console.error(error.stack);
-  process.exit(1);
+process.on('uncaughtException', function (error) {
+	console.error('uncaught exception:');
+	console.error(error.stack);
+	process.exit(1);
 });

 // Define global.WebSocket.
-global.WebSocket = function()
-{
-  this.close = function() {};
+global.WebSocket = function () {
+	this.close = function () {};
 };

 // Define global.navigator for bowser module.
 global.navigator = {
-  userAgent : ''
+	userAgent: '',
 };
diff --git a/test/include/loopSocket.js b/test/include/loopSocket.js
index 9a92d9a..c2a68dc 100644
--- a/test/include/loopSocket.js
+++ b/test/include/loopSocket.js
@@ -1,48 +1,42 @@
 // LoopSocket send message itself.
 // Used P2P logic: message call-id is modified in each leg.
-module.exports = class LoopSocket
-{
-  constructor()
-  {
-    this.url = 'ws://localhost:12345';
-    this.via_transport = 'WS';
-    this.sip_uri = 'sip:localhost:12345;transport=ws';
-  }
-
-  connect()
-  {
-    setTimeout(() => { this.onconnect(); }, 0);
-  }
-
-  disconnect()
-  {
-  }
-
-  send(message)
-  {
-    const message2 = this._modifyCallId(message);
-
-    setTimeout(() => { this.ondata(message2); }, 0);
-
-    return true;
-  }
-
-  // Call-ID: add or drop word '_second'.
-  _modifyCallId(message)
-  {
-    const ixBegin = message.indexOf('Call-ID');
-    const ixEnd = message.indexOf('\r', ixBegin);
-    let callId = message.substring(ixBegin+9, ixEnd);
-
-    if (callId.endsWith('_second'))
-    {
-      callId = callId.substring(0, callId.length - 7);
-    }
-    else
-    {
-      callId += '_second';
-    }
-
-    return `${message.substring(0, ixBegin)}Call-ID: ${callId}${message.substring(ixEnd)}`;
-  }
+module.exports = class LoopSocket {
+	constructor() {
+		this.url = 'ws://localhost:12345';
+		this.via_transport = 'WS';
+		this.sip_uri = 'sip:localhost:12345;transport=ws';
+	}
+
+	connect() {
+		setTimeout(() => {
+			this.onconnect();
+		}, 0);
+	}
+
+	disconnect() {}
+
+	send(message) {
+		const message2 = this._modifyCallId(message);
+
+		setTimeout(() => {
+			this.ondata(message2);
+		}, 0);
+
+		return true;
+	}
+
+	// Call-ID: add or drop word '_second'.
+	_modifyCallId(message) {
+		const ixBegin = message.indexOf('Call-ID');
+		const ixEnd = message.indexOf('\r', ixBegin);
+		let callId = message.substring(ixBegin + 9, ixEnd);
+
+		if (callId.endsWith('_second')) {
+			callId = callId.substring(0, callId.length - 7);
+		} else {
+			callId += '_second';
+		}
+
+		return `${message.substring(0, ixBegin)}Call-ID: ${callId}${message.substring(ixEnd)}`;
+	}
 };
diff --git a/test/include/testUA.js b/test/include/testUA.js
index 71caf67..2e426a5 100644
--- a/test/include/testUA.js
+++ b/test/include/testUA.js
@@ -1,54 +1,54 @@
 module.exports = {
-  SOCKET_DESCRIPTION : {
-    'via_transport' : 'WS',
-    'sip_uri'       : 'sip:localhost:12345;transport=ws',
-    'url'           : 'ws://localhost:12345'
-  },
+	SOCKET_DESCRIPTION: {
+		via_transport: 'WS',
+		sip_uri: 'sip:localhost:12345;transport=ws',
+		url: 'ws://localhost:12345',
+	},

-  UA_CONFIGURATION : {
-    uri                              : 'sip:f%61keUA@jssip.net',
-    password                         : '1234ññññ',
-    display_name                     : 'Fake UA ð→€ł !!!',
-    authorization_user               : 'fakeUA',
-    instance_id                      : 'uuid:8f1fa16a-1165-4a96-8341-785b1ef24f12',
-    registrar_server                 : 'registrar.jssip.NET:6060;TRansport=TCP',
-    register_expires                 : 600,
-    register                         : false,
-    connection_recovery_min_interval : 2,
-    connection_recovery_max_interval : 30,
-    use_preloaded_route              : true,
-    no_answer_timeout                : 60000,
-    session_timers                   : true
-  },
+	UA_CONFIGURATION: {
+		uri: 'sip:f%61keUA@jssip.net',
+		password: '1234ññññ',
+		display_name: 'Fake UA ð→€ł !!!',
+		authorization_user: 'fakeUA',
+		instance_id: 'uuid:8f1fa16a-1165-4a96-8341-785b1ef24f12',
+		registrar_server: 'registrar.jssip.NET:6060;TRansport=TCP',
+		register_expires: 600,
+		register: false,
+		connection_recovery_min_interval: 2,
+		connection_recovery_max_interval: 30,
+		use_preloaded_route: true,
+		no_answer_timeout: 60000,
+		session_timers: true,
+	},

-  UA_CONFIGURATION_AFTER_START : {
-    uri                 : 'sip:fakeUA@jssip.net',
-    password            : '1234ññññ',
-    display_name        : 'Fake UA ð→€ł !!!',
-    authorization_user  : 'fakeUA',
-    instance_id         : '8f1fa16a-1165-4a96-8341-785b1ef24f12', // Without 'uuid:'.
-    registrar_server    : 'sip:registrar.jssip.net:6060;transport=tcp',
-    register_expires    : 600,
-    register            : false,
-    use_preloaded_route : true,
-    no_answer_timeout   : 60000 * 1000, // Internally converted to miliseconds.
-    session_timers      : true
-  },
+	UA_CONFIGURATION_AFTER_START: {
+		uri: 'sip:fakeUA@jssip.net',
+		password: '1234ññññ',
+		display_name: 'Fake UA ð→€ł !!!',
+		authorization_user: 'fakeUA',
+		instance_id: '8f1fa16a-1165-4a96-8341-785b1ef24f12', // Without 'uuid:'.
+		registrar_server: 'sip:registrar.jssip.net:6060;transport=tcp',
+		register_expires: 600,
+		register: false,
+		use_preloaded_route: true,
+		no_answer_timeout: 60000 * 1000, // Internally converted to miliseconds.
+		session_timers: true,
+	},

-  UA_TRANSPORT_AFTER_START : {
-    'sockets' : [ {
-      'socket' : {
-        'via_transport' : 'WS',
-        'sip_uri'       : 'sip:localhost:12345;transport=ws',
-        'url'           : 'ws://localhost:12345'
-      },
-      'weight' : 0
-    } ],
-    'recovery_options' : {
-      'min_interval' : 2,
-      'max_interval' : 30
-    }
-  }
+	UA_TRANSPORT_AFTER_START: {
+		sockets: [
+			{
+				socket: {
+					via_transport: 'WS',
+					sip_uri: 'sip:localhost:12345;transport=ws',
+					url: 'ws://localhost:12345',
+				},
+				weight: 0,
+			},
+		],
+		recovery_options: {
+			min_interval: 2,
+			max_interval: 30,
+		},
+	},
 };
-
-
diff --git a/test/test-UA-no-WebRTC.js b/test/test-UA-no-WebRTC.js
index 840d5eb..9ef14fa 100644
--- a/test/test-UA-no-WebRTC.js
+++ b/test/test-UA-no-WebRTC.js
@@ -4,81 +4,99 @@ require('./include/common');
 const testUA = require('./include/testUA');
 const JsSIP = require('../');

-
-describe('UA No WebRTC', () =>
-{
-
-  test('UA wrong configuration', () =>
-  {
-    expect(() => new JsSIP.UA({ 'lalala': 'lololo' })).toThrow(JsSIP.Exceptions.ConfigurationError);
-  });
-
-  test('UA no WS connection', () =>
-  {
-    const config = testUA.UA_CONFIGURATION;
-    const wsSocket = new JsSIP.WebSocketInterface(testUA.SOCKET_DESCRIPTION.url);
-
-    config.sockets = wsSocket;
-
-    const ua = new JsSIP.UA(config);
-
-    expect(ua instanceof (JsSIP.UA)).toBeTruthy();
-
-    ua.start();
-
-    expect(ua.contact.toString()).toBe(`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`);
-    expect(ua.contact.toString({ outbound: false, anonymous: false, foo: true })).toBe(`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`);
-    expect(ua.contact.toString({ outbound: true })).toBe(`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws;ob>`);
-    expect(ua.contact.toString({ anonymous: true })).toBe('<sip:anonymous@anonymous.invalid;transport=ws>');
-    expect(ua.contact.toString({ anonymous: true, outbound: true })).toBe('<sip:anonymous@anonymous.invalid;transport=ws;ob>');
-
-    for (const parameter in testUA.UA_CONFIGURATION_AFTER_START)
-    {
-      if (Object.prototype.hasOwnProperty.call(
-        testUA.UA_CONFIGURATION_AFTER_START, parameter))
-      {
-        switch (parameter)
-        {
-          case 'uri':
-          case 'registrar_server':
-            // eslint-disable-next-line jest/no-conditional-expect
-            expect(ua.configuration[parameter].toString()).toBe(testUA.UA_CONFIGURATION_AFTER_START[parameter], `testing parameter ${parameter}`);
-            break;
-          case 'sockets':
-            console.warn('IGNORE SOCKETS');
-            break;
-          default:
-            // eslint-disable-next-line jest/no-conditional-expect
-            expect(ua.configuration[parameter]).toBe(testUA.UA_CONFIGURATION_AFTER_START[parameter], `testing parameter ${parameter}`);
-        }
-      }
-    }
-
-    const transport = testUA.UA_TRANSPORT_AFTER_START;
-    const sockets = transport.sockets;
-    const socket = sockets[0].socket;
-
-    expect(sockets.length).toEqual(ua.transport.sockets.length);
-    expect(sockets[0].weight).toEqual(ua.transport.sockets[0].weight);
-    expect(socket.via_transport).toEqual(ua.transport.via_transport);
-    expect(socket.sip_uri).toEqual(ua.transport.sip_uri);
-    expect(socket.url).toEqual(ua.transport.url);
-
-    expect(transport.recovery_options).toEqual(ua.transport.recovery_options);
-
-    ua.sendMessage('test', 'FAIL WITH CONNECTION_ERROR PLEASE', {
-      eventHandlers : {
-        failed : function(e)
-        {
-          expect(e.cause).toEqual(JsSIP.C.causes.CONNECTION_ERROR);
-        }
-      }
-    });
-
-    expect(
-      () => ua.sendMessage('sip:ibc@iñaki.ðđß', 'FAIL WITH INVALID_TARGET PLEASE')
-    ).toThrow();
-
-    ua.stop();
-  });
+describe('UA No WebRTC', () => {
+	test('UA wrong configuration', () => {
+		expect(() => new JsSIP.UA({ lalala: 'lololo' })).toThrow(
+			JsSIP.Exceptions.ConfigurationError
+		);
+	});
+
+	test('UA no WS connection', () => {
+		const config = testUA.UA_CONFIGURATION;
+		const wsSocket = new JsSIP.WebSocketInterface(
+			testUA.SOCKET_DESCRIPTION.url
+		);
+
+		config.sockets = wsSocket;
+
+		const ua = new JsSIP.UA(config);
+
+		expect(ua instanceof JsSIP.UA).toBeTruthy();
+
+		ua.start();
+
+		expect(ua.contact.toString()).toBe(
+			`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`
+		);
+		expect(
+			ua.contact.toString({ outbound: false, anonymous: false, foo: true })
+		).toBe(
+			`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`
+		);
+		expect(ua.contact.toString({ outbound: true })).toBe(
+			`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws;ob>`
+		);
+		expect(ua.contact.toString({ anonymous: true })).toBe(
+			'<sip:anonymous@anonymous.invalid;transport=ws>'
+		);
+		expect(ua.contact.toString({ anonymous: true, outbound: true })).toBe(
+			'<sip:anonymous@anonymous.invalid;transport=ws;ob>'
+		);
+
+		for (const parameter in testUA.UA_CONFIGURATION_AFTER_START) {
+			if (
+				Object.prototype.hasOwnProperty.call(
+					testUA.UA_CONFIGURATION_AFTER_START,
+					parameter
+				)
+			) {
+				switch (parameter) {
+					case 'uri':
+					case 'registrar_server': {
+						expect(ua.configuration[parameter].toString()).toBe(
+							testUA.UA_CONFIGURATION_AFTER_START[parameter],
+							`testing parameter ${parameter}`
+						);
+						break;
+					}
+					case 'sockets': {
+						console.warn('IGNORE SOCKETS');
+						break;
+					}
+					default: {
+						expect(ua.configuration[parameter]).toBe(
+							testUA.UA_CONFIGURATION_AFTER_START[parameter],
+							`testing parameter ${parameter}`
+						);
+					}
+				}
+			}
+		}
+
+		const transport = testUA.UA_TRANSPORT_AFTER_START;
+		const sockets = transport.sockets;
+		const socket = sockets[0].socket;
+
+		expect(sockets.length).toEqual(ua.transport.sockets.length);
+		expect(sockets[0].weight).toEqual(ua.transport.sockets[0].weight);
+		expect(socket.via_transport).toEqual(ua.transport.via_transport);
+		expect(socket.sip_uri).toEqual(ua.transport.sip_uri);
+		expect(socket.url).toEqual(ua.transport.url);
+
+		expect(transport.recovery_options).toEqual(ua.transport.recovery_options);
+
+		ua.sendMessage('test', 'FAIL WITH CONNECTION_ERROR PLEASE', {
+			eventHandlers: {
+				failed: function (e) {
+					expect(e.cause).toEqual(JsSIP.C.causes.CONNECTION_ERROR);
+				},
+			},
+		});
+
+		expect(() =>
+			ua.sendMessage('sip:ibc@iñaki.ðđß', 'FAIL WITH INVALID_TARGET PLEASE')
+		).toThrow();
+
+		ua.stop();
+	});
 });
diff --git a/test/test-UA-subscriber-notifier.js b/test/test-UA-subscriber-notifier.js
index 62b5979..96729db 100644
--- a/test/test-UA-subscriber-notifier.js
+++ b/test/test-UA-subscriber-notifier.js
@@ -2,185 +2,174 @@ require('./include/common');
 const JsSIP = require('../');
 const LoopSocket = require('./include/loopSocket');

-describe('subscriber/notifier communication', () =>
-{
-  test('should handle subscriber/notifier communication', () => new Promise((resolve) =>
-  {
-    let eventSequence = 0;
-
-    const TARGET = 'ikq';
-    const REQUEST_URI = 'sip:ikq@example.com';
-    const CONTACT_URI = 'sip:ikq@abcdefabcdef.invalid;transport=ws';
-    const SUBSCRIBE_ACCEPT = 'application/text, text/plain';
-    const EVENT_NAME = 'weather';
-    const CONTENT_TYPE = 'text/plain';
-    const WEATHER_REQUEST = 'Please report the weather condition';
-    const WEATHER_REPORT = '+20..+24°C, no precipitation, light wind';
-
-    /**
-     * @param {JsSIP.UA} ua
-     */
-    function createSubscriber(ua)
-    {
-      const options = {
-        expires     : 3600,
-        contentType : CONTENT_TYPE,
-        params      : null
-      };
-
-      const subscriber = ua.subscribe(TARGET, EVENT_NAME, SUBSCRIBE_ACCEPT, options);
-
-      subscriber.on('active', () =>
-      {
-        // 'receive notify with subscription-state: active'
-        expect(++eventSequence).toBe(6);
-      });
-
-      subscriber.on('notify', (isFinal, notify, body, contType) =>
-      {
-        eventSequence++;
-        // 'receive notify'
-        expect(eventSequence === 7 || eventSequence === 11).toBe(true);
-
-        expect(notify.method).toBe('NOTIFY');
-        expect(notify.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'notify contact'
-        expect(body).toBe(WEATHER_REPORT); // 'notify body'
-        expect(contType).toBe(CONTENT_TYPE); // 'notify content-type'
-
-        const subsState = notify.parseHeader('subscription-state').state;
-
-        expect(subsState === 'pending' || subsState === 'active' || subsState === 'terminated').toBe(true); // 'notify subscription-state'
-
-        // After receiving the first notify, send un-subscribe.
-        if (eventSequence === 7)
-        {
-          ++eventSequence; // 'send un-subscribe'
-
-          subscriber.terminate(WEATHER_REQUEST);
-        }
-      });
-
-      subscriber.on('terminated', (terminationCode, reason, retryAfter) =>
-      {
-        expect(++eventSequence).toBe(12); // 'subscriber terminated'
-        expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED);
-        expect(reason).toBeUndefined();
-        expect(retryAfter).toBeUndefined();
-
-        ua.stop();
-      });
-
-      subscriber.on('accepted', () =>
-      {
-        expect(++eventSequence).toBe(5); // 'initial subscribe accepted'
-      });
-
-      expect(++eventSequence).toBe(2); // 'send subscribe'
-
-      subscriber.subscribe(WEATHER_REQUEST);
-    }
-
-    /**
-     * @param {JsSIP.UA} ua
-     */
-    function createNotifier(ua, subscribe)
-    {
-      const notifier = ua.notify(subscribe, CONTENT_TYPE, { pending: false });
-
-      // Receive subscribe (includes initial)
-      notifier.on('subscribe', (isUnsubscribe, subs, body, contType) =>
-      {
-        expect(subscribe.method).toBe('SUBSCRIBE');
-        expect(subscribe.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'subscribe contact'
-        expect(subscribe.getHeader('accept')).toBe(SUBSCRIBE_ACCEPT); // 'subscribe accept'
-        expect(body).toBe(WEATHER_REQUEST); // 'subscribe body'
-        expect(contType).toBe(CONTENT_TYPE); // 'subscribe content-type'
-
-        expect(++eventSequence).toBe(isUnsubscribe ? 9 : 4);
-        if (isUnsubscribe)
-        {
-          // 'send final notify'
-          notifier.terminate(WEATHER_REPORT);
-        }
-        else
-        {
-          // 'send notify'
-          notifier.notify(WEATHER_REPORT);
-        }
-      });
-
-      // Example only. Never reached.
-      notifier.on('expired', () =>
-      {
-        notifier.terminate(WEATHER_REPORT, 'timeout');
-      });
-
-      notifier.on('terminated', () =>
-      {
-        expect(++eventSequence).toBe(10); // 'notifier terminated'
-      });
-
-      notifier.start();
-    }
-
-    // Start JsSIP UA with loop socket.
-    const config =
-    {
-      sockets     : new LoopSocket(), // message sending itself, with modified Call-ID
-      uri         : REQUEST_URI,
-      contact_uri : CONTACT_URI,
-      register    : false
-    };
-
-    const ua = new JsSIP.UA(config);
-
-    // Uncomment to see SIP communication
-    // JsSIP.debug.enable('JsSIP:*');
-
-    ua.on('newSubscribe', (e) =>
-    {
-      expect(++eventSequence).toBe(3); // 'receive initial subscribe'
-
-      const subs = e.request;
-      const ev = subs.parseHeader('event');
-
-      expect(subs.ruri.toString()).toBe(REQUEST_URI); // 'initial subscribe uri'
-      expect(ev.event).toBe(EVENT_NAME); // 'subscribe event'
-
-      if (ev.event !== EVENT_NAME)
-      {
-        subs.reply(489); // "Bad Event"
-
-        return;
-      }
-
-      const accepts = subs.getHeaders('accept');
-      const canUse = accepts && accepts.some((v) => v.includes(CONTENT_TYPE));
-
-      expect(canUse).toBe(true); // 'notifier can use subscribe accept header'
-
-      if (!canUse)
-      {
-        subs.reply(406); // "Not Acceptable"
-
-        return;
-      }
-
-      createNotifier(ua, subs);
-    });
-
-    ua.on('connected', () =>
-    {
-      expect(++eventSequence).toBe(1); // 'socket connected'
-
-      createSubscriber(ua);
-    });
-
-    ua.on('disconnected', () =>
-    {
-      resolve();
-    });
-
-    ua.start();
-  }));
+describe('subscriber/notifier communication', () => {
+	test('should handle subscriber/notifier communication', () =>
+		new Promise(resolve => {
+			let eventSequence = 0;
+
+			const TARGET = 'ikq';
+			const REQUEST_URI = 'sip:ikq@example.com';
+			const CONTACT_URI = 'sip:ikq@abcdefabcdef.invalid;transport=ws';
+			const SUBSCRIBE_ACCEPT = 'application/text, text/plain';
+			const EVENT_NAME = 'weather';
+			const CONTENT_TYPE = 'text/plain';
+			const WEATHER_REQUEST = 'Please report the weather condition';
+			const WEATHER_REPORT = '+20..+24°C, no precipitation, light wind';
+
+			/**
+			 * @param {JsSIP.UA} ua
+			 */
+			function createSubscriber(ua) {
+				const options = {
+					expires: 3600,
+					contentType: CONTENT_TYPE,
+					params: null,
+				};
+
+				const subscriber = ua.subscribe(
+					TARGET,
+					EVENT_NAME,
+					SUBSCRIBE_ACCEPT,
+					options
+				);
+
+				subscriber.on('active', () => {
+					// 'receive notify with subscription-state: active'
+					expect(++eventSequence).toBe(6);
+				});
+
+				subscriber.on('notify', (isFinal, notify, body, contType) => {
+					eventSequence++;
+					// 'receive notify'
+					expect(eventSequence === 7 || eventSequence === 11).toBe(true);
+
+					expect(notify.method).toBe('NOTIFY');
+					expect(notify.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'notify contact'
+					expect(body).toBe(WEATHER_REPORT); // 'notify body'
+					expect(contType).toBe(CONTENT_TYPE); // 'notify content-type'
+
+					const subsState = notify.parseHeader('subscription-state').state;
+
+					expect(
+						subsState === 'pending' ||
+							subsState === 'active' ||
+							subsState === 'terminated'
+					).toBe(true); // 'notify subscription-state'
+
+					// After receiving the first notify, send un-subscribe.
+					if (eventSequence === 7) {
+						++eventSequence; // 'send un-subscribe'
+
+						subscriber.terminate(WEATHER_REQUEST);
+					}
+				});
+
+				subscriber.on('terminated', (terminationCode, reason, retryAfter) => {
+					expect(++eventSequence).toBe(12); // 'subscriber terminated'
+					expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED);
+					expect(reason).toBeUndefined();
+					expect(retryAfter).toBeUndefined();
+
+					ua.stop();
+				});
+
+				subscriber.on('accepted', () => {
+					expect(++eventSequence).toBe(5); // 'initial subscribe accepted'
+				});
+
+				expect(++eventSequence).toBe(2); // 'send subscribe'
+
+				subscriber.subscribe(WEATHER_REQUEST);
+			}
+
+			/**
+			 * @param {JsSIP.UA} ua
+			 */
+			function createNotifier(ua, subscribe) {
+				const notifier = ua.notify(subscribe, CONTENT_TYPE, { pending: false });
+
+				// Receive subscribe (includes initial)
+				notifier.on('subscribe', (isUnsubscribe, subs, body, contType) => {
+					expect(subscribe.method).toBe('SUBSCRIBE');
+					expect(subscribe.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'subscribe contact'
+					expect(subscribe.getHeader('accept')).toBe(SUBSCRIBE_ACCEPT); // 'subscribe accept'
+					expect(body).toBe(WEATHER_REQUEST); // 'subscribe body'
+					expect(contType).toBe(CONTENT_TYPE); // 'subscribe content-type'
+
+					expect(++eventSequence).toBe(isUnsubscribe ? 9 : 4);
+					if (isUnsubscribe) {
+						// 'send final notify'
+						notifier.terminate(WEATHER_REPORT);
+					} else {
+						// 'send notify'
+						notifier.notify(WEATHER_REPORT);
+					}
+				});
+
+				// Example only. Never reached.
+				notifier.on('expired', () => {
+					notifier.terminate(WEATHER_REPORT, 'timeout');
+				});
+
+				notifier.on('terminated', () => {
+					expect(++eventSequence).toBe(10); // 'notifier terminated'
+				});
+
+				notifier.start();
+			}
+
+			// Start JsSIP UA with loop socket.
+			const config = {
+				sockets: new LoopSocket(), // message sending itself, with modified Call-ID
+				uri: REQUEST_URI,
+				contact_uri: CONTACT_URI,
+				register: false,
+			};
+
+			const ua = new JsSIP.UA(config);
+
+			// Uncomment to see SIP communication
+			// JsSIP.debug.enable('JsSIP:*');
+
+			ua.on('newSubscribe', e => {
+				expect(++eventSequence).toBe(3); // 'receive initial subscribe'
+
+				const subs = e.request;
+				const ev = subs.parseHeader('event');
+
+				expect(subs.ruri.toString()).toBe(REQUEST_URI); // 'initial subscribe uri'
+				expect(ev.event).toBe(EVENT_NAME); // 'subscribe event'
+
+				if (ev.event !== EVENT_NAME) {
+					subs.reply(489); // "Bad Event"
+
+					return;
+				}
+
+				const accepts = subs.getHeaders('accept');
+				const canUse = accepts && accepts.some(v => v.includes(CONTENT_TYPE));
+
+				expect(canUse).toBe(true); // 'notifier can use subscribe accept header'
+
+				if (!canUse) {
+					subs.reply(406); // "Not Acceptable"
+
+					return;
+				}
+
+				createNotifier(ua, subs);
+			});
+
+			ua.on('connected', () => {
+				expect(++eventSequence).toBe(1); // 'socket connected'
+
+				createSubscriber(ua);
+			});
+
+			ua.on('disconnected', () => {
+				resolve();
+			});
+
+			ua.start();
+		}));
 });
diff --git a/test/test-classes.js b/test/test-classes.js
index 02d9002..71f0f8e 100644
--- a/test/test-classes.js
+++ b/test/test-classes.js
@@ -1,156 +1,152 @@
 require('./include/common');
 const JsSIP = require('../');

-describe('URI Tests', () =>
-{
-  test('new URI', () =>
-  {
-    const uri = new JsSIP.URI(null, 'alice', 'jssip.net', 6060);
+describe('URI Tests', () => {
+	test('new URI', () => {
+		const uri = new JsSIP.URI(null, 'alice', 'jssip.net', 6060);

-    expect(uri.scheme).toBe('sip');
-    expect(uri.user).toBe('alice');
-    expect(uri.host).toBe('jssip.net');
-    expect(uri.port).toBe(6060);
-    expect(uri.toString()).toBe('sip:alice@jssip.net:6060');
-    expect(uri.toAor()).toBe('sip:alice@jssip.net');
-    expect(uri.toAor(false)).toBe('sip:alice@jssip.net');
-    expect(uri.toAor(true)).toBe('sip:alice@jssip.net:6060');
-
-    uri.scheme = 'SIPS';
-    expect(uri.scheme).toBe('sips');
-    expect(uri.toAor()).toBe('sips:alice@jssip.net');
-    uri.scheme = 'sip';
-
-    uri.user = 'Iñaki ðđ';
-    expect(uri.user).toBe('Iñaki ðđ');
-    expect(uri.toString()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net:6060');
-    expect(uri.toAor()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net');
-
-    uri.user = '%61lice';
-    expect(uri.toAor()).toBe('sip:alice@jssip.net');
-
-    uri.user = null;
-    expect(uri.user).toBeNull();
-    expect(uri.toAor()).toBe('sip:jssip.net');
-    uri.user = 'alice';
-
-    expect(() =>
-    {
-      uri.host = null;
-    }).toThrow(TypeError);
-
-    expect(() =>
-    {
-      uri.host = { bar: 'foo' };
-    }).toThrow(TypeError);
-
-    expect(uri.host).toBe('jssip.net');
-
-    uri.host = 'VERSATICA.com';
-    expect(uri.host).toBe('versatica.com');
-    uri.host = 'jssip.net';
-
-    uri.port = null;
-    expect(uri.port).toBeNull();
-
-    uri.port = undefined;
-    expect(uri.port).toBeNull();
-
-    uri.port = 'ABCD'; // Should become null.
-    expect(uri.toString()).toBe('sip:alice@jssip.net');
-
-    uri.port = '123ABCD'; // Should become 123.
-    expect(uri.toString()).toBe('sip:alice@jssip.net:123');
-
-    uri.port = 0;
-    expect(uri.port).toBe(0);
-    expect(uri.toString()).toBe('sip:alice@jssip.net:0');
-    uri.port = null;
-
-    expect(uri.hasParam('foo')).toBe(false);
-
-    uri.setParam('Foo', null);
-    expect(uri.hasParam('FOO')).toBe(true);
-
-    uri.setParam('Baz', 123);
-    expect(uri.getParam('baz')).toBe('123');
-    expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123');
-
-    uri.setParam('zero', 0);
-    expect(uri.hasParam('ZERO')).toBe(true);
-    expect(uri.getParam('ZERO')).toBe('0');
-    expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123;zero=0');
-    expect(uri.deleteParam('ZERO')).toBe('0');
-
-    expect(uri.deleteParam('baZ')).toBe('123');
-    expect(uri.deleteParam('NOO')).toBeUndefined();
-    expect(uri.toString()).toBe('sip:alice@jssip.net;foo');
-
-    uri.clearParams();
-    expect(uri.toString()).toBe('sip:alice@jssip.net');
-
-    expect(uri.hasHeader('foo')).toBe(false);
-
-    uri.setHeader('Foo', 'LALALA');
-    expect(uri.hasHeader('FOO')).toBe(true);
-    expect(uri.getHeader('FOO')).toEqual([ 'LALALA' ]);
-    expect(uri.toString()).toBe('sip:alice@jssip.net?Foo=LALALA');
-
-    uri.setHeader('bAz', [ 'ABC-1', 'ABC-2' ]);
-    expect(uri.getHeader('baz')).toEqual([ 'ABC-1', 'ABC-2' ]);
-    expect(uri.toString()).toBe('sip:alice@jssip.net?Foo=LALALA&Baz=ABC-1&Baz=ABC-2');
-
-    expect(uri.deleteHeader('baZ')).toEqual([ 'ABC-1', 'ABC-2' ]);
-    expect(uri.deleteHeader('NOO')).toBeUndefined();
+		expect(uri.scheme).toBe('sip');
+		expect(uri.user).toBe('alice');
+		expect(uri.host).toBe('jssip.net');
+		expect(uri.port).toBe(6060);
+		expect(uri.toString()).toBe('sip:alice@jssip.net:6060');
+		expect(uri.toAor()).toBe('sip:alice@jssip.net');
+		expect(uri.toAor(false)).toBe('sip:alice@jssip.net');
+		expect(uri.toAor(true)).toBe('sip:alice@jssip.net:6060');

-    uri.clearHeaders();
-    expect(uri.toString()).toBe('sip:alice@jssip.net');
+		uri.scheme = 'SIPS';
+		expect(uri.scheme).toBe('sips');
+		expect(uri.toAor()).toBe('sips:alice@jssip.net');
+		uri.scheme = 'sip';

-    const uri2 = uri.clone();
+		uri.user = 'Iñaki ðđ';
+		expect(uri.user).toBe('Iñaki ðđ');
+		expect(uri.toString()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net:6060');
+		expect(uri.toAor()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net');

-    expect(uri2.toString()).toBe(uri.toString());
-    uri2.user = 'popo';
-    expect(uri2.user).toBe('popo');
-    expect(uri.user).toBe('alice');
-  });
+		uri.user = '%61lice';
+		expect(uri.toAor()).toBe('sip:alice@jssip.net');
+
+		uri.user = null;
+		expect(uri.user).toBeNull();
+		expect(uri.toAor()).toBe('sip:jssip.net');
+		uri.user = 'alice';
+
+		expect(() => {
+			uri.host = null;
+		}).toThrow(TypeError);
+
+		expect(() => {
+			uri.host = { bar: 'foo' };
+		}).toThrow(TypeError);
+
+		expect(uri.host).toBe('jssip.net');
+
+		uri.host = 'VERSATICA.com';
+		expect(uri.host).toBe('versatica.com');
+		uri.host = 'jssip.net';
+
+		uri.port = null;
+		expect(uri.port).toBeNull();
+
+		uri.port = undefined;
+		expect(uri.port).toBeNull();
+
+		uri.port = 'ABCD'; // Should become null.
+		expect(uri.toString()).toBe('sip:alice@jssip.net');
+
+		uri.port = '123ABCD'; // Should become 123.
+		expect(uri.toString()).toBe('sip:alice@jssip.net:123');
+
+		uri.port = 0;
+		expect(uri.port).toBe(0);
+		expect(uri.toString()).toBe('sip:alice@jssip.net:0');
+		uri.port = null;
+
+		expect(uri.hasParam('foo')).toBe(false);
+
+		uri.setParam('Foo', null);
+		expect(uri.hasParam('FOO')).toBe(true);
+
+		uri.setParam('Baz', 123);
+		expect(uri.getParam('baz')).toBe('123');
+		expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123');
+
+		uri.setParam('zero', 0);
+		expect(uri.hasParam('ZERO')).toBe(true);
+		expect(uri.getParam('ZERO')).toBe('0');
+		expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123;zero=0');
+		expect(uri.deleteParam('ZERO')).toBe('0');
+
+		expect(uri.deleteParam('baZ')).toBe('123');
+		expect(uri.deleteParam('NOO')).toBeUndefined();
+		expect(uri.toString()).toBe('sip:alice@jssip.net;foo');
+
+		uri.clearParams();
+		expect(uri.toString()).toBe('sip:alice@jssip.net');
+
+		expect(uri.hasHeader('foo')).toBe(false);
+
+		uri.setHeader('Foo', 'LALALA');
+		expect(uri.hasHeader('FOO')).toBe(true);
+		expect(uri.getHeader('FOO')).toEqual(['LALALA']);
+		expect(uri.toString()).toBe('sip:alice@jssip.net?Foo=LALALA');
+
+		uri.setHeader('bAz', ['ABC-1', 'ABC-2']);
+		expect(uri.getHeader('baz')).toEqual(['ABC-1', 'ABC-2']);
+		expect(uri.toString()).toBe(
+			'sip:alice@jssip.net?Foo=LALALA&Baz=ABC-1&Baz=ABC-2'
+		);
+
+		expect(uri.deleteHeader('baZ')).toEqual(['ABC-1', 'ABC-2']);
+		expect(uri.deleteHeader('NOO')).toBeUndefined();
+
+		uri.clearHeaders();
+		expect(uri.toString()).toBe('sip:alice@jssip.net');
+
+		const uri2 = uri.clone();
+
+		expect(uri2.toString()).toBe(uri.toString());
+		uri2.user = 'popo';
+		expect(uri2.user).toBe('popo');
+		expect(uri.user).toBe('alice');
+	});
 });

-describe('NameAddr Tests', () =>
-{
-  test('new NameAddr', () =>
-  {
-    const uri = new JsSIP.URI('sip', 'alice', 'jssip.net');
-    const name = new JsSIP.NameAddrHeader(uri, 'Alice æßð');
+describe('NameAddr Tests', () => {
+	test('new NameAddr', () => {
+		const uri = new JsSIP.URI('sip', 'alice', 'jssip.net');
+		const name = new JsSIP.NameAddrHeader(uri, 'Alice æßð');

-    expect(name.display_name).toBe('Alice æßð');
-    expect(name.toString()).toBe('"Alice æßð" <sip:alice@jssip.net>');
+		expect(name.display_name).toBe('Alice æßð');
+		expect(name.toString()).toBe('"Alice æßð" <sip:alice@jssip.net>');

-    name.display_name = null;
-    expect(name.toString()).toBe('<sip:alice@jssip.net>');
+		name.display_name = null;
+		expect(name.toString()).toBe('<sip:alice@jssip.net>');

-    name.display_name = 0;
-    expect(name.toString()).toBe('"0" <sip:alice@jssip.net>');
+		name.display_name = 0;
+		expect(name.toString()).toBe('"0" <sip:alice@jssip.net>');

-    name.display_name = '';
-    expect(name.toString()).toBe('<sip:alice@jssip.net>');
+		name.display_name = '';
+		expect(name.toString()).toBe('<sip:alice@jssip.net>');

-    name.setParam('Foo', null);
-    expect(name.hasParam('FOO')).toBe(true);
+		name.setParam('Foo', null);
+		expect(name.hasParam('FOO')).toBe(true);

-    name.setParam('Baz', 123);
-    expect(name.getParam('baz')).toBe('123');
-    expect(name.toString()).toBe('<sip:alice@jssip.net>;foo;baz=123');
+		name.setParam('Baz', 123);
+		expect(name.getParam('baz')).toBe('123');
+		expect(name.toString()).toBe('<sip:alice@jssip.net>;foo;baz=123');

-    expect(name.deleteParam('bAz')).toBe('123');
+		expect(name.deleteParam('bAz')).toBe('123');

-    name.clearParams();
-    expect(name.toString()).toBe('<sip:alice@jssip.net>');
+		name.clearParams();
+		expect(name.toString()).toBe('<sip:alice@jssip.net>');

-    const name2 = name.clone();
+		const name2 = name.clone();

-    expect(name2.toString()).toBe(name.toString());
-    name2.display_name = '@ł€';
-    expect(name2.display_name).toBe('@ł€');
-    expect(name.user).toBeUndefined();
-  });
+		expect(name2.toString()).toBe(name.toString());
+		name2.display_name = '@ł€';
+		expect(name2.display_name).toBe('@ł€');
+		expect(name.user).toBeUndefined();
+	});
 });
diff --git a/test/test-digestAuthentication.js b/test/test-digestAuthentication.js
index 83d4a46..7acf27c 100644
--- a/test/test-digestAuthentication.js
+++ b/test/test-digestAuthentication.js
@@ -4,149 +4,134 @@ const DigestAuthentication = require('../src/DigestAuthentication.js');
 // Results of this tests originally obtained from RFC 2617 and:
 // 'https://pernau.at/kd/sipdigest.php'

-describe('DigestAuthentication', () =>
-{
-  test('parse no auth testrealm@host.com -RFC 2617-', () =>
-  {
-    const method = 'GET';
-    const ruri = '/dir/index.html';
-    const cnonce = '0a4f113b';
-    const credentials =
-    {
-      username : 'Mufasa',
-      password : 'Circle Of Life',
-      realm    : 'testrealm@host.com',
-      ha1      : null
-    };
-    const challenge =
-    {
-      algorithm : 'MD5',
-      realm     : 'testrealm@host.com',
-      nonce     : 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
-      opaque    : '5ccc069c403ebaf9f0171e9517f40e41',
-      stale     : null,
-      qop       : 'auth'
-    };
-
-    const digest = new DigestAuthentication(credentials);
-
-    digest.authenticate({ method, ruri }, challenge, cnonce);
-
-    expect(digest._response).toBe('6629fae49393a05397450978507c4ef1');
-  });
-
-  test('digest authenticate qop = null', () =>
-  {
-    const method = 'REGISTER';
-    const ruri = 'sip:testrealm@host.com';
-    const credentials = {
-      username : 'testuser',
-      password : 'testpassword',
-      realm    : 'testrealm@host.com',
-      ha1      : null
-    };
-    const challenge =
-    {
-      algorithm : 'MD5',
-      realm     : 'testrealm@host.com',
-      nonce     : '5a071f75353f667787615249c62dcc7b15a4828f',
-      opaque    : null,
-      stale     : null,
-      qop       : null
-    };
-
-    const digest = new DigestAuthentication(credentials);
-
-    digest.authenticate({ method, ruri }, challenge);
-
-    expect(digest._response).toBe('f99e05f591f147facbc94ff23b4b1dee');
-  });
-
-  test('digest authenticate qop = auth', () =>
-  {
-    const method = 'REGISTER';
-    const ruri = 'sip:testrealm@host.com';
-    const cnonce = '0a4f113b';
-    const credentials =
-    {
-      username : 'testuser',
-      password : 'testpassword',
-      realm    : 'testrealm@host.com',
-      ha1      : null
-    };
-    const challenge =
-    {
-      algorithm : 'MD5',
-      realm     : 'testrealm@host.com',
-      nonce     : '5a071f75353f667787615249c62dcc7b15a4828f',
-      opaque    : null,
-      stale     : null,
-      qop       : 'auth'
-    };
-
-    const digest = new DigestAuthentication(credentials);
-
-    digest.authenticate({ method, ruri }, challenge, cnonce);
-
-    expect(digest._response).toBe('a69b9c2ea0dea1437a21df6ddc9b05e4');
-  });
-
-  test('digest authenticate qop = auth-int and empty body', () =>
-  {
-    const method = 'REGISTER';
-    const ruri = 'sip:testrealm@host.com';
-    const cnonce = '0a4f113b';
-    const credentials =
-    {
-      username : 'testuser',
-      password : 'testpassword',
-      realm    : 'testrealm@host.com',
-      ha1      : null
-    };
-    const challenge =
-    {
-      algorithm : 'MD5',
-      realm     : 'testrealm@host.com',
-      nonce     : '5a071f75353f667787615249c62dcc7b15a4828f',
-      opaque    : null,
-      stale     : null,
-      qop       : 'auth-int'
-    };
-
-    const digest = new DigestAuthentication(credentials);
-
-    digest.authenticate({ method, ruri }, challenge, cnonce);
-
-    expect(digest._response).toBe('82b3cab8b1c4df404434db6a0581650c');
-  });
-
-  test('digest authenticate qop = auth-int and non-empty body', () =>
-  {
-    const method = 'REGISTER';
-    const ruri = 'sip:testrealm@host.com';
-    const body = 'TEST BODY';
-    const cnonce = '0a4f113b';
-    const credentials =
-    {
-      username : 'testuser',
-      password : 'testpassword',
-      realm    : 'testrealm@host.com',
-      ha1      : null
-    };
-    const challenge =
-    {
-      algorithm : 'MD5',
-      realm     : 'testrealm@host.com',
-      nonce     : '5a071f75353f667787615249c62dcc7b15a4828f',
-      opaque    : null,
-      stale     : null,
-      qop       : 'auth-int'
-    };
-
-    const digest = new DigestAuthentication(credentials);
-
-    digest.authenticate({ method, ruri, body }, challenge, cnonce);
-
-    expect(digest._response).toBe('7bf0e9de3fbb5da121974509d617f532');
-  });
+describe('DigestAuthentication', () => {
+	test('parse no auth testrealm@host.com -RFC 2617-', () => {
+		const method = 'GET';
+		const ruri = '/dir/index.html';
+		const cnonce = '0a4f113b';
+		const credentials = {
+			username: 'Mufasa',
+			password: 'Circle Of Life',
+			realm: 'testrealm@host.com',
+			ha1: null,
+		};
+		const challenge = {
+			algorithm: 'MD5',
+			realm: 'testrealm@host.com',
+			nonce: 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
+			opaque: '5ccc069c403ebaf9f0171e9517f40e41',
+			stale: null,
+			qop: 'auth',
+		};
+
+		const digest = new DigestAuthentication(credentials);
+
+		digest.authenticate({ method, ruri }, challenge, cnonce);
+
+		expect(digest._response).toBe('6629fae49393a05397450978507c4ef1');
+	});
+
+	test('digest authenticate qop = null', () => {
+		const method = 'REGISTER';
+		const ruri = 'sip:testrealm@host.com';
+		const credentials = {
+			username: 'testuser',
+			password: 'testpassword',
+			realm: 'testrealm@host.com',
+			ha1: null,
+		};
+		const challenge = {
+			algorithm: 'MD5',
+			realm: 'testrealm@host.com',
+			nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+			opaque: null,
+			stale: null,
+			qop: null,
+		};
+
+		const digest = new DigestAuthentication(credentials);
+
+		digest.authenticate({ method, ruri }, challenge);
+
+		expect(digest._response).toBe('f99e05f591f147facbc94ff23b4b1dee');
+	});
+
+	test('digest authenticate qop = auth', () => {
+		const method = 'REGISTER';
+		const ruri = 'sip:testrealm@host.com';
+		const cnonce = '0a4f113b';
+		const credentials = {
+			username: 'testuser',
+			password: 'testpassword',
+			realm: 'testrealm@host.com',
+			ha1: null,
+		};
+		const challenge = {
+			algorithm: 'MD5',
+			realm: 'testrealm@host.com',
+			nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+			opaque: null,
+			stale: null,
+			qop: 'auth',
+		};
+
+		const digest = new DigestAuthentication(credentials);
+
+		digest.authenticate({ method, ruri }, challenge, cnonce);
+
+		expect(digest._response).toBe('a69b9c2ea0dea1437a21df6ddc9b05e4');
+	});
+
+	test('digest authenticate qop = auth-int and empty body', () => {
+		const method = 'REGISTER';
+		const ruri = 'sip:testrealm@host.com';
+		const cnonce = '0a4f113b';
+		const credentials = {
+			username: 'testuser',
+			password: 'testpassword',
+			realm: 'testrealm@host.com',
+			ha1: null,
+		};
+		const challenge = {
+			algorithm: 'MD5',
+			realm: 'testrealm@host.com',
+			nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+			opaque: null,
+			stale: null,
+			qop: 'auth-int',
+		};
+
+		const digest = new DigestAuthentication(credentials);
+
+		digest.authenticate({ method, ruri }, challenge, cnonce);
+
+		expect(digest._response).toBe('82b3cab8b1c4df404434db6a0581650c');
+	});
+
+	test('digest authenticate qop = auth-int and non-empty body', () => {
+		const method = 'REGISTER';
+		const ruri = 'sip:testrealm@host.com';
+		const body = 'TEST BODY';
+		const cnonce = '0a4f113b';
+		const credentials = {
+			username: 'testuser',
+			password: 'testpassword',
+			realm: 'testrealm@host.com',
+			ha1: null,
+		};
+		const challenge = {
+			algorithm: 'MD5',
+			realm: 'testrealm@host.com',
+			nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+			opaque: null,
+			stale: null,
+			qop: 'auth-int',
+		};
+
+		const digest = new DigestAuthentication(credentials);
+
+		digest.authenticate({ method, ruri, body }, challenge, cnonce);
+
+		expect(digest._response).toBe('7bf0e9de3fbb5da121974509d617f532');
+	});
 });
diff --git a/test/test-normalizeTarget.js b/test/test-normalizeTarget.js
index 7d48000..add8e69 100644
--- a/test/test-normalizeTarget.js
+++ b/test/test-normalizeTarget.js
@@ -1,63 +1,57 @@
 require('./include/common');
 const JsSIP = require('../');

-
-describe('normalizeTarget', () =>
-{
-  test('valid targets', () =>
-  {
-    const domain = 'jssip.net';
-
-    function test_ok(given_data, expected)
-    {
-      const uri = JsSIP.Utils.normalizeTarget(given_data, domain);
-
-      expect(uri instanceof (JsSIP.URI)).toBeTruthy();
-      expect(uri.toString()).toEqual(expected);
-    }
-
-    test_ok('%61lice', 'sip:alice@jssip.net');
-    test_ok('ALICE', 'sip:ALICE@jssip.net');
-    test_ok('alice@DOMAIN.com', 'sip:alice@domain.com');
-    test_ok('iñaki', 'sip:i%C3%B1aki@jssip.net');
-    test_ok('€€€', 'sip:%E2%82%AC%E2%82%AC%E2%82%AC@jssip.net');
-    test_ok('iñaki@aliax.net', 'sip:i%C3%B1aki@aliax.net');
-    test_ok('SIP:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
-    test_ok('SIPs:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
-    test_ok('ibc@gmail.com@aliax.net', 'sip:ibc%40gmail.com@aliax.net');
-    test_ok('alice-1:passwd', 'sip:alice-1:passwd@jssip.net');
-    test_ok('SIP:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
-    test_ok('sips:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
-    test_ok('alice-3:passwd@domain.COM', 'sip:alice-3:passwd@domain.com');
-    test_ok('SIP:alice-4:passwd@domain.COM', 'sip:alice-4:passwd@domain.com');
-    test_ok('sip:+1234@aliax.net', 'sip:+1234@aliax.net');
-    test_ok('+999', 'sip:+999@jssip.net');
-    test_ok('*999', 'sip:*999@jssip.net');
-    test_ok('#999/?:1234', 'sip:%23999/?:1234@jssip.net');
-    test_ok('tel:+12345678', 'sip:+12345678@jssip.net');
-    test_ok('tel:(+34)-944-43-89', 'sip:+349444389@jssip.net');
-    test_ok('+123.456.78-9', 'sip:+123456789@jssip.net');
-    test_ok('+ALICE-123.456.78-9', 'sip:+ALICE-123.456.78-9@jssip.net');
-  });
-
-  test('invalid targets', () =>
-  {
-    const domain = 'jssip.net';
-
-    function test_error(given_data)
-    {
-      expect(JsSIP.Utils.normalizeTarget(given_data, domain)).toBe(undefined);
-    }
-
-    test_error(null);
-    test_error(undefined);
-    test_error(NaN);
-    test_error(false);
-    test_error(true);
-    test_error('');
-    test_error('ibc@iñaki.com');
-    test_error('ibc@aliax.net;;;;;');
-
-    expect(JsSIP.Utils.normalizeTarget('alice')).toBe(undefined);
-  });
+describe('normalizeTarget', () => {
+	test('valid targets', () => {
+		const domain = 'jssip.net';
+
+		function test_ok(given_data, expected) {
+			const uri = JsSIP.Utils.normalizeTarget(given_data, domain);
+
+			expect(uri instanceof JsSIP.URI).toBeTruthy();
+			expect(uri.toString()).toEqual(expected);
+		}
+
+		test_ok('%61lice', 'sip:alice@jssip.net');
+		test_ok('ALICE', 'sip:ALICE@jssip.net');
+		test_ok('alice@DOMAIN.com', 'sip:alice@domain.com');
+		test_ok('iñaki', 'sip:i%C3%B1aki@jssip.net');
+		test_ok('€€€', 'sip:%E2%82%AC%E2%82%AC%E2%82%AC@jssip.net');
+		test_ok('iñaki@aliax.net', 'sip:i%C3%B1aki@aliax.net');
+		test_ok('SIP:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
+		test_ok('SIPs:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
+		test_ok('ibc@gmail.com@aliax.net', 'sip:ibc%40gmail.com@aliax.net');
+		test_ok('alice-1:passwd', 'sip:alice-1:passwd@jssip.net');
+		test_ok('SIP:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
+		test_ok('sips:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
+		test_ok('alice-3:passwd@domain.COM', 'sip:alice-3:passwd@domain.com');
+		test_ok('SIP:alice-4:passwd@domain.COM', 'sip:alice-4:passwd@domain.com');
+		test_ok('sip:+1234@aliax.net', 'sip:+1234@aliax.net');
+		test_ok('+999', 'sip:+999@jssip.net');
+		test_ok('*999', 'sip:*999@jssip.net');
+		test_ok('#999/?:1234', 'sip:%23999/?:1234@jssip.net');
+		test_ok('tel:+12345678', 'sip:+12345678@jssip.net');
+		test_ok('tel:(+34)-944-43-89', 'sip:+349444389@jssip.net');
+		test_ok('+123.456.78-9', 'sip:+123456789@jssip.net');
+		test_ok('+ALICE-123.456.78-9', 'sip:+ALICE-123.456.78-9@jssip.net');
+	});
+
+	test('invalid targets', () => {
+		const domain = 'jssip.net';
+
+		function test_error(given_data) {
+			expect(JsSIP.Utils.normalizeTarget(given_data, domain)).toBe(undefined);
+		}
+
+		test_error(null);
+		test_error(undefined);
+		test_error(NaN);
+		test_error(false);
+		test_error(true);
+		test_error('');
+		test_error('ibc@iñaki.com');
+		test_error('ibc@aliax.net;;;;;');
+
+		expect(JsSIP.Utils.normalizeTarget('alice')).toBe(undefined);
+	});
 });
diff --git a/test/test-parser.js b/test/test-parser.js
index 34cbef2..8970bbf 100644
--- a/test/test-parser.js
+++ b/test/test-parser.js
@@ -3,395 +3,411 @@ const JsSIP = require('../');
 const testUA = require('./include/testUA');
 const Parser = require('../src/Parser');

+describe('parser', () => {
+	test('parse URI', () => {
+		const data =
+			'SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2';
+		const uri = JsSIP.URI.parse(data);
+
+		// Parsed data.
+		expect(uri instanceof JsSIP.URI).toBeTruthy();
+		expect(uri.scheme).toBe('sip');
+		expect(uri.user).toBe('aliCE');
+		expect(uri.host).toBe('versatica.com');
+		expect(uri.port).toBe(6060);
+		expect(uri.hasParam('transport')).toBe(true);
+		expect(uri.hasParam('nooo')).toBe(false);
+		expect(uri.getParam('transport')).toBe('tcp');
+		expect(uri.getParam('foo')).toBe('ABc');
+		expect(uri.getParam('baz')).toBe(null);
+		expect(uri.getParam('nooo')).toBe(undefined);
+		expect(uri.getHeader('x-header-1')).toEqual(['AaA1', 'AAA2']);
+		expect(uri.getHeader('X-HEADER-2')).toEqual(['BbB']);
+		expect(uri.getHeader('nooo')).toBe(undefined);
+		expect(uri.toString()).toBe(
+			'sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB'
+		);
+		expect(uri.toAor()).toBe('sip:aliCE@versatica.com');
+
+		// Alter data.
+		uri.user = 'Iñaki:PASSWD';
+		expect(uri.user).toBe('Iñaki:PASSWD');
+		expect(uri.deleteParam('foo')).toBe('ABc');
+		expect(uri.deleteHeader('x-header-1')).toEqual(['AaA1', 'AAA2']);
+		expect(uri.toString()).toBe(
+			'sip:I%C3%B1aki:PASSWD@versatica.com:6060;transport=tcp;baz?X-Header-2=BbB'
+		);
+		expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+		uri.clearParams();
+		uri.clearHeaders();
+		uri.port = null;
+		expect(uri.toString()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+		expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+	});
+
+	test('parse NameAddr', () => {
+		const data =
+			' "Iñaki ðđøþ foo \\"bar\\" \\\\\\\\ \\\\ \\\\d \\\\\\\\d \\\\\' \\\\\\"sdf\\\\\\"" ' +
+			'<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+		const name = JsSIP.NameAddrHeader.parse(data);
+
+		// Parsed data.
+		expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(name.display_name).toBe(
+			'Iñaki ðđøþ foo "bar" \\\\ \\ \\d \\\\d \\\' \\"sdf\\"'
+		);
+		expect(name.hasParam('qwe')).toBe(true);
+		expect(name.hasParam('asd')).toBe(true);
+		expect(name.hasParam('nooo')).toBe(false);
+		expect(name.getParam('qwe')).toBe('QWE');
+		expect(name.getParam('asd')).toBe(null);
+
+		const uri = name.uri;
+
+		expect(uri instanceof JsSIP.URI).toBeTruthy();
+		expect(uri.scheme).toBe('sip');
+		expect(uri.user).toBe('aliCE');
+		expect(uri.host).toBe('versatica.com');
+		expect(uri.port).toBe(6060);
+		expect(uri.hasParam('transport')).toBe(true);
+		expect(uri.hasParam('nooo')).toBe(false);
+		expect(uri.getParam('transport')).toBe('tcp');
+		expect(uri.getParam('foo')).toBe('ABc');
+		expect(uri.getParam('baz')).toBe(null);
+		expect(uri.getParam('nooo')).toBe(undefined);
+		expect(uri.getHeader('x-header-1')).toEqual(['AaA1', 'AAA2']);
+		expect(uri.getHeader('X-HEADER-2')).toEqual(['BbB']);
+		expect(uri.getHeader('nooo')).toBe(undefined);
+
+		// Alter data.
+		name.display_name = 'Foo Bar';
+		expect(name.display_name).toBe('Foo Bar');
+		name.display_name = null;
+		expect(name.display_name).toBe(null);
+		expect(name.toString()).toBe(
+			'<sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB>;qwe=QWE;asd'
+		);
+		uri.user = 'Iñaki:PASSWD';
+		expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+	});
+
+	test('parse invalid NameAddr with non UTF-8 characters', () => {
+		const buffer = Buffer.from([0xc0]);
+		const data = `"${buffer.toString()}"<sip:foo@bar.com>`;
+		const name = JsSIP.NameAddrHeader.parse(data);
+
+		// Parsed data.
+		expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(name.display_name).toBe(buffer.toString());
+
+		const uri = name.uri;
+
+		expect(uri instanceof JsSIP.URI).toBeTruthy();
+		expect(uri.scheme).toBe('sip');
+		expect(uri.user).toBe('foo');
+		expect(uri.host).toBe('bar.com');
+		expect(uri.port).toBe(undefined);
+	});
+
+	test('parse NameAddr with token display_name', () => {
+		const data =
+			'Foo    Foo Bar\tBaz<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+		const name = JsSIP.NameAddrHeader.parse(data);
+
+		// Parsed data.
+		expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(name.display_name).toBe('Foo Foo Bar Baz');
+	});
+
+	test('parse NameAddr with no space between DQUOTE and LAQUOT', () => {
+		const data =
+			'"Foo"<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+		const name = JsSIP.NameAddrHeader.parse(data);
+
+		// Parsed data.
+		expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(name.display_name).toBe('Foo');
+	});
+
+	test('parse NameAddr with no display_name', () => {
+		const data =
+			'<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+		const name = JsSIP.NameAddrHeader.parse(data);
+
+		// Parsed data.
+		expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(name.display_name).toBe(undefined);
+	});
+
+	test('parse multiple Contact', () => {
+		const data =
+			'"Iñaki @ł€" <SIP:+1234@ALIAX.net;Transport=WS>;+sip.Instance="abCD", sip:bob@biloxi.COM;headerParam, <sip:DOMAIN.com:5>';
+		const contacts = JsSIP.Grammar.parse(data, 'Contact');
+
+		expect(contacts instanceof Array).toBeTruthy();
+		expect(contacts.length).toBe(3);
+		const c1 = contacts[0].parsed;
+		const c2 = contacts[1].parsed;
+		const c3 = contacts[2].parsed;
+
+		// Parsed data.
+		expect(c1 instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(c1.display_name).toBe('Iñaki @ł€');
+		expect(c1.hasParam('+sip.instance')).toBe(true);
+		expect(c1.hasParam('nooo')).toBe(false);
+		expect(c1.getParam('+SIP.instance')).toBe('"abCD"');
+		expect(c1.getParam('nooo')).toBe(undefined);
+		expect(c1.uri instanceof JsSIP.URI).toBeTruthy();
+		expect(c1.uri.scheme).toBe('sip');
+		expect(c1.uri.user).toBe('+1234');
+		expect(c1.uri.host).toBe('aliax.net');
+		expect(c1.uri.port).toBe(undefined);
+		expect(c1.uri.getParam('transport')).toBe('ws');
+		expect(c1.uri.getParam('foo')).toBe(undefined);
+		expect(c1.uri.getHeader('X-Header')).toBe(undefined);
+		expect(c1.toString()).toBe(
+			'"Iñaki @ł€" <sip:+1234@aliax.net;transport=ws>;+sip.instance="abCD"'
+		);
+
+		// Alter data.
+		c1.display_name = '€€€';
+		expect(c1.display_name).toBe('€€€');
+		c1.uri.user = '+999';
+		expect(c1.uri.user).toBe('+999');
+		c1.setParam('+sip.instance', '"zxCV"');
+		expect(c1.getParam('+SIP.instance')).toBe('"zxCV"');
+		c1.setParam('New-Param', null);
+		expect(c1.hasParam('NEW-param')).toBe(true);
+		c1.uri.setParam('New-Param', null);
+		expect(c1.toString()).toBe(
+			'"€€€" <sip:+999@aliax.net;transport=ws;new-param>;+sip.instance="zxCV";new-param'
+		);
+
+		// Parsed data.
+		expect(c2 instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(c2.display_name).toBe(undefined);
+		expect(c2.hasParam('HEADERPARAM')).toBe(true);
+		expect(c2.uri instanceof JsSIP.URI).toBeTruthy();
+		expect(c2.uri.scheme).toBe('sip');
+		expect(c2.uri.user).toBe('bob');
+		expect(c2.uri.host).toBe('biloxi.com');
+		expect(c2.uri.port).toBe(undefined);
+		expect(c2.uri.hasParam('headerParam')).toBe(false);
+		expect(c2.toString()).toBe('<sip:bob@biloxi.com>;headerparam');
+
+		// Alter data.
+		c2.display_name = '@ł€ĸłæß';
+		expect(c2.toString()).toBe('"@ł€ĸłæß" <sip:bob@biloxi.com>;headerparam');
+
+		// Parsed data.
+		expect(c3 instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(c3.displayName).toBe(undefined);
+		expect(c3.uri instanceof JsSIP.URI).toBeTruthy();
+		expect(c3.uri.scheme).toBe('sip');
+		expect(c3.uri.user).toBe(undefined);
+		expect(c3.uri.host).toBe('domain.com');
+		expect(c3.uri.port).toBe(5);
+		expect(c3.uri.hasParam('nooo')).toBe(false);
+		expect(c3.toString()).toBe('<sip:domain.com:5>');
+
+		// Alter data.
+		c3.uri.setParam('newUriParam', 'zxCV');
+		c3.setParam('newHeaderParam', 'zxCV');
+		expect(c3.toString()).toBe(
+			'<sip:domain.com:5;newuriparam=zxCV>;newheaderparam=zxCV'
+		);
+	});
+
+	test('parse Via', () => {
+		let data =
+			'SIP /  3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n  BRanch=1234;Param1=Foo;paRAM2;param3=Bar';
+		let via = JsSIP.Grammar.parse(data, 'Via');
+
+		expect(via.protocol).toBe('SIP');
+		expect(via.transport).toBe('UDP');
+		expect(via.host).toBe('[1:ab::FF]');
+		expect(via.host_type).toBe('IPv6');
+		expect(via.port).toBe(6060);
+		expect(via.branch).toBe('1234');
+		expect(via.params).toEqual({
+			param1: 'Foo',
+			param2: undefined,
+			param3: 'Bar',
+		});
+
+		data =
+			'SIP /  3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n  BRanch=1234;rport=1111;Param1=Foo;paRAM2;param3=Bar';
+		via = JsSIP.Grammar.parse(data, 'Via');
+
+		expect(via.protocol).toBe('SIP');
+		expect(via.transport).toBe('UDP');
+		expect(via.host).toBe('[1:ab::FF]');
+		expect(via.host_type).toBe('IPv6');
+		expect(via.port).toBe(6060);
+		expect(via.branch).toBe('1234');
+		expect(via.rport).toBe(1111);
+		expect(via.params).toEqual({
+			param1: 'Foo',
+			param2: undefined,
+			param3: 'Bar',
+		});
+
+		data =
+			'SIP /  3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n  BRanch=1234;rport;Param1=Foo;paRAM2;param3=Bar';
+		via = JsSIP.Grammar.parse(data, 'Via');
+
+		expect(via.protocol).toBe('SIP');
+		expect(via.transport).toBe('UDP');
+		expect(via.host).toBe('[1:ab::FF]');
+		expect(via.host_type).toBe('IPv6');
+		expect(via.port).toBe(6060);
+		expect(via.branch).toBe('1234');
+		expect(via.rport).toBe(undefined);
+		expect(via.params).toEqual({
+			param1: 'Foo',
+			param2: undefined,
+			param3: 'Bar',
+		});
+	});
+
+	test('parse CSeq', () => {
+		const data = '123456  CHICKEN';
+		const cseq = JsSIP.Grammar.parse(data, 'CSeq');
+
+		expect(cseq.value).toBe(123456);
+		expect(cseq.method).toBe('CHICKEN');
+	});
+
+	test('parse authentication challenge', () => {
+		const data =
+			'Digest realm =  "[1:ABCD::abc]", nonce =  "31d0a89ed7781ce6877de5cb032bf114", qop="AUTH,autH-INt", algorithm =  md5  ,  stale =  TRUE , opaque = "00000188"';
+		const auth = JsSIP.Grammar.parse(data, 'challenge');
+
+		expect(auth.realm).toBe('[1:ABCD::abc]');
+		expect(auth.nonce).toBe('31d0a89ed7781ce6877de5cb032bf114');
+		expect(auth.qop).toEqual(['auth', 'auth-int']);
+		expect(auth.algorithm).toBe('MD5');
+		expect(auth.stale).toBe(true);
+		expect(auth.opaque).toBe('00000188');
+	});
+
+	test('parse Event', () => {
+		const data = 'Presence;Param1=QWe;paraM2';
+		const event = JsSIP.Grammar.parse(data, 'Event');
+
+		expect(event.event).toBe('presence');
+		expect(event.params).toEqual({ param1: 'QWe', param2: undefined });
+	});
+
+	test('parse Session-Expires', () => {
+		let data, session_expires;
+
+		data = '180;refresher=uac';
+		session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
+
+		expect(session_expires.expires).toBe(180);
+		expect(session_expires.refresher).toBe('uac');
+
+		data = '210  ;   refresher  =  UAS ; foo  =  bar';
+		session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
+
+		expect(session_expires.expires).toBe(210);
+		expect(session_expires.refresher).toBe('uas');
+	});
+
+	test('parse Reason', () => {
+		let data, reason;
+
+		data = 'SIP  ; cause = 488 ; text = "Wrong SDP"';
+		reason = JsSIP.Grammar.parse(data, 'Reason');
+
+		expect(reason.protocol).toBe('sip');
+		expect(reason.cause).toBe(488);
+		expect(reason.text).toBe('Wrong SDP');
+
+		data = 'ISUP; cause=500 ; LALA = foo';
+		reason = JsSIP.Grammar.parse(data, 'Reason');
+
+		expect(reason.protocol).toBe('isup');
+		expect(reason.cause).toBe(500);
+		expect(reason.text).toBe(undefined);
+		expect(reason.params.lala).toBe('foo');
+	});
+
+	test('parse host', () => {
+		let data, parsed;
+
+		data = 'versatica.com';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+		expect(parsed.host_type).toBe('domain');
+
+		data = 'myhost123';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+		expect(parsed.host_type).toBe('domain');
+
+		data = '1.2.3.4';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+		expect(parsed.host_type).toBe('IPv4');
+
+		data = '[1:0:fF::432]';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+		expect(parsed.host_type).toBe('IPv6');
+
+		data = '1.2.3.444';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
+
+		data = 'iñaki.com';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
+
+		data = '1.2.3.bar.qwe-asd.foo';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+		expect(parsed.host_type).toBe('domain');
+
+		data = '1.2.3.4.bar.qwe-asd.foo';
+		expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+		expect(parsed.host_type).toBe('domain');
+	});
+
+	test('parse Refer-To', () => {
+		let data, parsed;
+
+		data = 'sip:alice@versatica.com';
+		expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
+		expect(parsed.uri.scheme).toBe('sip');
+		expect(parsed.uri.user).toBe('alice');
+		expect(parsed.uri.host).toBe('versatica.com');
+
+		data = '<sip:bob@versatica.com?Accept-Contact=sip:bobsdesk.versatica.com>';
+		expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
+		expect(parsed.uri.scheme).toBe('sip');
+		expect(parsed.uri.user).toBe('bob');
+		expect(parsed.uri.host).toBe('versatica.com');
+		expect(parsed.uri.hasHeader('Accept-Contact')).toBe(true);
+	});
+
+	test('parse Replaces', () => {
+		let parsed;

-describe('parser', () =>
-{
-  test('parse URI', () =>
-  {
-    const data = 'SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2';
-    const uri = JsSIP.URI.parse(data);
-
-    // Parsed data.
-    expect(uri instanceof (JsSIP.URI)).toBeTruthy();
-    expect(uri.scheme).toBe('sip');
-    expect(uri.user).toBe('aliCE');
-    expect(uri.host).toBe('versatica.com');
-    expect(uri.port).toBe(6060);
-    expect(uri.hasParam('transport')).toBe(true);
-    expect(uri.hasParam('nooo')).toBe(false);
-    expect(uri.getParam('transport')).toBe('tcp');
-    expect(uri.getParam('foo')).toBe('ABc');
-    expect(uri.getParam('baz')).toBe(null);
-    expect(uri.getParam('nooo')).toBe(undefined);
-    expect(uri.getHeader('x-header-1')).toEqual([ 'AaA1', 'AAA2' ]);
-    expect(uri.getHeader('X-HEADER-2')).toEqual([ 'BbB' ]);
-    expect(uri.getHeader('nooo')).toBe(undefined);
-    expect(uri.toString()).toBe('sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB');
-    expect(uri.toAor()).toBe('sip:aliCE@versatica.com');
-
-    // Alter data.
-    uri.user = 'Iñaki:PASSWD';
-    expect(uri.user).toBe('Iñaki:PASSWD');
-    expect(uri.deleteParam('foo')).toBe('ABc');
-    expect(uri.deleteHeader('x-header-1')).toEqual([ 'AaA1', 'AAA2' ]);
-    expect(uri.toString()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com:6060;transport=tcp;baz?X-Header-2=BbB');
-    expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
-    uri.clearParams();
-    uri.clearHeaders();
-    uri.port = null;
-    expect(uri.toString()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
-    expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
-  });
-
-  test('parse NameAddr', () =>
-  {
-    const data = ' "Iñaki ðđøþ foo \\"bar\\" \\\\\\\\ \\\\ \\\\d \\\\\\\\d \\\\\' \\\\\\"sdf\\\\\\"" ' +
-            '<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
-    const name = JsSIP.NameAddrHeader.parse(data);
-
-    // Parsed data.
-    expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(name.display_name).toBe('Iñaki ðđøþ foo "bar" \\\\ \\ \\d \\\\d \\\' \\"sdf\\"');
-    expect(name.hasParam('qwe')).toBe(true);
-    expect(name.hasParam('asd')).toBe(true);
-    expect(name.hasParam('nooo')).toBe(false);
-    expect(name.getParam('qwe')).toBe('QWE');
-    expect(name.getParam('asd')).toBe(null);
-
-    const uri = name.uri;
-
-    expect(uri instanceof (JsSIP.URI)).toBeTruthy();
-    expect(uri.scheme).toBe('sip');
-    expect(uri.user).toBe('aliCE');
-    expect(uri.host).toBe('versatica.com');
-    expect(uri.port).toBe(6060);
-    expect(uri.hasParam('transport')).toBe(true);
-    expect(uri.hasParam('nooo')).toBe(false);
-    expect(uri.getParam('transport')).toBe('tcp');
-    expect(uri.getParam('foo')).toBe('ABc');
-    expect(uri.getParam('baz')).toBe(null);
-    expect(uri.getParam('nooo')).toBe(undefined);
-    expect(uri.getHeader('x-header-1')).toEqual([ 'AaA1', 'AAA2' ]);
-    expect(uri.getHeader('X-HEADER-2')).toEqual([ 'BbB' ]);
-    expect(uri.getHeader('nooo')).toBe(undefined);
-
-    // Alter data.
-    name.display_name = 'Foo Bar';
-    expect(name.display_name).toBe('Foo Bar');
-    name.display_name = null;
-    expect(name.display_name).toBe(null);
-    expect(name.toString()).toBe('<sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB>;qwe=QWE;asd');
-    uri.user = 'Iñaki:PASSWD';
-    expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
-  });
-
-  test('parse invalid NameAddr with non UTF-8 characters', () =>
-  {
-    const buffer = Buffer.from([ 0xC0 ]);
-    const data = `"${buffer.toString()}"` +
-            '<sip:foo@bar.com>';
-    const name = JsSIP.NameAddrHeader.parse(data);
-
-    // Parsed data.
-    expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(name.display_name).toBe(buffer.toString());
-
-    const uri = name.uri;
-
-    expect(uri instanceof (JsSIP.URI)).toBeTruthy();
-    expect(uri.scheme).toBe('sip');
-    expect(uri.user).toBe('foo');
-    expect(uri.host).toBe('bar.com');
-    expect(uri.port).toBe(undefined);
-  });
-
-  test('parse NameAddr with token display_name', () =>
-  {
-    const data = 'Foo    Foo Bar\tBaz<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
-    const name = JsSIP.NameAddrHeader.parse(data);
-
-    // Parsed data.
-    expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(name.display_name).toBe('Foo Foo Bar Baz');
-  });
-
-  test('parse NameAddr with no space between DQUOTE and LAQUOT', () =>
-  {
-    const data = '"Foo"<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
-    const name = JsSIP.NameAddrHeader.parse(data);
-
-    // Parsed data.
-    expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(name.display_name).toBe('Foo');
-  });
-
-  test('parse NameAddr with no display_name', () =>
-  {
-    const data = '<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
-    const name = JsSIP.NameAddrHeader.parse(data);
-
-    // Parsed data.
-    expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(name.display_name).toBe(undefined);
-  });
-
-  test('parse multiple Contact', () =>
-  {
-    const data = '"Iñaki @ł€" <SIP:+1234@ALIAX.net;Transport=WS>;+sip.Instance="abCD", sip:bob@biloxi.COM;headerParam, <sip:DOMAIN.com:5>';
-    const contacts = JsSIP.Grammar.parse(data, 'Contact');
-
-    expect(contacts instanceof (Array)).toBeTruthy();
-    expect(contacts.length).toBe(3);
-    const c1 = contacts[0].parsed;
-    const c2 = contacts[1].parsed;
-    const c3 = contacts[2].parsed;
-
-    // Parsed data.
-    expect(c1 instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(c1.display_name).toBe('Iñaki @ł€');
-    expect(c1.hasParam('+sip.instance')).toBe(true);
-    expect(c1.hasParam('nooo')).toBe(false);
-    expect(c1.getParam('+SIP.instance')).toBe('"abCD"');
-    expect(c1.getParam('nooo')).toBe(undefined);
-    expect(c1.uri instanceof (JsSIP.URI)).toBeTruthy();
-    expect(c1.uri.scheme).toBe('sip');
-    expect(c1.uri.user).toBe('+1234');
-    expect(c1.uri.host).toBe('aliax.net');
-    expect(c1.uri.port).toBe(undefined);
-    expect(c1.uri.getParam('transport')).toBe('ws');
-    expect(c1.uri.getParam('foo')).toBe(undefined);
-    expect(c1.uri.getHeader('X-Header')).toBe(undefined);
-    expect(c1.toString()).toBe('"Iñaki @ł€" <sip:+1234@aliax.net;transport=ws>;+sip.instance="abCD"');
-
-    // Alter data.
-    c1.display_name = '€€€';
-    expect(c1.display_name).toBe('€€€');
-    c1.uri.user = '+999';
-    expect(c1.uri.user).toBe('+999');
-    c1.setParam('+sip.instance', '"zxCV"');
-    expect(c1.getParam('+SIP.instance')).toBe('"zxCV"');
-    c1.setParam('New-Param', null);
-    expect(c1.hasParam('NEW-param')).toBe(true);
-    c1.uri.setParam('New-Param', null);
-    expect(c1.toString()).toBe('"€€€" <sip:+999@aliax.net;transport=ws;new-param>;+sip.instance="zxCV";new-param');
-
-    // Parsed data.
-    expect(c2 instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(c2.display_name).toBe(undefined);
-    expect(c2.hasParam('HEADERPARAM')).toBe(true);
-    expect(c2.uri instanceof (JsSIP.URI)).toBeTruthy();
-    expect(c2.uri.scheme).toBe('sip');
-    expect(c2.uri.user).toBe('bob');
-    expect(c2.uri.host).toBe('biloxi.com');
-    expect(c2.uri.port).toBe(undefined);
-    expect(c2.uri.hasParam('headerParam')).toBe(false);
-    expect(c2.toString()).toBe('<sip:bob@biloxi.com>;headerparam');
-
-    // Alter data.
-    c2.display_name = '@ł€ĸłæß';
-    expect(c2.toString()).toBe('"@ł€ĸłæß" <sip:bob@biloxi.com>;headerparam');
-
-    // Parsed data.
-    expect(c3 instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
-    expect(c3.displayName).toBe(undefined);
-    expect(c3.uri instanceof (JsSIP.URI)).toBeTruthy();
-    expect(c3.uri.scheme).toBe('sip');
-    expect(c3.uri.user).toBe(undefined);
-    expect(c3.uri.host).toBe('domain.com');
-    expect(c3.uri.port).toBe(5);
-    expect(c3.uri.hasParam('nooo')).toBe(false);
-    expect(c3.toString()).toBe('<sip:domain.com:5>');
-
-    // Alter data.
-    c3.uri.setParam('newUriParam', 'zxCV');
-    c3.setParam('newHeaderParam', 'zxCV');
-    expect(c3.toString()).toBe('<sip:domain.com:5;newuriparam=zxCV>;newheaderparam=zxCV');
-  });
-
-  test('parse Via', () =>
-  {
-    let data = 'SIP /  3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n  BRanch=1234;Param1=Foo;paRAM2;param3=Bar';
-    let via = JsSIP.Grammar.parse(data, 'Via');
-
-    expect(via.protocol).toBe('SIP');
-    expect(via.transport).toBe('UDP');
-    expect(via.host).toBe('[1:ab::FF]');
-    expect(via.host_type).toBe('IPv6');
-    expect(via.port).toBe(6060);
-    expect(via.branch).toBe('1234');
-    expect(via.params).toEqual({ param1: 'Foo', param2: undefined, param3: 'Bar' });
-
-    data = 'SIP /  3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n  BRanch=1234;rport=1111;Param1=Foo;paRAM2;param3=Bar';
-    via = JsSIP.Grammar.parse(data, 'Via');
-
-    expect(via.protocol).toBe('SIP');
-    expect(via.transport).toBe('UDP');
-    expect(via.host).toBe('[1:ab::FF]');
-    expect(via.host_type).toBe('IPv6');
-    expect(via.port).toBe(6060);
-    expect(via.branch).toBe('1234');
-    expect(via.rport).toBe(1111);
-    expect(via.params).toEqual({ param1: 'Foo', param2: undefined, param3: 'Bar' });
-
-    data = 'SIP /  3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n  BRanch=1234;rport;Param1=Foo;paRAM2;param3=Bar';
-    via = JsSIP.Grammar.parse(data, 'Via');
-
-    expect(via.protocol).toBe('SIP');
-    expect(via.transport).toBe('UDP');
-    expect(via.host).toBe('[1:ab::FF]');
-    expect(via.host_type).toBe('IPv6');
-    expect(via.port).toBe(6060);
-    expect(via.branch).toBe('1234');
-    expect(via.rport).toBe(undefined);
-    expect(via.params).toEqual({ param1: 'Foo', param2: undefined, param3: 'Bar' });
-  });
-
-  test('parse CSeq', () =>
-  {
-    const data = '123456  CHICKEN';
-    const cseq = JsSIP.Grammar.parse(data, 'CSeq');
-
-    expect(cseq.value).toBe(123456);
-    expect(cseq.method).toBe('CHICKEN');
-  });
-
-  test('parse authentication challenge', () =>
-  {
-    const data = 'Digest realm =  "[1:ABCD::abc]", nonce =  "31d0a89ed7781ce6877de5cb032bf114", qop="AUTH,autH-INt", algorithm =  md5  ,  stale =  TRUE , opaque = "00000188"';
-    const auth = JsSIP.Grammar.parse(data, 'challenge');
-
-    expect(auth.realm).toBe('[1:ABCD::abc]');
-    expect(auth.nonce).toBe('31d0a89ed7781ce6877de5cb032bf114');
-    expect(auth.qop).toEqual([ 'auth', 'auth-int' ]);
-    expect(auth.algorithm).toBe('MD5');
-    expect(auth.stale).toBe(true);
-    expect(auth.opaque).toBe('00000188');
-  });
-
-  test('parse Event', () =>
-  {
-    const data = 'Presence;Param1=QWe;paraM2';
-    const event = JsSIP.Grammar.parse(data, 'Event');
-
-    expect(event.event).toBe('presence');
-    expect(event.params).toEqual({ param1: 'QWe', param2: undefined });
-  });
-
-  test('parse Session-Expires', () =>
-  {
-    let data, session_expires;
-
-    data = '180;refresher=uac';
-    session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
-
-    expect(session_expires.expires).toBe(180);
-    expect(session_expires.refresher).toBe('uac');
-
-    data = '210  ;   refresher  =  UAS ; foo  =  bar';
-    session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
-
-    expect(session_expires.expires).toBe(210);
-    expect(session_expires.refresher).toBe('uas');
-  });
-
-  test('parse Reason', () =>
-  {
-    let data, reason;
-
-    data = 'SIP  ; cause = 488 ; text = "Wrong SDP"';
-    reason = JsSIP.Grammar.parse(data, 'Reason');
-
-    expect(reason.protocol).toBe('sip');
-    expect(reason.cause).toBe(488);
-    expect(reason.text).toBe('Wrong SDP');
-
-    data = 'ISUP; cause=500 ; LALA = foo';
-    reason = JsSIP.Grammar.parse(data, 'Reason');
-
-    expect(reason.protocol).toBe('isup');
-    expect(reason.cause).toBe(500);
-    expect(reason.text).toBe(undefined);
-    expect(reason.params.lala).toBe('foo');
-  });
-
-  test('parse host', () =>
-  {
-    let data, parsed;
-
-    data = 'versatica.com';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
-    expect(parsed.host_type).toBe('domain');
-
-    data = 'myhost123';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
-    expect(parsed.host_type).toBe('domain');
-
-    data = '1.2.3.4';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
-    expect(parsed.host_type).toBe('IPv4');
-
-    data = '[1:0:fF::432]';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
-    expect(parsed.host_type).toBe('IPv6');
-
-    data = '1.2.3.444';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
-
-    data = 'iñaki.com';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
-
-    data = '1.2.3.bar.qwe-asd.foo';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
-    expect(parsed.host_type).toBe('domain');
-
-    data = '1.2.3.4.bar.qwe-asd.foo';
-    expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
-    expect(parsed.host_type).toBe('domain');
-  });
-
-  test('parse Refer-To', () =>
-  {
-    let data, parsed;
-
-    data = 'sip:alice@versatica.com';
-    expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
-    expect(parsed.uri.scheme).toBe('sip');
-    expect(parsed.uri.user).toBe('alice');
-    expect(parsed.uri.host).toBe('versatica.com');
-
-    data = '<sip:bob@versatica.com?Accept-Contact=sip:bobsdesk.versatica.com>';
-    expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
-    expect(parsed.uri.scheme).toBe('sip');
-    expect(parsed.uri.user).toBe('bob');
-    expect(parsed.uri.host).toBe('versatica.com');
-    expect(parsed.uri.hasHeader('Accept-Contact')).toBe(true);
-  });
-
-  test('parse Replaces', () =>
-  {
-    let parsed;
-
-    const data = '5t2gpbrbi72v79p1i8mr;to-tag=03aq91cl9n;from-tag=kun98clbf7';
-
-    expect((parsed = JsSIP.Grammar.parse(data, 'Replaces'))).not.toBe(-1);
-    expect(parsed.call_id).toBe('5t2gpbrbi72v79p1i8mr');
-    expect(parsed.to_tag).toBe('03aq91cl9n');
-    expect(parsed.from_tag).toBe('kun98clbf7');
-  });
-
-  test('parse Status Line', () =>
-  {
-    const data = 'SIP/2.0 420 Bad Extension';
-    let parsed;
-
-    expect((parsed = JsSIP.Grammar.parse(data, 'Status_Line'))).not.toBe(-1);
-    expect(parsed.status_code).toBe(420);
-  });
-
-  test('parse message', () =>
-  {
-    // eslint-disable-next-line no-multi-str
-    const data = 'INVITE sip:bob@biloxi.com SIP/2.0\r\n\
+		const data = '5t2gpbrbi72v79p1i8mr;to-tag=03aq91cl9n;from-tag=kun98clbf7';
+
+		expect((parsed = JsSIP.Grammar.parse(data, 'Replaces'))).not.toBe(-1);
+		expect(parsed.call_id).toBe('5t2gpbrbi72v79p1i8mr');
+		expect(parsed.to_tag).toBe('03aq91cl9n');
+		expect(parsed.from_tag).toBe('kun98clbf7');
+	});
+
+	test('parse Status Line', () => {
+		const data = 'SIP/2.0 420 Bad Extension';
+		let parsed;
+
+		expect((parsed = JsSIP.Grammar.parse(data, 'Status_Line'))).not.toBe(-1);
+		expect(parsed.status_code).toBe(420);
+	});
+
+	test('parse message', () => {
+		const data =
+			// eslint-disable-next-line no-multi-str
+			'INVITE sip:bob@biloxi.com SIP/2.0\r\n\
 Via: SIP/2.0/TCP useragent.cisco.com;branch=z9hG4bK-a111\r\n\
 To: <sip:bob@biloxi.com>\r\n\
 From: "Anonymous" <sip:anonymous@anonymous.invalid>;tag=9802748\r\n\
@@ -401,24 +417,26 @@ Max-Forwards: 70\r\n\
 Privacy: id\r\n\
 P-Preferred-Identity: "Cullen Jennings" <sip:fluffy@cisco.com>\r\n\r\n';

-    const config = testUA.UA_CONFIGURATION;
-    const wsSocket = new JsSIP.WebSocketInterface(testUA.SOCKET_DESCRIPTION.url);
+		const config = testUA.UA_CONFIGURATION;
+		const wsSocket = new JsSIP.WebSocketInterface(
+			testUA.SOCKET_DESCRIPTION.url
+		);

-    config.sockets = wsSocket;
+		config.sockets = wsSocket;

-    const ua = new JsSIP.UA(config);
-    const message = Parser.parseMessage(data, ua);
+		const ua = new JsSIP.UA(config);
+		const message = Parser.parseMessage(data, ua);

-    expect(message.hasHeader('P-Preferred-Identity')).toBe(true);
+		expect(message.hasHeader('P-Preferred-Identity')).toBe(true);

-    const pai = message.getHeader('P-Preferred-Identity');
-    const nameAddress = JsSIP.NameAddrHeader.parse(pai);
+		const pai = message.getHeader('P-Preferred-Identity');
+		const nameAddress = JsSIP.NameAddrHeader.parse(pai);

-    expect(nameAddress instanceof JsSIP.NameAddrHeader).toBeTruthy();
-    expect(nameAddress.uri.user).toBe('fluffy');
-    expect(nameAddress.uri.host).toBe('cisco.com');
+		expect(nameAddress instanceof JsSIP.NameAddrHeader).toBeTruthy();
+		expect(nameAddress.uri.user).toBe('fluffy');
+		expect(nameAddress.uri.host).toBe('cisco.com');

-    expect(message.hasHeader('Privacy')).toBe(true);
-    expect(message.getHeader('Privacy')).toBe('id');
-  });
+		expect(message.hasHeader('Privacy')).toBe(true);
+		expect(message.getHeader('Privacy')).toBe('id');
+	});
 });
diff --git a/test/test-properties.js b/test/test-properties.js
index 5b9d7d5..0a1d4ee 100644
--- a/test/test-properties.js
+++ b/test/test-properties.js
@@ -2,16 +2,12 @@ require('./include/common');
 const JsSIP = require('../');
 const pkg = require('../package.json');

+describe('Properties', () => {
+	test('should have a name property', () => {
+		expect(JsSIP.name).toEqual(pkg.title);
+	});

-describe('Properties', () =>
-{
-  test('should have a name property', () =>
-  {
-    expect(JsSIP.name).toEqual(pkg.title);
-  });
-
-  test('should have a version property', () =>
-  {
-    expect(JsSIP.version).toEqual(pkg.version);
-  });
+	test('should have a version property', () => {
+		expect(JsSIP.version).toEqual(pkg.version);
+	});
 });
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1251e87
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,43 @@
+{
+	"compileOnSave": true,
+	"compilerOptions": {
+		"lib": ["es2024", "dom"],
+		"module": "NodeNext",
+		"moduleResolution": "NodeNext",
+		"outDir": "lib",
+		"rootDir": "src",
+		"allowJs": true,
+		// Do not generate declaration files automatically yet.
+		// "declaration": true,
+		// "declarationMap": true,
+		// "emitDeclarationOnly": false,
+		// "declarationDir": "lib",
+		"isolatedModules": true,
+		"verbatimModuleSyntax": false,
+		"useDefineForClassFields": true,
+		"esModuleInterop": false,
+		"allowImportingTsExtensions": false,
+		"allowUnreachableCode": false,
+		"allowUnusedLabels": false,
+		"alwaysStrict": true,
+		"exactOptionalPropertyTypes": false,
+		"noFallthroughCasesInSwitch": true,
+		"noImplicitAny": true,
+		"noImplicitOverride": true,
+		"noImplicitReturns": true,
+		"noImplicitThis": true,
+		"noPropertyAccessFromIndexSignature": true,
+		"noUncheckedIndexedAccess": true,
+		"noUnusedLocals": false,
+		"noUnusedParameters": false,
+		"strict": true,
+		"strictBindCallApply": true,
+		"strictBuiltinIteratorReturn": true,
+		"strictFunctionTypes": true,
+		"strictNullChecks": true,
+		"strictPropertyInitialization": true,
+		"useUnknownInCatchVariables": true,
+		"noUncheckedSideEffectImports": true,
+	},
+	"include": ["src"],
+}