UNPKG

diffusion

Version:

Diffusion JavaScript client

470 lines (469 loc) 18.3 kB
"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;