import stampit from 'stampit'; import {Meta, Model} from './base'; import Request from '../request'; import {EventEmittable} from '../utils'; import _ from 'lodash'; import QuerySet from '../querySet'; const ChannelQuerySet = stampit().compose(QuerySet).methods({ /** * Publishes to a channel. * @memberOf QuerySet * @instance * @param {Object} channel * @param {Object} message * @param {String} [room = null] * @returns {QuerySet} * @example {@lang javascript} * Channel.please().publish({ instanceName: 'test-instace', name: 'test-class' }, { content: 'my message'}); */ publish(properties, message, room = null) { this.properties = _.assign({}, this.properties, properties); this.payload = {payload: JSON.stringify(message)}; if (room) { this.payload.room = room; } this.method = 'POST'; this.endpoint = 'publish'; return this; }, /** * Allows polling of a channel. * @memberOf QuerySet * @instance * @param {Object} options * @param {Boolean} [start = true] * @returns {ChannelPoll} * @example {@lang javascript} * var poll = Channel.please().poll({ instanceName: 'test-instace', name: 'test-class' }); * * poll.on('start', function() { * console.log('poll::start'); * }); * * poll.on('stop', function() { * console.log('poll::stop'); * }); * * poll.on('message', function(message) { * console.log('poll::message', message); * }); * * poll.on('custom', function(message) { * console.log('poll::custom', message); * }); * * poll.on('create', function(data) { * console.log('poll::create', data); * }); * * poll.on('delete', function(data) { * console.log('poll::delete', data); * }); * * poll.on('update', function(data) { * console.log('poll::update', data); * }); * * poll.on('error', function(error) { * console.log('poll::error', error); * }); * * poll.start(); * */ poll(properties = {}, options = {}, start = true) { this.properties = _.assign({}, this.properties, properties); const config = this.getConfig(); const meta = this.model.getMeta(); const path = meta.resolveEndpointPath('poll', this.properties); options.path = path; const channelPoll = ChannelPoll.setConfig(config)(options); if (start === true) { channelPoll.start(); } return channelPoll; }, history(properties = {}, query = {}) { this.properties = _.assign({}, this.properties, properties); this.method = 'GET'; this.endpoint = 'history'; this.query = query; this._serialize = false; return this; } }); const ChannelMeta = Meta({ name: 'channel', pluralName: 'channels', endpoints: { 'detail': { 'methods': ['delete', 'patch', 'put', 'get'], 'path': '/v1.1/instances/{instanceName}/channels/{name}/' }, 'list': { 'methods': ['post', 'get'], 'path': '/v1.1/instances/{instanceName}/channels/' }, 'poll': { 'methods': ['get'], 'path': '/v1.1/instances/{instanceName}/channels/{name}/poll/' }, 'publish': { 'methods': ['post'], 'path': '/v1.1/instances/{instanceName}/channels/{name}/publish/' }, 'history': { 'methods': ['get'], 'path': '/v1.1/instances/{instanceName}/channels/{name}/history/' } } }); const channelConstraints = { instanceName: { presence: true, length: { minimum: 5 } }, name: { presence: true, string: true, length: { minimum: 5 } }, description: { string: true }, type: { inclusion: ['default', 'separate_rooms'] }, group: { numericality: true }, group_permissions: { inclusion: ['none', 'subscribe', 'publish'] }, other_permissions: { inclusion: ['none', 'subscribe', 'publish'] }, custom_publish: { boolean: true } }; /** * Wrapper around {@link http://docs.syncano.io/v0.1/docs/channels-poll|channels poll} endpoint which implements `EventEmitter` interface. * Use it via `Channel` poll method. * @constructor * @type {ChannelPoll} * @property {Number} [timeout = 300000] 5 mins * @property {String} [path = null] request path * @property {Number} [lastId = null] used internally in for loop * @property {Number} [room = null] * @property {Boolean} [abort = false] used internally to conrole for loop * @example {@lang javascript} * var poll = ChannelPoll.setConfig(config)({ * path: '/v1.1/instances/some-instance/channels/some-channel/poll/' * }); * * poll.on('start', function() { * console.log('poll::start'); * }); * * poll.on('stop', function() { * console.log('poll::stop'); * }); * * poll.on('message', function(message) { * console.log('poll::message', message); * }); * * poll.on('custom', function(message) { * console.log('poll::custom', message); * }); * * poll.on('create', function(data) { * console.log('poll::create', data); * }); * * poll.on('delete', function(data) { * console.log('poll::delete', data); * }); * * poll.on('update', function(data) { * console.log('poll::update', data); * }); * * poll.on('error', function(error) { * console.log('poll::error', error); * }); * * poll.start(); * */ export const ChannelPoll = stampit() .compose(Request, EventEmittable) .props({ timeout: 1000 * 60 * 5, path: null, lastId: null, room: null, abort: false }) .methods({ request() { const options = { timeout: this.timeout, query: { last_id: this.lastId, room: this.room } }; this.emit('request', options); return this.makeRequest('GET', this.path, options); }, start() { this.emit('start'); // some kind of while loop which uses Promises const loop = () => { if (this.abort === true) { this.emit('stop'); return } return this.request() .then((message) => { this.emit('message', message); this.emit(message.action, message); this.lastId = message.id; return message; }) .finally(loop) .catch((error) => { if (error.timeout && error.timeout === this.timeout) { return this.emit('timeout', error); } this.emit('error', error); this.stop(); }); } process.nextTick(loop); return this.stop; }, stop(removeListeners = false) { this.abort = true; if(removeListeners) { this.removeAllListeners(); } return this; } }); /** * OO wrapper around channels {@link http://docs.syncano.io/v0.1/docs/channels-list endpoint}. * **Channel** has two special methods called ``publish`` and ``poll``. First one will send message to the channel and second one will create {@link http://en.wikipedia.org/wiki/Push_technology#Long_polling long polling} connection which will listen for messages. * @constructor * @type {Channel} * @property {String} name * @property {String} instanceName * @property {String} type * @property {Number} [group = null] * @property {String} [group_permissions = null] * @property {String} [other_permissions = null] * @property {Boolean} [custom_publish = null] * @example {@lang javascript} * Channel.please().get('instance-name', 'channel-name').then((channel) => { * return channel.publish({x: 1}); * }); * * Channel.please().get('instance-name', 'channel-name').then((channel) => { * const poll = channel.poll(); * * poll.on('start', function() { * console.log('poll::start'); * }); * * poll.on('stop', function() { * console.log('poll::stop'); * }); * * poll.on('message', function(message) { * console.log('poll::message', message); * }); * * poll.on('custom', function(message) { * console.log('poll::custom', message); * }); * * poll.on('create', function(data) { * console.log('poll::create', data); * }); * * poll.on('delete', function(data) { * console.log('poll::delete', data); * }); * * poll.on('update', function(data) { * console.log('poll::update', data); * }); * * poll.on('error', function(error) { * console.log('poll::error', error); * }); * * poll.start(); * }); */ const Channel = stampit() .compose(Model) .setMeta(ChannelMeta) .setQuerySet(ChannelQuerySet) .methods({ poll(options = {}, start = true) { const config = this.getConfig(); const meta = this.getMeta(); const path = meta.resolveEndpointPath('poll', this); options.path = path; const channelPoll = ChannelPoll.setConfig(config)(options); if (start === true) { channelPoll.start(); } return channelPoll; }, publish(message, room = null) { const options = { payload: { payload: JSON.stringify(message) } }; const meta = this.getMeta(); const path = meta.resolveEndpointPath('publish', this); if (room !== null) { options.payload.room = room; } return this.makeRequest('POST', path, options); }, history(query = {}) { const meta = this.getMeta(); const path = meta.resolveEndpointPath('history', this); return this.makeRequest('GET', path, {query}); } }) .setConstraints(channelConstraints); export default Channel;