@fnlb-project/stanza
Version:
Modern XMPP in the browser, with a JSON API
298 lines (297 loc) • 11 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const async_1 = require("async");
const Constants_1 = require("../Constants");
const Utils_1 = require("../Utils");
const badRequest = { condition: Constants_1.StanzaErrorCondition.BadRequest };
const unsupportedInfo = {
condition: Constants_1.StanzaErrorCondition.FeatureNotImplemented,
jingleError: Constants_1.JingleErrorCondition.UnsupportedInfo,
type: 'modify'
};
class JingleSession {
constructor(opts) {
this.parent = opts.parent;
this.sid = opts.sid || (0, Utils_1.uuid)();
this.peerID = opts.peerID;
this.role = opts.initiator ? Constants_1.JingleSessionRole.Initiator : Constants_1.JingleSessionRole.Responder;
this._sessionState = 'starting';
this._connectionState = 'starting';
// We track the intial pending description types in case
// of the need for a tie-breaker.
this.pendingApplicationTypes = opts.applicationTypes || [];
this.pendingAction = undefined;
// Here is where we'll ensure that all actions are processed
// in order, even if a particular action requires async handling.
this.processingQueue = (0, async_1.priorityQueue)(async (task, next) => {
if (this.state === 'ended') {
// Don't process anything once the session has been ended
if (task.type === 'local' && task.reject) {
task.reject(new Error('Session ended'));
}
if (next) {
next();
}
return;
}
if (task.type === 'local') {
this._log('debug', 'Processing local action:', task.name);
try {
const res = await task.handler();
task.resolve(res);
}
catch (err) {
task.reject(err);
}
if (next) {
next();
}
return;
}
const { action, changes, cb } = task;
this._log('debug', 'Processing remote action:', action);
return new Promise(resolve => {
const done = (err, result) => {
cb(err, result);
if (next) {
next();
}
resolve();
};
switch (action) {
case Constants_1.JingleAction.ContentAccept:
return this.onContentAccept(changes, done);
case Constants_1.JingleAction.ContentAdd:
return this.onContentAdd(changes, done);
case Constants_1.JingleAction.ContentModify:
return this.onContentModify(changes, done);
case Constants_1.JingleAction.ContentReject:
return this.onContentReject(changes, done);
case Constants_1.JingleAction.ContentRemove:
return this.onContentRemove(changes, done);
case Constants_1.JingleAction.DescriptionInfo:
return this.onDescriptionInfo(changes, done);
case Constants_1.JingleAction.SecurityInfo:
return this.onSecurityInfo(changes, done);
case Constants_1.JingleAction.SessionAccept:
return this.onSessionAccept(changes, done);
case Constants_1.JingleAction.SessionInfo:
return this.onSessionInfo(changes, done);
case Constants_1.JingleAction.SessionInitiate:
return this.onSessionInitiate(changes, done);
case Constants_1.JingleAction.SessionTerminate:
return this.onSessionTerminate(changes, done);
case Constants_1.JingleAction.TransportAccept:
return this.onTransportAccept(changes, done);
case Constants_1.JingleAction.TransportInfo:
return this.onTransportInfo(changes, done);
case Constants_1.JingleAction.TransportReject:
return this.onTransportReject(changes, done);
case Constants_1.JingleAction.TransportReplace:
return this.onTransportReplace(changes, done);
default:
this._log('error', 'Invalid or unsupported action: ' + action);
done({ condition: Constants_1.StanzaErrorCondition.BadRequest });
}
});
}, 1);
}
get isInitiator() {
return this.role === Constants_1.JingleSessionRole.Initiator;
}
get peerRole() {
return this.isInitiator ? Constants_1.JingleSessionRole.Responder : Constants_1.JingleSessionRole.Initiator;
}
get state() {
return this._sessionState;
}
set state(value) {
if (value !== this._sessionState) {
this._log('info', 'Changing session state to: ' + value);
this._sessionState = value;
if (this.parent) {
this.parent.emit('sessionState', this, value);
}
}
}
get connectionState() {
return this._connectionState;
}
set connectionState(value) {
if (value !== this._connectionState) {
this._log('info', 'Changing connection state to: ' + value);
this._connectionState = value;
if (this.parent) {
this.parent.emit('connectionState', this, value);
}
}
}
send(action, data) {
data = data || {};
data.sid = this.sid;
data.action = action;
const requirePending = new Set([
Constants_1.JingleAction.ContentAccept,
Constants_1.JingleAction.ContentAdd,
Constants_1.JingleAction.ContentModify,
Constants_1.JingleAction.ContentReject,
Constants_1.JingleAction.ContentRemove,
Constants_1.JingleAction.SessionAccept,
Constants_1.JingleAction.SessionInitiate,
Constants_1.JingleAction.TransportAccept,
Constants_1.JingleAction.TransportReject,
Constants_1.JingleAction.TransportReplace
]);
if (requirePending.has(action)) {
this.pendingAction = action;
}
else {
this.pendingAction = undefined;
}
this.parent.signal(this, {
id: (0, Utils_1.uuid)(),
jingle: data,
to: this.peerID,
type: 'set'
});
}
processLocal(name, handler) {
return new Promise((resolve, reject) => {
this.processingQueue.push({
handler,
name,
reject,
resolve,
type: 'local'
}, 1 // Process local requests first
);
});
}
process(action, changes, cb) {
this.processingQueue.push({
action,
cb,
changes,
type: 'remote'
}, 2 // Process remote requests second
);
}
start(_opts, _next) {
this._log('error', 'Can not start base sessions');
this.end('unsupported-applications', true);
}
accept(_opts, _next) {
this._log('error', 'Can not accept base sessions');
this.end('unsupported-applications');
}
cancel() {
this.end('cancel');
}
decline() {
this.end('decline');
}
end(reason = 'success', silent = false) {
this.state = 'ended';
this.processingQueue.kill();
if (typeof reason === 'string') {
reason = {
condition: reason
};
}
if (!silent) {
this.send('session-terminate', {
reason
});
}
this.parent.emit('terminated', this, reason);
this.parent.forgetSession(this);
}
_log(level, message, ...data) {
if (this.parent) {
message = this.sid + ': ' + message;
this.parent.emit('log', level, message, ...data);
this.parent.emit('log:' + level, message, ...data);
}
}
onSessionInitiate(changes, cb) {
cb();
}
onSessionAccept(changes, cb) {
cb();
}
onSessionTerminate(changes, cb) {
this.end(changes.reason, true);
cb();
}
// It is mandatory to reply to a session-info action with
// an unsupported-info error if the info isn't recognized.
//
// However, a session-info action with no associated payload
// is acceptable (works like a ping).
onSessionInfo(changes, cb) {
if (!changes.info) {
cb();
}
else {
cb(unsupportedInfo);
}
}
// It is mandatory to reply to a security-info action with
// an unsupported-info error if the info isn't recognized.
onSecurityInfo(changes, cb) {
cb(unsupportedInfo);
}
// It is mandatory to reply to a description-info action with
// an unsupported-info error if the info isn't recognized.
onDescriptionInfo(changes, cb) {
cb(unsupportedInfo);
}
// It is mandatory to reply to a transport-info action with
// an unsupported-info error if the info isn't recognized.
onTransportInfo(changes, cb) {
cb(unsupportedInfo);
}
// It is mandatory to reply to a content-add action with either
// a content-accept or content-reject.
onContentAdd(changes, cb) {
// Allow ack for the content-add to be sent.
cb();
this.send(Constants_1.JingleAction.ContentReject, {
reason: {
condition: Constants_1.JingleReasonCondition.FailedApplication,
text: 'content-add is not supported'
}
});
}
onContentAccept(changes, cb) {
cb(badRequest);
}
onContentReject(changes, cb) {
cb(badRequest);
}
onContentModify(changes, cb) {
cb(badRequest);
}
onContentRemove(changes, cb) {
cb(badRequest);
}
// It is mandatory to reply to a transport-add action with either
// a transport-accept or transport-reject.
onTransportReplace(changes, cb) {
// Allow ack for the transport-replace be sent.
cb();
this.send(Constants_1.JingleAction.TransportReject, {
reason: {
condition: Constants_1.JingleReasonCondition.FailedTransport,
text: 'transport-replace is not supported'
}
});
}
onTransportAccept(changes, cb) {
cb(badRequest);
}
onTransportReject(changes, cb) {
cb(badRequest);
}
}
exports.default = JingleSession;