@awcrotwell/motion
Version:
Motion allows you to build reactive, real-time frontend UI components in your Amber application using pure Crystal that are reusable, testable & encapsulated. For brevity, we will call them MotionComponents.
255 lines (254 loc) • 8.26 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Socket = exports.Channel = void 0;
/* eslint-disable */
/* tslint:disable */
const EVENTS = {
join: 'join',
leave: 'leave',
message: 'message',
};
const STALE_CONNECTION_THRESHOLD_SECONDS = 100;
const SOCKET_POLLING_RATE = 10000;
/**
* Returns a numeric value for the current time
*/
const now = () => new Date().getTime();
/**
* Returns the difference between the current time and passed `time` in seconds
* @param {Number|Date} time - A numeric time or date object
*/
const secondsSince = (time) => (now() - time) / 1000;
/**
* Class for channel related functions (joining, leaving, subscribing and sending messages)
*/
class Channel {
/**
* @param {String} topic - topic to subscribe to
* @param {Socket} socket - A Socket instance
*/
constructor(topic, socket) {
this.topic = topic;
this.socket = socket;
this.onMessageHandlers = [];
}
/**
* Join a channel, subscribe to all channels messages
*/
join() {
this.socket.ws.send(JSON.stringify(Object.assign({ event: EVENTS.join, topic: this.topic }, arguments[0])));
}
/**
* Leave a channel, stop subscribing to channel messages
*/
leave() {
this.socket.ws.send(JSON.stringify({ event: EVENTS.leave, topic: this.topic }));
}
/**
* Calls all message handlers with a matching subject
*/
handleMessage(msg) {
this.onMessageHandlers.forEach((handler) => {
if (handler.subject === msg.subject)
handler.callback(msg.payload);
});
}
/**
* Subscribe to a channel subject
* @param {String} subject - subject to listen for: `msg:new`
* @param {function} callback - callback function when a new message arrives
*/
on(subject, callback) {
this.onMessageHandlers.push({ subject, callback });
}
/**
* Send a new message to the channel
* @param {String} subject - subject to send message to: `msg:new`
* @param {Object} payload - payload object: `{message: 'hello'}`
*/
push(subject, payload) {
this.socket.ws.send(JSON.stringify({
event: EVENTS.message, topic: this.topic, subject, payload,
}));
}
}
exports.Channel = Channel;
/**
* Class for maintaining connection with server and maintaining channels list
*/
class Socket {
/**
* @param {String} endpoint - Websocket endpont used in routes.cr file
*/
constructor(endpoint) {
this.endpoint = endpoint;
this.ws = null;
this.channels = [];
this.lastPing = now();
this.reconnectTries = 0;
this.attemptReconnect = true;
}
/**
* Returns whether or not the last received ping has been past the threshold
*/
_connectionIsStale() {
return secondsSince(this.lastPing) > STALE_CONNECTION_THRESHOLD_SECONDS;
}
/**
* Tries to reconnect to the websocket server using a recursive timeout
*/
_reconnect() {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = setTimeout(() => {
this.reconnectTries++;
this.connect(this.params);
this._reconnect();
}, this._reconnectInterval());
}
/**
* Returns an incrementing timeout interval based around the number of reconnection retries
*/
_reconnectInterval() {
return [1000, 2000, 5000, 10000][this.reconnectTries] || 10000;
}
/**
* Sets a recursive timeout to check if the connection is stale
*/
_poll() {
this.pollingTimeout = setTimeout(() => {
if (this._connectionIsStale()) {
this._reconnect();
}
else {
this._poll();
}
}, SOCKET_POLLING_RATE);
}
/**
* Clear polling timeout and start polling
*/
_startPolling() {
clearTimeout(this.pollingTimeout);
this._poll();
}
/**
* Sets `lastPing` to the curent time
*/
_handlePing() {
this.lastPing = now();
}
/**
* Clears reconnect timeout, resets variables an starts polling
*/
_reset() {
clearTimeout(this.reconnectTimeout);
this.reconnectTries = 0;
this.attemptReconnect = true;
this._startPolling();
}
/**
* Connect the socket to the server, and binds to native ws functions
* @param {Object} params - Optional parameters
* @param {String} params.location - Hostname to connect to, defaults to `window.location.hostname`
* @param {String} parmas.port - Port to connect to, defaults to `window.location.port`
* @param {String} params.protocol - Protocol to use, either 'wss' or 'ws'
*/
connect(params) {
this.params = params;
const opts = {
location: window.location.hostname,
port: window.location.port,
protocol: window.location.protocol === 'https:' ? 'wss:' : 'ws:',
};
if (params)
Object.assign(opts, params);
if (opts.port)
opts.location += `:${opts.port}`;
return new Promise((resolve, reject) => {
this.ws = new WebSocket(`${opts.protocol}//${opts.location}${this.endpoint}`);
this.ws.onmessage = (msg) => { this.handleMessage(msg); };
this.ws.onclose = () => {
if (this.attemptReconnect)
this._reconnect();
};
this.ws.onopen = () => {
this._reset();
resolve();
};
});
}
/**
* Closes the socket connection permanently
*/
disconnect() {
this.attemptReconnect = false;
clearTimeout(this.pollingTimeout);
clearTimeout(this.reconnectTimeout);
this.ws.close();
}
/**
* Adds a new channel to the socket channels list
* @param {String} topic - Topic for the channel: `chat_room:123`
*/
channel(topic) {
const channel = new Channel(topic, this);
this.channels.push(channel);
return channel;
}
/**
* Message handler for messages received
* @param {MessageEvent} msg - Message received from ws
*/
handleMessage(msg) {
if (msg.data === 'ping')
return this._handlePing();
const parsed_msg = JSON.parse(msg.data);
this.channels.forEach((channel) => {
if (channel.topic === parsed_msg.topic)
channel.handleMessage(parsed_msg);
});
}
}
exports.Socket = Socket;
exports.default = {
Channel,
Socket,
};
/**
* Allows delete links to post for security and ease of use similar to Rails jquery_ujs
*/
document.addEventListener('DOMContentLoaded', () => {
const elements = document.querySelectorAll("a[data-method='delete']");
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', (e) => {
e.preventDefault();
const message = elements[i].getAttribute('data-confirm') || 'Are you sure?';
if (confirm(message)) {
const form = document.createElement('form');
const input = document.createElement('input');
form.setAttribute('action', elements[i].getAttribute('href'));
form.setAttribute('method', 'POST');
input.setAttribute('type', 'hidden');
input.setAttribute('name', '_method');
input.setAttribute('value', 'DELETE');
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
return false;
});
}
});
if (!Date.prototype.toGranite) {
(function () {
function pad(number) {
if (number < 10) {
return `0${number}`;
}
return number;
}
Date.prototype.toGranite = function () {
return `${this.getUTCFullYear()}-${pad(this.getUTCMonth() + 1)}-${pad(this.getUTCDate())} ${pad(this.getUTCHours())}:${pad(this.getUTCMinutes())}:${pad(this.getUTCSeconds())}`;
};
}());
}