diffusion
Version:
Diffusion JavaScript client
470 lines (469 loc) • 18.3 kB
JavaScript
"use strict";
/**
* @module Conversation
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.create = exports.ConversationSet = exports.NoSuchConversationError = exports.Result = exports.ConversationIdGenerator = void 0;
var conversation_id_1 = require("./../conversation/conversation-id");
var logger = require("./../util/logger");
var Long = require("long");
/**
* A generator for creating sequential conversation IDs across an arbitrary
* number of conversation sets.
*/
var NEXT_CID = (function () {
var id = new Long(0);
return function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
id = id.add(1);
return [2 /*return*/, new conversation_id_1.ConversationId(id)];
});
}); };
})();
exports.ConversationIdGenerator = NEXT_CID;
/**
* The result code when calling {@link Conversation.respond} indicating
* how the response has been handled by the conversation.
*/
var Result;
(function (Result) {
Result[Result["ALREADY_FINISHED"] = 0] = "ALREADY_FINISHED";
Result[Result["HANDLED_AND_ACTIVE"] = 1] = "HANDLED_AND_ACTIVE";
Result[Result["HANDLED_AND_FINISHED"] = 2] = "HANDLED_AND_FINISHED";
})(Result = exports.Result || (exports.Result = {}));
var LOG = logger.create('Conversation Set');
/**
* An error thrown when no conversation could be found
*/
var NoSuchConversationError = /** @class */ (function (_super) {
__extends(NoSuchConversationError, _super);
function NoSuchConversationError(message) {
var _this = _super.call(this, message) || this;
_this.name = 'NoSuchConversationError';
return _this;
}
return NoSuchConversationError;
}(Error));
exports.NoSuchConversationError = NoSuchConversationError;
/**
* Internal Conversation implementation. Maintains state for a provided
* conversation handler to ensure recursive operations remain in-order.
*
* @param {Object} handler the conversation handler to dispatch events to.
* @constructor
*/
var Conversation = /** @class */ (function () {
/**
* Create a new conversation
*
* @param handler the response handler
* @param completedExceptionally a callback to call when the conversation
* completed exceptionally
*/
function Conversation(handler, completedExceptionally) {
/**
* A flag inticating a reserved state to prevent recursive discards
*/
this.reserved = false;
/**
* A flag indicating that the conversation is closed
*/
this.closed = false;
this.handler = handler;
this.completedExceptionally = completedExceptionally;
}
/**
* Open the conversation
*
* @param cid the conversation ID
*/
Conversation.prototype.open = function (cid) {
this.handler.onOpen(cid);
};
/**
* Send a response to the handler.
*
* @param cid the conversation ID
* @param response the response
* @return the result code indicating how the response has been
* handled
*/
Conversation.prototype.respond = function (cid, response) {
if (this.closed) {
return Result.ALREADY_FINISHED;
}
// set as reserved so as to prevent recursive discards
var previous = this.reserved;
this.reserved = true;
var close;
try {
close = this.handler.onResponse(cid, response);
}
catch (e) {
this.completedExceptionally(cid, this, e);
throw e;
}
if (this.closed) {
LOG.debug('Conversation already closed', cid);
return Result.HANDLED_AND_FINISHED;
}
else if (close) {
LOG.debug('Response handler closed conversation', cid);
this.closed = true;
return Result.HANDLED_AND_FINISHED;
}
else if (this.pendingDiscard) {
LOG.debug('Conversation closed with pending reason', cid);
this.notifyDiscard(cid, this.pendingDiscard);
return Result.HANDLED_AND_FINISHED;
}
else {
this.reserved = previous;
return Result.HANDLED_AND_ACTIVE;
}
};
/**
* Discard the conversation unless it is already finished.
*
* @param cid the conversation ID to discard
* @param reason why the conversation was discarded
*/
Conversation.prototype.discard = function (cid, reason) {
if (this.reserved) {
this.pendingDiscard = reason;
}
else {
this.notifyDiscard(cid, reason);
}
};
/**
* Close the conversation
*/
Conversation.prototype.close = function () {
this.closed = true;
};
/**
* Close the conversation and notify the handler
*
* @param cid the conversation ID to close
* @param reason why the conversation was closed
*/
Conversation.prototype.notifyDiscard = function (cid, reason) {
if (!this.closed) {
this.closed = true;
try {
this.handler.onDiscard(cid, reason);
}
catch (e) {
LOG.error('Application handler threw exception [cid=' + cid + ']', e.stack);
throw e;
}
}
};
return Conversation;
}());
/**
* Manage a set of conversations. The conversations usually relate to a single
* peer.
*
* <p>
* This class doesn't provide any communication methods itself, it simply tracks
* the state associated with a conversation.
*
* A conversation can be created with {@link newConversation}, which returns a
* unique ID that can be serialised, and later used to refer to the
* conversation. Each conversation is associated with a caller-supplied
* `ResponseHandler`. The `ResponseHandler` receives a sequence of
* callbacks for the conversation according to a well-defined life cycle,
* allowing it to manage the associated application state.
*
* If {@link respond} is called with the ID of an active conversation, the
* supplied response will be passed to {@link ResponseHandler.onResponse}. For
* every conversation, the response handler will receive an initial
* {@link ResponseHandler.onOpen} call, followed by one or more
* `onResponse` calls. If `onResponse` returns `true`, the
* conversation will be closed normally and no further calls will be made to the
* handler for that conversation. Returning `true` from `onResponse`
* is the only way to close a conversation normally (rather than discard it).
* This places the onus on the handler to determine when the conversation
* finishes normally, based on the received message and/or its own state.
*
* A conversation can be 'discarded' to close it in an abnormal fashion.
* {@link discardAll} discards all of the conversations owned by
* this conversation set. Once called, this conversation set enters a
* 'discarded' state, and new conversations will be discarded immediately after
* they are opened. Individual conversations can be discarded with
* {@link discard}. Whichever method is used to
* discard a conversation, {@link ResponseHandler.onDiscard} will be the final
* call to the handler for that conversation.
*
* <p>
* Finally, if {@link ResponseHandler.onOpen} or
* {@link ResponseHandler.onResponse} throws an exception, the conversation will
* complete exceptionally. The exception is logged at debug level and re-thrown.
*
* <p>
* The ways a conversation can close are summarised in the following table.
* <table>
* <tr>
* <th>Cause</th>
* <th>Close type</th>
* <th>How close is reported</th>
* </tr>
* <tr>
* <td>`ResponseHandler.onResponse()` returns true</td>
* <td>Closed normally</td>
* <td>Not reported</td>
* </tr>
* <tr>
* <td>`ResponseHandler.onOpen()` throws exception</td>
* <td>Closed exceptionally</td>
* <td>Exception is re-thrown to caller of `newConversation()`.<br/>
* Debug log message</td>
* </tr>
* <tr>
* <td>`ResponseHandler.onResponse()` throws exception</td>
* <td>Closed exceptionally</td>
* <td>Exception is re-thrown to caller of `respond()`.<br/>
* Debug log message</td>
* </tr>
* <tr>
* <td>`discard()`<br/>
* `discardAll()`</td>
* <td>Discarded</td>
* <td>`ResponseHandler.onDiscard()` is called.</td>
* </tr>
* </table>
*/
var ConversationSet = /** @class */ (function () {
/**
* Create a conversation set
*
* @param idGenerator the generator function for conversation ID
*/
function ConversationSet(idGenerator) {
this.nextCID = idGenerator;
this.conversations = {};
}
/**
* Complete a conversation exceptionally.
*
* @param cid the conversation ID
* @param conversation the conversation
* @param error the error that caused the exception
*/
ConversationSet.prototype.completedExceptionally = function (cid, conversation, e) {
LOG.debug('Application handler threw exception [cid=' + cid + ']', e.stack);
conversation.close();
delete this.conversations[cid.toString()];
};
/**
* Open a new conversation.
*
* @param responseHandler the response handler
* @return the ID that can be used to refer to the
* conversation.
*/
ConversationSet.prototype.newConversation = function (responseHandler) {
return __awaiter(this, void 0, void 0, function () {
var conversation, cid;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
conversation = new Conversation(responseHandler, this.completedExceptionally.bind(this));
return [4 /*yield*/, this.nextCID()];
case 1:
cid = _a.sent();
this.conversations[cid.toString()] = conversation;
try {
conversation.open(cid);
}
catch (e) {
this.completedExceptionally(cid, conversation, e);
throw e;
}
if (this.discardReason !== undefined) {
this.discard(cid, this.discardReason);
}
return [2 /*return*/, cid];
}
});
});
};
/**
* Notify the handler of a response.
*
* @param cid the conversation ID
* @param response the response
* @throws an error if the conversation is not found
*/
ConversationSet.prototype.respond = function (cid, response) {
var conversation = this.conversations[cid.toString()];
/* tslint:disable-next-line:strict-type-predicates */
if (conversation === undefined) {
LOG.debug("No conversation for cid: " + cid + ", response: ", response);
throw new NoSuchConversationError('No such conversation');
}
var result = conversation.respond(cid, response);
switch (result) {
case Result.ALREADY_FINISHED:
throw new NoSuchConversationError('No such conversation');
case Result.HANDLED_AND_ACTIVE:
break;
default:
delete this.conversations[cid.toString()];
}
};
/**
* Variant of {@link respond} that does nothing if there is no open
* conversation with the given ID.
*
* @param cid the conversation ID
* @param response the response
*/
ConversationSet.prototype.respondIfPresent = function (cid, response) {
try {
this.respond(cid, response);
}
catch (e) {
if (e.name !== 'NoSuchConversationError') {
throw e;
}
}
};
/**
* Discard the specified conversation.
*
* This method will discard the conversation specified by the provided ID,
* notifying the corresponding handler's {@link ResponseHandler.onDiscard}
* method.
*
* Subsequent calls to this method have no effect.
*
* If there is no conversation with the given id, this method has no effect.
*
* @param cid the conversation ID to discard
* @param reason why the conversation was discarded
*/
ConversationSet.prototype.discard = function (cid, reason) {
var conversation = this.conversations[cid.toString()];
delete this.conversations[cid.toString()];
if (conversation) {
conversation.discard(cid, reason);
}
};
/**
* Discard all conversations in this set.
*
* When this method returns, all current conversations will have been
* discarded, notifying their corresponding handlers, and new conversations
* will be discarded immediately. Calling {@link newConversation} will
* immediately call the supplied handler's
* {@link ResponseHandler.onDiscard}.
*
* Subsequent calls to this method have no effect.
* @param reason why the conversation set was discarded
*/
ConversationSet.prototype.discardAll = function (reason) {
var e_1, _a;
if (this.discardReason !== undefined) {
return;
}
this.discardReason = reason;
try {
for (var _b = __values(Object.keys(this.conversations)), _c = _b.next(); !_c.done; _c = _b.next()) {
var cid = _c.value;
this.discard(conversation_id_1.ConversationId.fromString(cid), reason);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
};
/**
* Get the size of the conversation set
*
* @return the number of conversations that have been created
* and not discarded
*/
ConversationSet.prototype.size = function () {
return Object.keys(this.conversations).length;
};
return ConversationSet;
}());
exports.ConversationSet = ConversationSet;
/**
* Factory function to create a new conversation set
*
* @return a new conversation set
*/
function create() {
return new ConversationSet(NEXT_CID);
}
exports.create = create;