@stackend/api
Version:
JS bindings to api.stackend.com
751 lines • 28.9 kB
JavaScript
"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