UNPKG

@stackend/api

Version:

JS bindings to api.stackend.com

751 lines 28.9 kB
"use strict"; 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 __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.removeAll = exports.removeInstance = exports.getInstance = exports.removeInitializer = exports.addInitializer = exports.USER_NOTIFICATION_CONTEXT = exports.USER_NOTIFICATION_COMPONENT = exports.REALTIME_CONTEXT = exports.REALTIME_COMPONENT = exports.ForumSubscription = exports.BlogSubscription = exports.CommentsSubscription = exports.Subscription = exports.RealTimeMessageType = exports.RealTimeFunctionName = exports.StackendWebSocketEvent = void 0; var sockjs_client_1 = __importDefault(require("sockjs-client")); var CommunityContext_1 = require("../api/CommunityContext"); /** * Events */ var StackendWebSocketEvent; (function (StackendWebSocketEvent) { StackendWebSocketEvent["SOCKET_OPENING"] = "socketOpening.ws"; StackendWebSocketEvent["SOCKET_OPENED"] = "socketOpened.ws"; StackendWebSocketEvent["MESSAGE_RECEIVED"] = "messageReceived.ws"; StackendWebSocketEvent["SOCKET_CLOSED"] = "socketClosed.ws"; StackendWebSocketEvent["SOCKET_ERROR"] = "socketError.ws"; })(StackendWebSocketEvent = exports.StackendWebSocketEvent || (exports.StackendWebSocketEvent = {})); /** * Functions that supports real time subscriptions */ var RealTimeFunctionName; (function (RealTimeFunctionName) { RealTimeFunctionName["COMMENT"] = "comment"; RealTimeFunctionName["BLOG"] = "blog"; RealTimeFunctionName["LIKE"] = "like"; RealTimeFunctionName["FORUM"] = "forum"; })(RealTimeFunctionName = exports.RealTimeFunctionName || (exports.RealTimeFunctionName = {})); /** * Real time message types */ var RealTimeMessageType; (function (RealTimeMessageType) { RealTimeMessageType["PING"] = "PING"; RealTimeMessageType["SUBSCRIBE"] = "SUBSCRIBE"; RealTimeMessageType["UNSUBSCRIBE"] = "UNSUBSCRIBE"; RealTimeMessageType["UNSUBSCRIBE_ALL"] = "UNSUBSCRIBE_ALL"; RealTimeMessageType["PONG"] = "PONG"; RealTimeMessageType["OBJECT_CREATED"] = "OBJECT_CREATED"; RealTimeMessageType["OBJECT_MODIFIED"] = "OBJECT_MODIFIED"; RealTimeMessageType["OBJECT_REMOVED"] = "OBJECT_REMOVED"; RealTimeMessageType["ERROR"] = "ERROR"; })(RealTimeMessageType = exports.RealTimeMessageType || (exports.RealTimeMessageType = {})); var Subscription = /** @class */ (function () { function Subscription(component, context, referenceId) { if (!component) { throw 'Component is required'; } if (!context) { throw 'context is required'; } if (!referenceId) { throw 'referenceId is required'; } this.component = component; this.context = context; this.referenceId = referenceId; this.key = 'sub:' + context + ':' + component + ':' + referenceId; } Subscription.prototype.getKey = function () { return this.key; }; return Subscription; }()); exports.Subscription = Subscription; var CommentsSubscription = /** @class */ (function (_super) { __extends(CommentsSubscription, _super); function CommentsSubscription(context, referenceId) { return _super.call(this, RealTimeFunctionName.COMMENT, context, referenceId) || this; } return CommentsSubscription; }(Subscription)); exports.CommentsSubscription = CommentsSubscription; var BlogSubscription = /** @class */ (function (_super) { __extends(BlogSubscription, _super); function BlogSubscription(context, blogId) { return _super.call(this, RealTimeFunctionName.BLOG, context, blogId) || this; } return BlogSubscription; }(Subscription)); exports.BlogSubscription = BlogSubscription; var ForumSubscription = /** @class */ (function (_super) { __extends(ForumSubscription, _super); function ForumSubscription(context, referenceId) { return _super.call(this, RealTimeFunctionName.FORUM, context, referenceId) || this; } return ForumSubscription; }(Subscription)); exports.ForumSubscription = ForumSubscription; /** * Component used for real time notifications about xcap object creation/editing/deletion * (a comment has been made in this collection of comments) */ exports.REALTIME_COMPONENT = 'realtime'; /** * Context used for real time info */ exports.REALTIME_CONTEXT = exports.REALTIME_COMPONENT; /** * Component used for FB style notifications (X has liked my post, Y has replied to my post ...) */ exports.USER_NOTIFICATION_COMPONENT = 'usernotification'; /** * Context used for FB style notifications */ exports.USER_NOTIFICATION_CONTEXT = 'notifications_site'; var DEFAULT_RECONNECT_DELAY = 10 * 1000; /** * A web socket that is used to send and receive events from the stackend server. * * // Register your notification handler * addInitializer((sws: StackendWebSocket) => { * sws.addListener((type, event, message) => { * console.log("Got notification: ", type, event, data); * }, StackendWebSocketEvent.MESSAGE_RECEIVED, communityContext, USER_NOTIFICATION_COMPONENT); * }); * * // Get the global instance * const community: Community = ...; * const sws: StackendWebSocket = dispatch(getInstance(community.xcapCommunityName)); * * // Request data from the server * sws.send({ * communityContext: "stackend:notifications_site", * componentName: USER_NOTIFICATION_COMPONENT, * messageType: "GET_NUMBER_OF_UNSEEN", * }); * * // Subscribe to real time object notifications * sws.subscribe(new CommentsSubscription("comments", 123), (message: Message, payload: RealTimePayload) => { * console.log("Real time notification", message, payload); * }); */ var StackendWebSocket = /** @class */ (function () { /** * Construct a new web socket */ function StackendWebSocket(community, url) { var _this = this; this.debug = false; this.hasConnected = false; this.socket = null; this.isOpen = false; this.sendQueue = []; this.pongTimeoutId = null; /** Low level listeners. maps from broadcast id to array of listeners */ this.listeners = {}; this.realTimeListeners = {}; this.reconnectTimer = null; this.reconnectDelayMs = DEFAULT_RECONNECT_DELAY; this._onOpen = function () { _this._broadcast(StackendWebSocketEvent.SOCKET_OPENED); _this.isOpen = true; _this.reconnectDelayMs = DEFAULT_RECONNECT_DELAY; }; this._onMessage = function (m) { var message = JSON.parse(m.data); _this._broadcast(StackendWebSocketEvent.MESSAGE_RECEIVED, m, message); }; this._onClose = function (e) { _this._broadcast(StackendWebSocketEvent.SOCKET_CLOSED, e); if (_this.isOpen) { // Not closed by api. Assume error _this._scheduleReconnect(); } _this.isOpen = false; }; this._onError = function (e) { console.error('Stackend: WebSocket error: ', e); _this._broadcast(StackendWebSocketEvent.SOCKET_ERROR, e); _this._broadcast(StackendWebSocketEvent.SOCKET_CLOSED, e); _this.isOpen = false; _this._scheduleReconnect(); }; this._scheduleReconnect = function () { if (_this.reconnectTimer) { clearTimeout(_this.reconnectTimer); } console.debug('Stackend: WebSocket reconnect in ' + Math.round(_this.reconnectDelayMs / 1000) + 's'); _this.reconnectTimer = setTimeout(_this._doReconnect, _this.reconnectDelayMs); }; this._doReconnect = function () { var _a; if (_this.isOpen) { return; } console.debug('Stackend: WebSocket reconnecting'); (_a = _this.socket) === null || _a === void 0 ? void 0 : _a.close(); // May cause _onError to be invoked _this.socket = null; // Double the retry delay every time it fails _this.reconnectDelayMs = 2 * _this.reconnectDelayMs; try { _this.connect(); if (_this.socket == null) { _this._scheduleReconnect(); } } catch (e) { _this._scheduleReconnect(); } }; this.xcapCommunityName = community.xcapCommunityName; this.url = url || '/' + community.permalink + StackendWebSocket.DEFAULT_URL; } StackendWebSocket.prototype.getXcapCommunityName = function () { return this.xcapCommunityName; }; /** * Enable / disable debugging * @param debug */ StackendWebSocket.prototype.setDebug = function (debug) { this.debug = debug; }; /** * Connect */ StackendWebSocket.prototype.connect = function () { if (this.socket !== null) { return; } this._broadcast(StackendWebSocketEvent.SOCKET_OPENING); this.socket = new sockjs_client_1.default(this.url); this.socket.onopen = this._onOpen; this.socket.onmessage = this._onMessage; this.socket.onclose = this._onClose; this.socket.onerror = this._onError; this.hasConnected = true; }; StackendWebSocket.prototype.close = function () { var _a; this.isOpen = false; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close(); if (this.pongTimeoutId != null) { console.log('StackendWebSocket: Removing pong interval.'); clearInterval(this.pongTimeoutId); } delete instances[this.xcapCommunityName]; }; StackendWebSocket.prototype.validateMessage = function (message) { if (!message) { throw 'No message'; } if (!message.messageType) { throw 'messageType required: ' + JSON.stringify(message); } if (!message.componentName) { throw 'componentName required: ' + +JSON.stringify(message); } if (!message.communityContext || message.communityContext.startsWith('undefined:') || message.communityContext.endsWith(':undefined')) { throw 'communityContext required: ' + +JSON.stringify(message); } }; /** * Send a message * @param message */ StackendWebSocket.prototype.send = function (message) { this.validateMessage(message); if (this.debug) { console.log('send, hasConnected: ' + this.hasConnected); } // FIXME: Queue or fail if (!this.hasConnected) { return; } this.sendQueue.push(message); this._sendInternal(); }; /** * Send a ping */ StackendWebSocket.prototype.ping = function () { this.send({ communityContext: this.xcapCommunityName + ':' + exports.REALTIME_COMPONENT, componentName: exports.REALTIME_COMPONENT, messageType: RealTimeMessageType.PING }); }; StackendWebSocket.prototype._addRealTimeListenerForSubscription = function (subscription, listener) { return this._addRealTimeListener(subscription.getKey(), listener); }; StackendWebSocket.prototype._addRealTimeListenerForReference = function (component, obfuscatedReference, listener) { var key = this._getReferenceKey(component, obfuscatedReference); return this._addRealTimeListener(key, listener); }; StackendWebSocket.prototype._getReferenceKey = function (component, obfuscatedReference) { return 'ref:' + component + ':' + obfuscatedReference; }; StackendWebSocket.prototype._getReferenceData = function (key) { if (!key) { return null; } var m = key.match(/^ref:([^:]+):([^:]+)$/); if (m) { return { component: m[1], obfuscatedReference: m[2] }; } return null; }; StackendWebSocket.prototype._addRealTimeListener = function (key, listener) { var listeners = this.realTimeListeners[key]; if (!listeners) { listeners = []; this.realTimeListeners[key] = listeners; } if (listeners.indexOf(listener) == -1) { listeners.push(listener); return true; } return false; }; StackendWebSocket.prototype._removeRealTimeListener = function (key, listener) { var listeners = this.realTimeListeners[key]; if (listeners) { for (var i = 0; i < listeners.length; i++) { var l = listeners[i]; if (l === listener) { listeners.splice(i, 1); if (listeners.length === 0) { delete this.realTimeListeners[key]; } return true; } } } return false; }; StackendWebSocket.prototype._removeRealTimeListenerForSubscription = function (subscription, listener) { return this._removeRealTimeListener(subscription.getKey(), listener); }; StackendWebSocket.prototype._removeRealTimeListenerForReference = function (component, obfuscatedReference, listener) { var key = this._getReferenceKey(component, obfuscatedReference); return this._removeRealTimeListener(key, listener); }; StackendWebSocket.prototype._removeAllRealTimeListeners = function (context) { if (!context) { var n_1 = Object.keys(this.realTimeListeners).length; this.realTimeListeners = {}; return n_1; } var subRe = new RegExp('^sub:' + context + ':.*'); var refRe = new RegExp('^ref:[^:]+:' + context + ':.*'); var n = 0; for (var _i = 0, _a = Object.keys(this.realTimeListeners); _i < _a.length; _i++) { var key = _a[_i]; var l = this.realTimeListeners[key]; if (subRe.test(key) || refRe.test(key)) { if (l) { n += l.length; } delete this.realTimeListeners[key]; } } return n; }; /** * Subscribe to object creation/modification/deletion notifications * @param subscription * @param listener */ StackendWebSocket.prototype.subscribe = function (subscription, listener) { this._addRealTimeListenerForSubscription(subscription, listener); this.send({ communityContext: this.xcapCommunityName + ':' + exports.REALTIME_COMPONENT, componentName: exports.REALTIME_COMPONENT, messageType: RealTimeMessageType.SUBSCRIBE, payload: { function: subscription.component, context: subscription.context, referenceId: subscription.referenceId } }); }; /** * Subscribe to a set of references * @param component * @param context * @param obfuscatedReferences * @param listener */ StackendWebSocket.prototype.subscribeMultiple = function (component, context, obfuscatedReferences, listener) { var _this = this; obfuscatedReferences.forEach(function (r) { _this._addRealTimeListenerForReference(component, r, listener); }); this.send({ communityContext: this.xcapCommunityName + ':' + exports.REALTIME_COMPONENT, componentName: exports.REALTIME_COMPONENT, messageType: RealTimeMessageType.SUBSCRIBE, payload: { function: component, context: context, references: obfuscatedReferences } }); }; /* FIXME: Complete this _restoreSubscriptionsOnReconnect(): void { const subsByComponent: { [component: string]: any } = {}; Object.keys(this.realTimeListeners).forEach(k => { const listeners: Array<RealTimeListener> = this.realTimeListeners[k]; const rd = this._getReferenceData(k); if (rd) { subsByComponent[rd.component]; } }); } */ /** * Unsubscribe from object creation/modification/deletion notifications * @param subscription * @param listener */ StackendWebSocket.prototype.unsubscribe = function (subscription, listener) { this._removeRealTimeListenerForSubscription(subscription, listener); this.send({ communityContext: this.xcapCommunityName + ':' + exports.REALTIME_COMPONENT, componentName: exports.REALTIME_COMPONENT, messageType: RealTimeMessageType.UNSUBSCRIBE, payload: { function: subscription.component, context: subscription.context, referenceId: subscription.referenceId } }); }; /** * Unsubscribe from a set of references * @param component * @param context * @param obfuscatedReferences * @param listener */ StackendWebSocket.prototype.unsubscribeMultiple = function (component, context, obfuscatedReferences, listener) { var _this = this; obfuscatedReferences.forEach(function (r) { _this._removeRealTimeListenerForReference(component, r, listener); }); this.send({ communityContext: this.xcapCommunityName + ':' + exports.REALTIME_COMPONENT, componentName: exports.REALTIME_COMPONENT, messageType: RealTimeMessageType.UNSUBSCRIBE, payload: { function: component, context: context, references: obfuscatedReferences } }); }; /** * Unsubscribe from all real time notifications * @param context */ StackendWebSocket.prototype.unsubscribeAll = function (context) { if (!context) throw 'context must be supplied'; this._removeAllRealTimeListeners(context); this.send({ communityContext: this.xcapCommunityName + ':' + exports.REALTIME_COMPONENT, componentName: exports.REALTIME_COMPONENT, messageType: RealTimeMessageType.UNSUBSCRIBE_ALL, payload: { context: context } }); }; /** * Get a broadcast identifier. * @param type * @param context * @param componentName * @returns {string} */ StackendWebSocket.prototype._getBroadcastIdentifier = function (type, context, componentName) { var key = ''; if (type) { key += type; } else { key = '*'; } if (context) { if (componentName) { key += '-' + context + '-' + componentName; } else { throw 'Both context and componentName must be specified'; } } return key; }; StackendWebSocket.prototype._sendInternal = function () { var _this = this; if (this.socket && this.isOpen && this.sendQueue.length > 0) { while (this.sendQueue.length > 0) { var message = this.sendQueue.shift(); var x = __assign({}, message); if (x) { if (x.payload) { // FIXME: This double encoding is stupid x.payload = JSON.stringify(x.payload); } if (this.debug) { console.log('Sending message:', x); } this.socket.send(JSON.stringify(x)); } } } else { setTimeout(function () { _this._sendInternal(); }, 1000); } }; /** * Call all listeners that matches * @param type * @param message * @param event */ StackendWebSocket.prototype._broadcast = function (type, event, message) { var identifier = null; if (message) { var cc = (0, CommunityContext_1.parseCommunityContext)(message === null || message === void 0 ? void 0 : message.communityContext); identifier = this._getBroadcastIdentifier(type, cc === null || cc === void 0 ? void 0 : cc.context, message.componentName); } else { identifier = this._getBroadcastIdentifier(type); } if (this.debug) { console.log('Broadcasting', type, identifier, message); } var a = this.listeners[identifier]; var n = 0; if (a) { a.forEach(function (f) { n++; f(type, event, message); }); } // Catch all listener var b = this.listeners['*']; if (b) { b.forEach(function (f) { n++; f(type, event, message); }); } // Real time listeners if (message != null && type === StackendWebSocketEvent.MESSAGE_RECEIVED) { switch (message.messageType) { case RealTimeMessageType.OBJECT_CREATED: case RealTimeMessageType.OBJECT_MODIFIED: case RealTimeMessageType.OBJECT_REMOVED: { var payload_1 = JSON.parse(message.payload); var subKey = this._getSubscriptionKey(payload_1); var listeners = this.realTimeListeners[subKey]; if (listeners) { listeners.forEach(function (l) { n++; l(message, payload_1); }); } if (payload_1.obfuscatedReference) { var refKey = this._getReferenceKey(payload_1.component, payload_1.obfuscatedReference); var listeners_1 = this.realTimeListeners[refKey]; if (listeners_1) { listeners_1.forEach(function (l) { n++; l(message, payload_1); }); } } break; } default: break; } } if (this.debug) { console.log(type, identifier + ' delivered to ' + n + ' listeners'); } }; StackendWebSocket.prototype._getSubscriptionKey = function (payload) { // Should be context + ':' + component + ':' + referenceId; var cc = (0, CommunityContext_1.parseCommunityContext)(payload.communityContext); return 'sub:' + (cc === null || cc === void 0 ? void 0 : cc.context) + ':' + payload.component + ':' + payload.referenceId; }; /** * Add a listener that receives broadcasts. * For a catch all listener: sws.addListener(listener); * To a catch a specific event in all contexts: sws.addListener(listener, StackendWebSocketEvent.SOCKET_CLOSED); * To catch events for specific components ws.addListener(listener, StackendWebSocketEvent.MESSAGE_RECEIVED, 'stackend:notifications_site', 'usernotification'); * * @param listener: Listener * @param type * @param communityContext as returned by getBroadcastIdentifier * @param componentName */ StackendWebSocket.prototype.addListener = function (listener, type, communityContext, componentName) { if (typeof listener !== 'function') { throw 'Listener must be a function'; } var broadcastIdentifier = this._getBroadcastIdentifier(type, communityContext, componentName); var a = this.listeners[broadcastIdentifier]; if (!a) { a = []; this.listeners[broadcastIdentifier] = a; } a.push(listener); }; /** * Short hand for adding a listener for StackendWebSocketEvent.MESSAGE_RECEIVED * @param listener * @param communityContext * @param componentName */ StackendWebSocket.prototype.addMessageListener = function (listener, communityContext, componentName) { this.addListener(listener, StackendWebSocketEvent.MESSAGE_RECEIVED, communityContext, componentName); }; StackendWebSocket.DEFAULT_URL = '/spring/ws'; return StackendWebSocket; }()); exports.default = StackendWebSocket; var instances = {}; var initializers = []; /** * Add a global initializer used when getInstance() creates a StackendWebSocket * @param initializer */ function addInitializer(initializer) { initializers.push(initializer); } exports.addInitializer = addInitializer; /** * Remove an initializer * @param initializer */ function removeInitializer(initializer) { for (var i = 0; i < initializers.length; i++) { var init = initializers[i]; if (init === initializer) { initializers.splice(i, 1); return true; } initializers.push(initializer); } return false; } exports.removeInitializer = removeInitializer; /** * Get the community instance. All registered initializers are run */ function getInstance(community) { return function (dispatch, getState) { if (!(community === null || community === void 0 ? void 0 : community.id)) throw 'Community required'; var instance = instances[community.xcapCommunityName]; if (!instance) { var config = getState().config; var url = config.server + config.contextPath + '/' + community.permalink + StackendWebSocket.DEFAULT_URL; console.debug('Stackend: Creating StackendWebSocket for ' + community.xcapCommunityName + ', ' + url); var sws_1 = new StackendWebSocket(community, url); for (var _i = 0, initializers_1 = initializers; _i < initializers_1.length; _i++) { var init = initializers_1[_i]; init(sws_1); } sws_1.connect(); instances[community.xcapCommunityName] = sws_1; instance = sws_1; // console.log('StackendWebSocket: Adding pong listener.'); // sws.addMessageListener( // (type, event, message) => { // if (message && message.messageType === 'PONG') { // console.log('Got Pong: ', type, event, message); // } // }, // REALTIME_COMPONENT, // REALTIME_COMPONENT // ); // console.log('StackendWebSocket: Adding ping interval.'); var pongTimeoutId = setInterval(function () { sws_1.ping(); }, 60 * 1000); sws_1.pongTimeoutId = pongTimeoutId; } return instance; }; } exports.getInstance = getInstance; /** * Shut down and remove the community instance */ function removeInstance(xcapCommunityName) { var instance = instances[xcapCommunityName]; if (instance) { instance.close(); delete instances[xcapCommunityName]; return true; } return false; } exports.removeInstance = removeInstance; /** * Remove all instances */ function removeAll() { var n = 0; for (var _i = 0, _a = Object.keys(instances); _i < _a.length; _i++) { var xcapCommunityName = _a[_i]; if (removeInstance(xcapCommunityName)) { n++; } } return n; } exports.removeAll = removeAll; //# sourceMappingURL=StackendWebSocket.js.map