@qikdev/sdk
Version:
Promise based Javascript SDK
458 lines (341 loc) • 22.9 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8">
<title>qik.socket.js - Documentation</title>
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
<script src="scripts/nav.js" defer></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
<label for="nav-trigger" class="navicon-button x">
<div class="navicon"></div>
</label>
<label for="nav-trigger" class="overlay"></label>
<nav >
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="access.html">access</a><ul class='methods'><li data-type='method'><a href="access.html#.actionableScopes">actionableScopes</a></li><li data-type='method'><a href="access.html#.allPermissions">allPermissions</a></li><li data-type='method'><a href="access.html#.allPermissionTypes">allPermissionTypes</a></li><li data-type='method'><a href="access.html#.allUserScopes">allUserScopes</a></li><li data-type='method'><a href="access.html#.canCreate">canCreate</a></li><li data-type='method'><a href="access.html#.canDeleteItem">canDeleteItem</a></li><li data-type='method'><a href="access.html#.canEditItem">canEditItem</a></li><li data-type='method'><a href="access.html#.canEraseItem">canEraseItem</a></li><li data-type='method'><a href="access.html#.canKnowOf">canKnowOf</a></li><li data-type='method'><a href="access.html#.canListItem">canListItem</a></li><li data-type='method'><a href="access.html#.canRestoreItem">canRestoreItem</a></li><li data-type='method'><a href="access.html#.canViewItem">canViewItem</a></li><li data-type='method'><a href="access.html#.checkActionAccess">checkActionAccess</a></li><li data-type='method'><a href="access.html#.getAllDescendants">getAllDescendants</a></li><li data-type='method'><a href="access.html#.has">has</a></li><li data-type='method'><a href="access.html#.hasFeature">hasFeature</a></li><li data-type='method'><a href="access.html#.hashPermissions">hashPermissions</a></li><li data-type='method'><a href="access.html#.isAdministrator">isAdministrator</a></li><li data-type='method'><a href="access.html#.isAssignee">isAssignee</a></li><li data-type='method'><a href="access.html#.isOwner">isOwner</a></li></ul></li><li><a href="api.html">api</a><ul class='methods'><li data-type='method'><a href="api.html#.generateEndpointURL">generateEndpointURL</a></li></ul></li><li><a href="auth.html">auth</a><ul class='methods'><li data-type='method'><a href="auth.html#.changeOrganisation">changeOrganisation</a></li><li data-type='method'><a href="auth.html#.ensureValidToken">ensureValidToken</a></li><li data-type='method'><a href="auth.html#.getCurrentToken">getCurrentToken</a></li><li data-type='method'><a href="auth.html#.getCurrentUser">getCurrentUser</a></li><li data-type='method'><a href="auth.html#.impersonate">impersonate</a></li><li data-type='method'><a href="auth.html#.login">login</a></li><li data-type='method'><a href="auth.html#.logout">logout</a></li><li data-type='method'><a href="auth.html#.retrieveUserFromResetToken">retrieveUserFromResetToken</a></li><li data-type='method'><a href="auth.html#.sendResetPasswordRequest">sendResetPasswordRequest</a></li><li data-type='method'><a href="auth.html#.set">set</a></li><li data-type='method'><a href="auth.html#.sync">sync</a></li><li data-type='method'><a href="auth.html#.updateUserWithToken">updateUserWithToken</a></li></ul></li><li><a href="cache.html">cache</a><ul class='methods'><li data-type='method'><a href="cache.html#.get">get</a></li><li data-type='method'><a href="cache.html#.reset">reset</a></li></ul></li><li><a href="content.html">content</a><ul class='methods'><li data-type='method'><a href="content.html#.create">create</a></li><li data-type='method'><a href="content.html#.delete">delete</a></li><li data-type='method'><a href="content.html#.get">get</a></li><li data-type='method'><a href="content.html#.getFromSlug">getFromSlug</a></li><li data-type='method'><a href="content.html#.glossary">glossary</a></li><li data-type='method'><a href="content.html#.list">list</a></li><li data-type='method'><a href="content.html#.patch">patch</a></li><li data-type='method'><a href="content.html#.restore">restore</a></li><li data-type='method'><a href="content.html#.scopeGlossary">scopeGlossary</a></li><li data-type='method'><a href="content.html#.update">update</a></li><li data-type='method'><a href="content.html#.validateField">validateField</a></li><li data-type='method'><a href="content.html#.variables">variables</a></li></ul></li><li><a href="files.html">files</a><ul class='methods'><li data-type='method'><a href="files.html#.downloadUrl">downloadUrl</a></li><li data-type='method'><a href="files.html#.duration">duration</a></li><li data-type='method'><a href="files.html#.filesize">filesize</a></li><li data-type='method'><a href="files.html#.getBinaryTypeFromMime">getBinaryTypeFromMime</a></li><li data-type='method'><a href="files.html#.mediaUrl">mediaUrl</a></li></ul></li><li><a href="filter.html">filter</a><ul class='methods'><li data-type='method'><a href="filter.html#.activeFilterComparators">activeFilterComparators</a></li><li data-type='method'><a href="filter.html#.activeFilterKeys">activeFilterKeys</a></li><li data-type='method'><a href="filter.html#.activeFilterOperators">activeFilterOperators</a></li><li data-type='method'><a href="filter.html#.activeFilters">activeFilters</a></li><li data-type='method'><a href="filter.html#.activeFilterValues">activeFilterValues</a></li><li data-type='method'><a href="filter.html#.filterChangeString">filterChangeString</a></li><li data-type='method'><a href="filter.html#.isValidFilter">isValidFilter</a></li></ul></li><li><a href="geo.html">geo</a><ul class='methods'><li data-type='method'><a href="geo.html#.distance">distance</a></li><li data-type='method'><a href="geo.html#.getDefaultCountry">getDefaultCountry</a></li><li data-type='method'><a href="geo.html#.getDefaultTimezone">getDefaultTimezone</a></li><li data-type='method'><a href="geo.html#.getPosition">getPosition</a></li><li data-type='method'><a href="geo.html#.stopWatchPosition">stopWatchPosition</a></li><li data-type='method'><a href="geo.html#.watchPosition">watchPosition</a></li></ul></li><li><a href="module-QikStorage-CookieStorageAdapter.html">CookieStorageAdapter</a><ul class='methods'><li data-type='method'><a href="module-QikStorage-CookieStorageAdapter.html#getCookieOptions">getCookieOptions</a></li><li data-type='method'><a href="module-QikStorage-CookieStorageAdapter.html#isCookieMode">isCookieMode</a></li></ul></li><li><a href="module-QikStorage-LocalStorageAdapter.html">LocalStorageAdapter</a></li><li><a href="module-QikStorage-StorageAdapter.html">StorageAdapter</a><ul class='methods'><li data-type='method'><a href="module-QikStorage-StorageAdapter.html#clearUser">clearUser</a></li><li data-type='method'><a href="module-QikStorage-StorageAdapter.html#getAccessToken">getAccessToken</a></li><li data-type='method'><a href="module-QikStorage-StorageAdapter.html#getRefreshToken">getRefreshToken</a></li><li data-type='method'><a href="module-QikStorage-StorageAdapter.html#getTokenExpiry">getTokenExpiry</a></li><li data-type='method'><a href="module-QikStorage-StorageAdapter.html#getUser">getUser</a></li><li data-type='method'><a href="module-QikStorage-StorageAdapter.html#isAvailable">isAvailable</a></li><li data-type='method'><a href="module-QikStorage-StorageAdapter.html#setUser">setUser</a></li></ul></li><li><a href="socket.html">socket</a><ul class='methods'><li data-type='method'><a href="socket.html#.channel">channel</a></li><li data-type='method'><a href="socket.html#.close">close</a></li><li data-type='method'><a href="socket.html#.connect">connect</a></li><li data-type='method'><a href="socket.html#.reconnect">reconnect</a></li><li data-type='method'><a href="socket.html#.subscribe">subscribe</a></li><li data-type='method'><a href="socket.html#.unsubscribe">unsubscribe</a></li></ul></li><li><a href="system.html">system</a><ul class='methods'><li data-type='method'><a href="system.html#.countries">countries</a></li></ul></li><li><a href="utils.html">utils</a><ul class='methods'><li data-type='method'><a href="utils.html#.cleanValue">cleanValue</a></li><li data-type='method'><a href="utils.html#.comma">comma</a></li><li data-type='method'><a href="utils.html#.errorMessage">errorMessage</a></li><li data-type='method'><a href="utils.html#.exists">exists</a></li><li data-type='method'><a href="utils.html#.extractFromArray">extractFromArray</a></li><li data-type='method'><a href="utils.html#.formatCurrency">formatCurrency</a></li><li data-type='method'><a href="utils.html#.getTypeFromID">getTypeFromID</a></li><li data-type='method'><a href="utils.html#.guid">guid</a></li><li data-type='method'><a href="utils.html#.hash">hash</a></li><li data-type='method'><a href="utils.html#.id">id</a></li><li data-type='method'><a href="utils.html#.ids">ids</a></li><li data-type='method'><a href="utils.html#.isValidEmailAddress">isValidEmailAddress</a></li><li data-type='method'><a href="utils.html#.loadExternalScript">loadExternalScript</a></li><li data-type='method'><a href="utils.html#.loadExternalStyle">loadExternalStyle</a></li><li data-type='method'><a href="utils.html#.machineName">machineName</a></li><li data-type='method'><a href="utils.html#.mapParameters">mapParameters</a></li><li data-type='method'><a href="utils.html#.matchInArray">matchInArray</a></li><li data-type='method'><a href="utils.html#.parseBoolean">parseBoolean</a></li><li data-type='method'><a href="utils.html#.parseDate">parseDate</a></li><li data-type='method'><a href="utils.html#.parseEmail">parseEmail</a></li><li data-type='method'><a href="utils.html#.parseInt">parseInt</a></li><li data-type='method'><a href="utils.html#.parseNumber">parseNumber</a></li><li data-type='method'><a href="utils.html#.parseURL">parseURL</a></li></ul></li></ul><h3>Modules</h3><ul><li><a href="module-QikStorage.html">QikStorage</a><ul class='methods'><li data-type='method'><a href="module-QikStorage.html#.createStorageAdapter">createStorageAdapter</a></li></ul></li></ul><h3>Global</h3><ul><li><a href="global.html#EventDispatcher">EventDispatcher</a></li></ul>
</nav>
<div id="main">
<h1 class="page-title">qik.socket.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import { EventDispatcher } from './qik.utils.js';
///////////////////////////////////////////////////
/**
* Creates a new instance of QikSocket a module of the SDK
* that contains all helper functions to do with realtime sockets
* @name socket
* @constructor
* @hideconstructor
* @param {QikAPI} qik A reference to the parent instance of the QikCore module. This module is usually created by a QikCore instance that passes itself in as the first argument.
*/
const QikSocket = function(qik, mode) {
mode = mode || 'production';
if (!qik.auth) {
throw new Error(`Can't Instantiate QikSocket before QikAuth exists`);
}
const windowID = qik.utils.guid();
///////////////////////////////////////////////////
let service = {
debug: false,
url: `wss://iqtm6zjz3l.execute-api.ap-southeast-2.amazonaws.com/${mode}`,
connected: false,
windowID,
autoreconnect:true,
}
let socket;
let timer;
let buckets = {};
const channels = {};
//Create a new dispatcher
const dispatcher = new EventDispatcher();
dispatcher.bootstrap(service);
///////////////////////////////////////////////////
function getSubscriptions() {
return Object.keys(buckets);
}
///////////////////////////////////////////////////
/**
* @description Create an event dispatcher instance and subscribe to a new socket channel.
* This can then be used to receive socket events for this channel
* @alias socket.channel
* @param {String} key The key or name of the channel to subscribe to
* @example
* const socketChannel = await sdk.socket.channel('awesome:notifications')
* socketChannel.addEventListener('message', function(message) {
* console.log('Received a new message from the socket')
* })
*
* // Disconnect from this channel and remove all listeners
* socketChannel.destroy();
*
*/
service.channel = async function(key) {
if (buckets[key]) {
return buckets[key];
}
const bucket = new EventDispatcher();
function dispatchMessage(message) {
bucket.dispatch('message', message);
}
// Destroy this channel - stop receiving its events and tell the
// server to unsubscribe. Only touches this channel's own listeners,
// leaving every other channel (and connection-level listeners) intact.
bucket.destroy = function() {
service.removeEventListener(key, dispatchMessage);
service.unsubscribe(key);
bucket.removeAllListeners();
delete buckets[key];
}
// Cache the fully-formed bucket (with destroy attached) BEFORE the
// connect() await below. A concurrent channel(key) call can hit the
// early return above while we're suspended awaiting connect(); if we
// registered the bucket before assigning destroy, that caller would
// receive a bucket whose destroy() is missing and throw on teardown.
buckets[key] = bucket;
if (!service.connected) {
// Return the socket
await service.connect();
}
// Subscribe to this channel
service.subscribe(key);
// Listen for all relevant events
service.addEventListener(key, dispatchMessage);
return buckets[key];
}
///////////////////////////////////////////////////
function ping() {
broadcast({
action: 'ping',
})
}
function startHeartbeat() {
if (timer) {
return;
}
timer = setInterval(ping, 10000);
}
function stopHeartbeat() {
if (!timer) {
return;
}
clearInterval(timer)
timer = null;
}
///////////////////////////////////////////////////
function socketOpened(event) {
service.debug ? console.log("[socket] Connection open", event) : null;
service.connected = true;
dispatcher.dispatch('connected', event);
// startHeartbeat();
}
async function socketClosed(event) {
if (event.wasClean) {
service.debug ? console.log(`[socket] Connection closed cleanly, code=${event.code} reason=${event.reason}`) : null;
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
service.debug ? console.log('[socket] - connection closed due to error', event) : null
}
service.connected = false;
dispatcher.dispatch('disconnected', event);
socket = null;
if(service.autoreconnect) {
service.reconnect();
}
}
function socketError(error) {
console.log('[event] socketError', event);
service.debug ? console.log("[socket] Error", error) : null;
dispatcher.dispatch('error', error);
}
function socketMessageReceived(event) {
const eventData = JSON.parse(event.data);
service.debug ? console.log("[socket] message received", eventData) : null;
// Dispatch a generic message
dispatcher.dispatch('message', eventData);
if (eventData.channel) {
dispatcher.dispatch(eventData.channel, eventData);
}
}
async function broadcast(data) {
socket.send(JSON.stringify(data));
}
///////////////////////////////////////////////////
function wait() {
return new Promise(function(resolve) {
function check() {
if (service.connected) {
return resolve(socket);
}
setTimeout(check, 1000);
}
check();
})
}
///////////////////////////////////////////////////
/**
* @description Connect to the socket service
* @alias socket.connect
* @example
* const connected = await sdk.socket.connect()
*
*/
service.connect = async function() {
if (socket) {
if (!socket.connected) {
return wait();
}
console.log('[socket] - Socket is already connected');
return socket;
}
const accessToken = qik.auth.getCurrentToken();
if (!accessToken) {
service.debug ? console.log('[socket] - Must be authenticated to connect to socket') : null;
return;
}
socket = new WebSocket(`${service.url}?access_token=${accessToken}&windowid=${windowID}`);
socket.addEventListener('close', socketClosed);
socket.addEventListener('error', socketError);
socket.addEventListener('open', socketOpened);
socket.addEventListener('message', socketMessageReceived);
service.debug ? console.log('[socket] - Connected with event listeners') : null;
return wait();
}
/**
* @description Reconnect to the socket service
* @alias socket.reconnect
* @example
* const connected = await sdk.socket.reconnect()
*
*/
service.reconnect = async function() {
if(service.connected) {
return;
}
service.debug ? console.log(`[socket] reconnecting...`) : null;
// Reconnect all listeners
const result = await service.connect();
// Resubscribe to all previously connected channels
const subscribedChannels = Object.keys(channels);
subscribedChannels.forEach(function(channel) {
service.subscribe(channel);
})
return result;
}
///////////////////////////////////////////////////
/**
* @description Close the connection to the socket service
* @alias socket.close
* @example
* const closed = await sdk.socket.close()
*
*/
service.close = async function() {
if (!socket) {
console.log('[socket] - Socket is not connected');
return;
}
const previousSetting = service.autoreconnect;
service.autoreconnect = false;
socket.close();
socket.removeEventListener('close', socketClosed);
socket.removeEventListener('error', socketError);
socket.removeEventListener('open', socketOpened);
socket.removeEventListener('message', socketMessageReceived);
socket = null;
socketClosed({ wasClean: true });
service.autoreconnect = previousSetting;
}
service.disconnect = service.close;
///////////////////////////////////////////////////
/**
* @description Subscribe to a socket channel
* @alias socket.subscribe
* @param {String} key The name of the channel to subscribe to
* @example
* const subscribed = await sdk.socket.subscribe('some:cool:alert')
*
*/
service.subscribe = async function(channel) {
if (!socket) {
console.log(`[socket] - Can't subscribe to channel as socket is not connected`);
return;
}
broadcast({
action: 'subscribe',
channel,
})
channels[channel] = true;
dispatcher.dispatch('subscribe', channel)
service.debug ? console.log(`[socket] - subscribed to ${channel}`) : null;
}
///////////////////////////////////////////////////
/**
* @description Unsubscribe from a socket channel
* @alias socket.unsubscribe
* @param {String} key The name of the channel to unsubscribe from
* @example
* const unsubscribed = await sdk.socket.unsubscribe('some:cool:alert')
*
*/
service.unsubscribe = async function(channel) {
if (!socket) {
console.log(`[socket] - Can't unsubscribe from channel as socket is not connected`);
return;
}
broadcast({
action: 'unsubscribe',
channel,
})
delete channels[channel];
dispatcher.dispatch('unsubscribe', channel)
service.debug ? console.log(`[socket] - unsubscribed from ${channel}`) : null;
}
///////////////////////////////////////////////////
// Reconnect the socket whenever the authenticated organisation changes.
// The WebSocket authenticates with an access token at connect time, so the
// connection stays bound to whichever organisation was active when it
// opened. Switching organisations issues a new token but leaves the socket
// untouched, so without this it keeps streaming the previous
// organisation's events. Reconnecting re-authenticates with the new token.
let currentOrganisationID = qik.utils.id(qik.auth.getCurrentUser()?.organisation);
qik.auth.addEventListener('change', async function(user) {
const organisationID = qik.utils.id(user?.organisation);
// Ignore auth changes that don't change the organisation (e.g. routine
// access token refreshes) so we don't churn the connection needlessly.
if (organisationID === currentOrganisationID) {
return;
}
currentOrganisationID = organisationID;
// Nothing to reconnect if the socket was never opened.
if (!socket) {
return;
}
service.debug ? console.log('[socket] - organisation changed, reconnecting') : null;
await service.close();
await service.reconnect();
});
///////////////////////////////////////////////////
return service;
}
export default QikSocket;</code></pre>
</article>
</section>
</div>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.11</a> on Mon Jul 13 2026 18:49:48 GMT+1000 (Australian Eastern Standard Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
</footer>
<script>prettyPrint();</script>
<script src="scripts/polyfill.js"></script>
<script src="scripts/linenumber.js"></script>
</body>
</html>