jsforce
Version:
Salesforce API Library for JavaScript
1,519 lines (1,266 loc) • 246 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.jsforce||(g.jsforce = {}));g=(g.modules||(g.modules = {}));g=(g.api||(g.api = {}));g.Streaming = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
* @file Manages Streaming APIs
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
'use strict';
var events = window.jsforce.require('events'),
inherits = window.jsforce.require('inherits'),
_ = window.jsforce.require('lodash/core'),
Faye = require('faye'),
jsforce = window.jsforce.require('./core');
/**
* Streaming API topic class
*
* @class Streaming~Topic
* @param {Streaming} steaming - Streaming API object
* @param {String} name - Topic name
*/
var Topic = function(streaming, name) {
this._streaming = streaming;
this.name = name;
};
/**
* @typedef {Object} Streaming~StreamingMessage
* @prop {Object} event
* @prop {Object} event.type - Event type
* @prop {Record} sobject - Record information
*/
/**
* Subscribe listener to topic
*
* @method Streaming~Topic#subscribe
* @param {Callback.<Streaming~StreamingMesasge>} listener - Streaming message listener
* @returns {Subscription} - Faye subscription object
*/
Topic.prototype.subscribe = function(listener) {
return this._streaming.subscribe(this.name, listener);
};
/**
* Unsubscribe listener from topic
*
* @method Streaming~Topic#unsubscribe
* @param {Callback.<Streaming~StreamingMesasge>} listener - Streaming message listener
* @returns {Streaming~Topic}
*/
Topic.prototype.unsubscribe = function(listener) {
this._streaming.unsubscribe(this.name, listener);
return this;
};
/*--------------------------------------------*/
/**
* Streaming API Generic Streaming Channel
*
* @class Streaming~Channel
* @param {Streaming} steaming - Streaming API object
* @param {String} name - Channel name (starts with "/u/")
*/
var Channel = function(streaming, name) {
this._streaming = streaming;
this._name = name;
};
/**
* Subscribe to hannel
*
* @param {Callback.<Streaming~StreamingMessage>} listener - Streaming message listener
* @returns {Subscription} - Faye subscription object
*/
Channel.prototype.subscribe = function(listener) {
return this._streaming.subscribe(this._name, listener);
};
Channel.prototype.unsubscribe = function(listener) {
this._streaming.unsubscribe(this._name, listener);
return this;
};
Channel.prototype.push = function(events, callback) {
var isArray = _.isArray(events);
events = isArray ? events : [ events ];
var conn = this._streaming._conn;
if (!this._id) {
this._id = conn.sobject('StreamingChannel').findOne({ Name: this._name }, 'Id')
.then(function(rec) { return rec.Id });
}
return this._id.then(function(id) {
var channelUrl = '/sobjects/StreamingChannel/' + id + '/push';
return conn.requestPost(channelUrl, { pushEvents: events });
}).then(function(rets) {
return isArray ? rets : rets[0];
}).thenCall(callback);
};
/*--------------------------------------------*/
/**
* Streaming API class
*
* @class
* @extends events.EventEmitter
* @param {Connection} conn - Connection object
*/
var Streaming = function(conn) {
this._conn = conn;
};
inherits(Streaming, events.EventEmitter);
/** @private **/
Streaming.prototype._createClient = function(replay) {
var endpointUrl = [
this._conn.instanceUrl,
// special endpoint "/cometd/replay/xx.x" is only available in 36.0.
// See https://releasenotes.docs.salesforce.com/en-us/summer16/release-notes/rn_api_streaming_classic_replay.htm
"cometd" + (replay && this._conn.version === "36.0" ? "/replay" : ""),
this._conn.version
].join('/');
var fayeClient = new Faye.Client(endpointUrl, {});
fayeClient.setHeader('Authorization', 'OAuth '+this._conn.accessToken);
return fayeClient;
};
/** @private **/
Streaming.prototype._getFayeClient = function(channelName) {
var isGeneric = channelName.indexOf('/u/') === 0;
var clientType = isGeneric ? 'generic' : 'pushTopic';
if (!this._fayeClients || !this._fayeClients[clientType]) {
this._fayeClients = this._fayeClients || {};
this._fayeClients[clientType] = this._createClient(isGeneric);
if (this._fayeClients[clientType]._dispatcher.getConnectionTypes().indexOf('callback-polling') === -1) {
// prevent streaming API server error
this._fayeClients[clientType]._dispatcher.selectTransport('long-polling');
this._fayeClients[clientType]._dispatcher._transport.batching = false;
}
}
return this._fayeClients[clientType];
};
/**
* Get named topic
*
* @param {String} name - Topic name
* @returns {Streaming~Topic}
*/
Streaming.prototype.topic = function(name) {
this._topics = this._topics || {};
var topic = this._topics[name] =
this._topics[name] || new Topic(this, name);
return topic;
};
/**
* Get Channel for Id
* @param {String} channelId - Id of StreamingChannel object
* @returns {Streaming~Channel}
*/
Streaming.prototype.channel = function(channelId) {
return new Channel(this, channelId);
};
/**
* Subscribe topic/channel
*
* @param {String} name - Topic name
* @param {Callback.<Streaming~StreamingMessage>} listener - Streaming message listener
* @returns {Subscription} - Faye subscription object
*/
Streaming.prototype.subscribe = function(name, listener) {
var channelName = name.indexOf('/') === 0 ? name : '/topic/' + name;
var fayeClient = this._getFayeClient(channelName);
return fayeClient.subscribe(channelName, listener);
};
/**
* Unsubscribe topic
*
* @param {String} name - Topic name
* @param {Callback.<Streaming~StreamingMessage>} listener - Streaming message listener
* @returns {Streaming}
*/
Streaming.prototype.unsubscribe = function(name, listener) {
var channelName = name.indexOf('/') === 0 ? name : '/topic/' + name;
var fayeClient = this._getFayeClient(channelName);
fayeClient.unsubscribe(channelName, listener);
return this;
};
/*--------------------------------------------*/
/*
* Register hook in connection instantiation for dynamically adding this API module features
*/
jsforce.on('connection:new', function(conn) {
conn.streaming = new Streaming(conn);
});
module.exports = Streaming;
},{"faye":5}],2:[function(require,module,exports){
// shim for using process in browser
var process = module.exports = {};
// cached from whatever global is present so that test runners that stub it
// don't break things. But we need to wrap it in a try catch in case it is
// wrapped in strict mode code which doesn't define any globals. It's inside a
// function because try/catches deoptimize in certain engines.
var cachedSetTimeout;
var cachedClearTimeout;
(function () {
try {
cachedSetTimeout = setTimeout;
} catch (e) {
cachedSetTimeout = function () {
throw new Error('setTimeout is not defined');
}
}
try {
cachedClearTimeout = clearTimeout;
} catch (e) {
cachedClearTimeout = function () {
throw new Error('clearTimeout is not defined');
}
}
} ())
var queue = [];
var draining = false;
var currentQueue;
var queueIndex = -1;
function cleanUpNextTick() {
if (!draining || !currentQueue) {
return;
}
draining = false;
if (currentQueue.length) {
queue = currentQueue.concat(queue);
} else {
queueIndex = -1;
}
if (queue.length) {
drainQueue();
}
}
function drainQueue() {
if (draining) {
return;
}
var timeout = cachedSetTimeout(cleanUpNextTick);
draining = true;
var len = queue.length;
while(len) {
currentQueue = queue;
queue = [];
while (++queueIndex < len) {
if (currentQueue) {
currentQueue[queueIndex].run();
}
}
queueIndex = -1;
len = queue.length;
}
currentQueue = null;
draining = false;
cachedClearTimeout(timeout);
}
process.nextTick = function (fun) {
var args = new Array(arguments.length - 1);
if (arguments.length > 1) {
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
}
queue.push(new Item(fun, args));
if (queue.length === 1 && !draining) {
cachedSetTimeout(drainQueue, 0);
}
};
// v8 likes predictible objects
function Item(fun, array) {
this.fun = fun;
this.array = array;
}
Item.prototype.run = function () {
this.fun.apply(null, this.array);
};
process.title = 'browser';
process.browser = true;
process.env = {};
process.argv = [];
process.version = ''; // empty string to avoid regexp issues
process.versions = {};
function noop() {}
process.on = noop;
process.addListener = noop;
process.once = noop;
process.off = noop;
process.removeListener = noop;
process.removeAllListeners = noop;
process.emit = noop;
process.binding = function (name) {
throw new Error('process.binding is not supported');
};
process.cwd = function () { return '/' };
process.chdir = function (dir) {
throw new Error('process.chdir is not supported');
};
process.umask = function() { return 0; };
},{}],3:[function(require,module,exports){
"use strict";
// rawAsap provides everything we need except exception management.
var rawAsap = require("./raw");
// RawTasks are recycled to reduce GC churn.
var freeTasks = [];
// We queue errors to ensure they are thrown in right order (FIFO).
// Array-as-queue is good enough here, since we are just dealing with exceptions.
var pendingErrors = [];
var requestErrorThrow = rawAsap.makeRequestCallFromTimer(throwFirstError);
function throwFirstError() {
if (pendingErrors.length) {
throw pendingErrors.shift();
}
}
/**
* Calls a task as soon as possible after returning, in its own event, with priority
* over other events like animation, reflow, and repaint. An error thrown from an
* event will not interrupt, nor even substantially slow down the processing of
* other events, but will be rather postponed to a lower priority event.
* @param {{call}} task A callable object, typically a function that takes no
* arguments.
*/
module.exports = asap;
function asap(task) {
var rawTask;
if (freeTasks.length) {
rawTask = freeTasks.pop();
} else {
rawTask = new RawTask();
}
rawTask.task = task;
rawAsap(rawTask);
}
// We wrap tasks with recyclable task objects. A task object implements
// `call`, just like a function.
function RawTask() {
this.task = null;
}
// The sole purpose of wrapping the task is to catch the exception and recycle
// the task object after its single use.
RawTask.prototype.call = function () {
try {
this.task.call();
} catch (error) {
if (asap.onerror) {
// This hook exists purely for testing purposes.
// Its name will be periodically randomized to break any code that
// depends on its existence.
asap.onerror(error);
} else {
// In a web browser, exceptions are not fatal. However, to avoid
// slowing down the queue of pending tasks, we rethrow the error in a
// lower priority turn.
pendingErrors.push(error);
requestErrorThrow();
}
} finally {
this.task = null;
freeTasks[freeTasks.length] = this;
}
};
},{"./raw":4}],4:[function(require,module,exports){
(function (global){
"use strict";
// Use the fastest means possible to execute a task in its own turn, with
// priority over other events including IO, animation, reflow, and redraw
// events in browsers.
//
// An exception thrown by a task will permanently interrupt the processing of
// subsequent tasks. The higher level `asap` function ensures that if an
// exception is thrown by a task, that the task queue will continue flushing as
// soon as possible, but if you use `rawAsap` directly, you are responsible to
// either ensure that no exceptions are thrown from your task, or to manually
// call `rawAsap.requestFlush` if an exception is thrown.
module.exports = rawAsap;
function rawAsap(task) {
if (!queue.length) {
requestFlush();
flushing = true;
}
// Equivalent to push, but avoids a function call.
queue[queue.length] = task;
}
var queue = [];
// Once a flush has been requested, no further calls to `requestFlush` are
// necessary until the next `flush` completes.
var flushing = false;
// `requestFlush` is an implementation-specific method that attempts to kick
// off a `flush` event as quickly as possible. `flush` will attempt to exhaust
// the event queue before yielding to the browser's own event loop.
var requestFlush;
// The position of the next task to execute in the task queue. This is
// preserved between calls to `flush` so that it can be resumed if
// a task throws an exception.
var index = 0;
// If a task schedules additional tasks recursively, the task queue can grow
// unbounded. To prevent memory exhaustion, the task queue will periodically
// truncate already-completed tasks.
var capacity = 1024;
// The flush function processes all tasks that have been scheduled with
// `rawAsap` unless and until one of those tasks throws an exception.
// If a task throws an exception, `flush` ensures that its state will remain
// consistent and will resume where it left off when called again.
// However, `flush` does not make any arrangements to be called again if an
// exception is thrown.
function flush() {
while (index < queue.length) {
var currentIndex = index;
// Advance the index before calling the task. This ensures that we will
// begin flushing on the next task the task throws an error.
index = index + 1;
queue[currentIndex].call();
// Prevent leaking memory for long chains of recursive calls to `asap`.
// If we call `asap` within tasks scheduled by `asap`, the queue will
// grow, but to avoid an O(n) walk for every task we execute, we don't
// shift tasks off the queue after they have been executed.
// Instead, we periodically shift 1024 tasks off the queue.
if (index > capacity) {
// Manually shift all values starting at the index back to the
// beginning of the queue.
for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) {
queue[scan] = queue[scan + index];
}
queue.length -= index;
index = 0;
}
}
queue.length = 0;
index = 0;
flushing = false;
}
// `requestFlush` is implemented using a strategy based on data collected from
// every available SauceLabs Selenium web driver worker at time of writing.
// https://docs.google.com/spreadsheets/d/1mG-5UYGup5qxGdEMWkhP6BWCz053NUb2E1QoUTU16uA/edit#gid=783724593
// Safari 6 and 6.1 for desktop, iPad, and iPhone are the only browsers that
// have WebKitMutationObserver but not un-prefixed MutationObserver.
// Must use `global` instead of `window` to work in both frames and web
// workers. `global` is a provision of Browserify, Mr, Mrs, or Mop.
var BrowserMutationObserver = global.MutationObserver || global.WebKitMutationObserver;
// MutationObservers are desirable because they have high priority and work
// reliably everywhere they are implemented.
// They are implemented in all modern browsers.
//
// - Android 4-4.3
// - Chrome 26-34
// - Firefox 14-29
// - Internet Explorer 11
// - iPad Safari 6-7.1
// - iPhone Safari 7-7.1
// - Safari 6-7
if (typeof BrowserMutationObserver === "function") {
requestFlush = makeRequestCallFromMutationObserver(flush);
// MessageChannels are desirable because they give direct access to the HTML
// task queue, are implemented in Internet Explorer 10, Safari 5.0-1, and Opera
// 11-12, and in web workers in many engines.
// Although message channels yield to any queued rendering and IO tasks, they
// would be better than imposing the 4ms delay of timers.
// However, they do not work reliably in Internet Explorer or Safari.
// Internet Explorer 10 is the only browser that has setImmediate but does
// not have MutationObservers.
// Although setImmediate yields to the browser's renderer, it would be
// preferrable to falling back to setTimeout since it does not have
// the minimum 4ms penalty.
// Unfortunately there appears to be a bug in Internet Explorer 10 Mobile (and
// Desktop to a lesser extent) that renders both setImmediate and
// MessageChannel useless for the purposes of ASAP.
// https://github.com/kriskowal/q/issues/396
// Timers are implemented universally.
// We fall back to timers in workers in most engines, and in foreground
// contexts in the following browsers.
// However, note that even this simple case requires nuances to operate in a
// broad spectrum of browsers.
//
// - Firefox 3-13
// - Internet Explorer 6-9
// - iPad Safari 4.3
// - Lynx 2.8.7
} else {
requestFlush = makeRequestCallFromTimer(flush);
}
// `requestFlush` requests that the high priority event queue be flushed as
// soon as possible.
// This is useful to prevent an error thrown in a task from stalling the event
// queue if the exception handled by Node.js’s
// `process.on("uncaughtException")` or by a domain.
rawAsap.requestFlush = requestFlush;
// To request a high priority event, we induce a mutation observer by toggling
// the text of a text node between "1" and "-1".
function makeRequestCallFromMutationObserver(callback) {
var toggle = 1;
var observer = new BrowserMutationObserver(callback);
var node = document.createTextNode("");
observer.observe(node, {characterData: true});
return function requestCall() {
toggle = -toggle;
node.data = toggle;
};
}
// The message channel technique was discovered by Malte Ubl and was the
// original foundation for this library.
// http://www.nonblocking.io/2011/06/windownexttick.html
// Safari 6.0.5 (at least) intermittently fails to create message ports on a
// page's first load. Thankfully, this version of Safari supports
// MutationObservers, so we don't need to fall back in that case.
// function makeRequestCallFromMessageChannel(callback) {
// var channel = new MessageChannel();
// channel.port1.onmessage = callback;
// return function requestCall() {
// channel.port2.postMessage(0);
// };
// }
// For reasons explained above, we are also unable to use `setImmediate`
// under any circumstances.
// Even if we were, there is another bug in Internet Explorer 10.
// It is not sufficient to assign `setImmediate` to `requestFlush` because
// `setImmediate` must be called *by name* and therefore must be wrapped in a
// closure.
// Never forget.
// function makeRequestCallFromSetImmediate(callback) {
// return function requestCall() {
// setImmediate(callback);
// };
// }
// Safari 6.0 has a problem where timers will get lost while the user is
// scrolling. This problem does not impact ASAP because Safari 6.0 supports
// mutation observers, so that implementation is used instead.
// However, if we ever elect to use timers in Safari, the prevalent work-around
// is to add a scroll event listener that calls for a flush.
// `setTimeout` does not call the passed callback if the delay is less than
// approximately 7 in web workers in Firefox 8 through 18, and sometimes not
// even then.
function makeRequestCallFromTimer(callback) {
return function requestCall() {
// We dispatch a timeout with a specified delay of 0 for engines that
// can reliably accommodate that request. This will usually be snapped
// to a 4 milisecond delay, but once we're flushing, there's no delay
// between events.
var timeoutHandle = setTimeout(handleTimer, 0);
// However, since this timer gets frequently dropped in Firefox
// workers, we enlist an interval handle that will try to fire
// an event 20 times per second until it succeeds.
var intervalHandle = setInterval(handleTimer, 50);
function handleTimer() {
// Whichever timer succeeds will cancel both timers and
// execute the callback.
clearTimeout(timeoutHandle);
clearInterval(intervalHandle);
callback();
}
};
}
// This is for `asap.js` only.
// Its name will be periodically randomized to break any code that depends on
// its existence.
rawAsap.makeRequestCallFromTimer = makeRequestCallFromTimer;
// ASAP was originally a nextTick shim included in Q. This was factored out
// into this ASAP package. It was later adapted to RSVP which made further
// amendments. These decisions, particularly to marginalize MessageChannel and
// to capture the MutationObserver implementation in a closure, were integrated
// back into ASAP proper.
// https://github.com/tildeio/rsvp.js/blob/cddf7232546a9cf858524b75cde6f9edf72620a7/lib/rsvp/asap.js
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],5:[function(require,module,exports){
'use strict';
var constants = require('./util/constants'),
Logging = require('./mixins/logging');
var Faye = {
VERSION: constants.VERSION,
Client: require('./protocol/client'),
Scheduler: require('./protocol/scheduler')
};
Logging.wrapper = Faye;
module.exports = Faye;
},{"./mixins/logging":7,"./protocol/client":11,"./protocol/scheduler":17,"./util/constants":29}],6:[function(require,module,exports){
(function (global){
'use strict';
var Promise = require('../util/promise');
module.exports = {
then: function(callback, errback) {
var self = this;
if (!this._promise)
this._promise = new Promise(function(resolve, reject) {
self._resolve = resolve;
self._reject = reject;
});
if (arguments.length === 0)
return this._promise;
else
return this._promise.then(callback, errback);
},
callback: function(callback, context) {
return this.then(function(value) { callback.call(context, value) });
},
errback: function(callback, context) {
return this.then(null, function(reason) { callback.call(context, reason) });
},
timeout: function(seconds, message) {
this.then();
var self = this;
this._timer = global.setTimeout(function() {
self._reject(message);
}, seconds * 1000);
},
setDeferredStatus: function(status, value) {
if (this._timer) global.clearTimeout(this._timer);
this.then();
if (status === 'succeeded')
this._resolve(value);
else if (status === 'failed')
this._reject(value);
else
delete this._promise;
}
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../util/promise":34}],7:[function(require,module,exports){
'use strict';
var toJSON = require('../util/to_json');
var Logging = {
LOG_LEVELS: {
fatal: 4,
error: 3,
warn: 2,
info: 1,
debug: 0
},
writeLog: function(messageArgs, level) {
var logger = Logging.logger || (Logging.wrapper || Logging).logger;
if (!logger) return;
var args = Array.prototype.slice.apply(messageArgs),
banner = '[Faye',
klass = this.className,
message = args.shift().replace(/\?/g, function() {
try {
return toJSON(args.shift());
} catch (error) {
return '[Object]';
}
});
if (klass) banner += '.' + klass;
banner += '] ';
if (typeof logger[level] === 'function')
logger[level](banner + message);
else if (typeof logger === 'function')
logger(banner + message);
}
};
for (var key in Logging.LOG_LEVELS)
(function(level) {
Logging[level] = function() {
this.writeLog(arguments, level);
};
})(key);
module.exports = Logging;
},{"../util/to_json":36}],8:[function(require,module,exports){
'use strict';
var extend = require('../util/extend'),
EventEmitter = require('../util/event_emitter');
var Publisher = {
countListeners: function(eventType) {
return this.listeners(eventType).length;
},
bind: function(eventType, listener, context) {
var slice = Array.prototype.slice,
handler = function() { listener.apply(context, slice.call(arguments)) };
this._listeners = this._listeners || [];
this._listeners.push([eventType, listener, context, handler]);
return this.on(eventType, handler);
},
unbind: function(eventType, listener, context) {
this._listeners = this._listeners || [];
var n = this._listeners.length, tuple;
while (n--) {
tuple = this._listeners[n];
if (tuple[0] !== eventType) continue;
if (listener && (tuple[1] !== listener || tuple[2] !== context)) continue;
this._listeners.splice(n, 1);
this.removeListener(eventType, tuple[3]);
}
}
};
extend(Publisher, EventEmitter.prototype);
Publisher.trigger = Publisher.emit;
module.exports = Publisher;
},{"../util/event_emitter":32,"../util/extend":33}],9:[function(require,module,exports){
(function (global){
'use strict';
module.exports = {
addTimeout: function(name, delay, callback, context) {
this._timeouts = this._timeouts || {};
if (this._timeouts.hasOwnProperty(name)) return;
var self = this;
this._timeouts[name] = global.setTimeout(function() {
delete self._timeouts[name];
callback.call(context);
}, 1000 * delay);
},
removeTimeout: function(name) {
this._timeouts = this._timeouts || {};
var timeout = this._timeouts[name];
if (!timeout) return;
global.clearTimeout(timeout);
delete this._timeouts[name];
},
removeAllTimeouts: function() {
this._timeouts = this._timeouts || {};
for (var name in this._timeouts) this.removeTimeout(name);
}
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],10:[function(require,module,exports){
'use strict';
var Class = require('../util/class'),
extend = require('../util/extend'),
Publisher = require('../mixins/publisher'),
Grammar = require('./grammar');
var Channel = Class({
initialize: function(name) {
this.id = this.name = name;
},
push: function(message) {
this.trigger('message', message);
},
isUnused: function() {
return this.countListeners('message') === 0;
}
});
extend(Channel.prototype, Publisher);
extend(Channel, {
HANDSHAKE: '/meta/handshake',
CONNECT: '/meta/connect',
SUBSCRIBE: '/meta/subscribe',
UNSUBSCRIBE: '/meta/unsubscribe',
DISCONNECT: '/meta/disconnect',
META: 'meta',
SERVICE: 'service',
expand: function(name) {
var segments = this.parse(name),
channels = ['/**', name];
var copy = segments.slice();
copy[copy.length - 1] = '*';
channels.push(this.unparse(copy));
for (var i = 1, n = segments.length; i < n; i++) {
copy = segments.slice(0, i);
copy.push('**');
channels.push(this.unparse(copy));
}
return channels;
},
isValid: function(name) {
return Grammar.CHANNEL_NAME.test(name) ||
Grammar.CHANNEL_PATTERN.test(name);
},
parse: function(name) {
if (!this.isValid(name)) return null;
return name.split('/').slice(1);
},
unparse: function(segments) {
return '/' + segments.join('/');
},
isMeta: function(name) {
var segments = this.parse(name);
return segments ? (segments[0] === this.META) : null;
},
isService: function(name) {
var segments = this.parse(name);
return segments ? (segments[0] === this.SERVICE) : null;
},
isSubscribable: function(name) {
if (!this.isValid(name)) return null;
return !this.isMeta(name) && !this.isService(name);
},
Set: Class({
initialize: function() {
this._channels = {};
},
getKeys: function() {
var keys = [];
for (var key in this._channels) keys.push(key);
return keys;
},
remove: function(name) {
delete this._channels[name];
},
hasSubscription: function(name) {
return this._channels.hasOwnProperty(name);
},
subscribe: function(names, subscription) {
var name;
for (var i = 0, n = names.length; i < n; i++) {
name = names[i];
var channel = this._channels[name] = this._channels[name] || new Channel(name);
channel.bind('message', subscription);
}
},
unsubscribe: function(name, subscription) {
var channel = this._channels[name];
if (!channel) return false;
channel.unbind('message', subscription);
if (channel.isUnused()) {
this.remove(name);
return true;
} else {
return false;
}
},
distributeMessage: function(message) {
var channels = Channel.expand(message.channel);
for (var i = 0, n = channels.length; i < n; i++) {
var channel = this._channels[channels[i]];
if (channel) channel.trigger('message', message);
}
}
})
});
module.exports = Channel;
},{"../mixins/publisher":8,"../util/class":28,"../util/extend":33,"./grammar":15}],11:[function(require,module,exports){
(function (global){
'use strict';
var asap = require('asap'),
Class = require('../util/class'),
Promise = require('../util/promise'),
URI = require('../util/uri'),
array = require('../util/array'),
browser = require('../util/browser'),
constants = require('../util/constants'),
extend = require('../util/extend'),
validateOptions = require('../util/validate_options'),
Deferrable = require('../mixins/deferrable'),
Logging = require('../mixins/logging'),
Publisher = require('../mixins/publisher'),
Channel = require('./channel'),
Dispatcher = require('./dispatcher'),
Error = require('./error'),
Extensible = require('./extensible'),
Publication = require('./publication'),
Subscription = require('./subscription');
var Client = Class({ className: 'Client',
UNCONNECTED: 1,
CONNECTING: 2,
CONNECTED: 3,
DISCONNECTED: 4,
HANDSHAKE: 'handshake',
RETRY: 'retry',
NONE: 'none',
CONNECTION_TIMEOUT: 60,
DEFAULT_ENDPOINT: '/bayeux',
INTERVAL: 0,
initialize: function(endpoint, options) {
this.info('New client created for ?', endpoint);
options = options || {};
validateOptions(options, ['interval', 'timeout', 'endpoints', 'proxy', 'retry', 'scheduler', 'websocketExtensions', 'tls', 'ca']);
this._channels = new Channel.Set();
this._dispatcher = Dispatcher.create(this, endpoint || this.DEFAULT_ENDPOINT, options);
this._messageId = 0;
this._state = this.UNCONNECTED;
this._responseCallbacks = {};
this._advice = {
reconnect: this.RETRY,
interval: 1000 * (options.interval || this.INTERVAL),
timeout: 1000 * (options.timeout || this.CONNECTION_TIMEOUT)
};
this._dispatcher.timeout = this._advice.timeout / 1000;
this._dispatcher.bind('message', this._receiveMessage, this);
if (browser.Event && global.onbeforeunload !== undefined)
browser.Event.on(global, 'beforeunload', function() {
if (array.indexOf(this._dispatcher._disabled, 'autodisconnect') < 0)
this.disconnect();
}, this);
},
addWebsocketExtension: function(extension) {
return this._dispatcher.addWebsocketExtension(extension);
},
disable: function(feature) {
return this._dispatcher.disable(feature);
},
setHeader: function(name, value) {
return this._dispatcher.setHeader(name, value);
},
// Request
// MUST include: * channel
// * version
// * supportedConnectionTypes
// MAY include: * minimumVersion
// * ext
// * id
//
// Success Response Failed Response
// MUST include: * channel MUST include: * channel
// * version * successful
// * supportedConnectionTypes * error
// * clientId MAY include: * supportedConnectionTypes
// * successful * advice
// MAY include: * minimumVersion * version
// * advice * minimumVersion
// * ext * ext
// * id * id
// * authSuccessful
handshake: function(callback, context) {
if (this._advice.reconnect === this.NONE) return;
if (this._state !== this.UNCONNECTED) return;
this._state = this.CONNECTING;
var self = this;
this.info('Initiating handshake with ?', URI.stringify(this._dispatcher.endpoint));
this._dispatcher.selectTransport(constants.MANDATORY_CONNECTION_TYPES);
this._sendMessage({
channel: Channel.HANDSHAKE,
version: constants.BAYEUX_VERSION,
supportedConnectionTypes: this._dispatcher.getConnectionTypes()
}, {}, function(response) {
if (response.successful) {
this._state = this.CONNECTED;
this._dispatcher.clientId = response.clientId;
this._dispatcher.selectTransport(response.supportedConnectionTypes);
this.info('Handshake successful: ?', this._dispatcher.clientId);
this.subscribe(this._channels.getKeys(), true);
if (callback) asap(function() { callback.call(context) });
} else {
this.info('Handshake unsuccessful');
global.setTimeout(function() { self.handshake(callback, context) }, this._dispatcher.retry * 1000);
this._state = this.UNCONNECTED;
}
}, this);
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// * connectionType * clientId
// MAY include: * ext MAY include: * error
// * id * advice
// * ext
// * id
// * timestamp
connect: function(callback, context) {
if (this._advice.reconnect === this.NONE) return;
if (this._state === this.DISCONNECTED) return;
if (this._state === this.UNCONNECTED)
return this.handshake(function() { this.connect(callback, context) }, this);
this.callback(callback, context);
if (this._state !== this.CONNECTED) return;
this.info('Calling deferred actions for ?', this._dispatcher.clientId);
this.setDeferredStatus('succeeded');
this.setDeferredStatus('unknown');
if (this._connectRequest) return;
this._connectRequest = true;
this.info('Initiating connection for ?', this._dispatcher.clientId);
this._sendMessage({
channel: Channel.CONNECT,
clientId: this._dispatcher.clientId,
connectionType: this._dispatcher.connectionType
}, {}, this._cycleConnection, this);
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// MAY include: * ext * clientId
// * id MAY include: * error
// * ext
// * id
disconnect: function() {
if (this._state !== this.CONNECTED) return;
this._state = this.DISCONNECTED;
this.info('Disconnecting ?', this._dispatcher.clientId);
var promise = new Publication();
this._sendMessage({
channel: Channel.DISCONNECT,
clientId: this._dispatcher.clientId
}, {}, function(response) {
if (response.successful) {
this._dispatcher.close();
promise.setDeferredStatus('succeeded');
} else {
promise.setDeferredStatus('failed', Error.parse(response.error));
}
}, this);
this.info('Clearing channel listeners for ?', this._dispatcher.clientId);
this._channels = new Channel.Set();
return promise;
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// * subscription * clientId
// MAY include: * ext * subscription
// * id MAY include: * error
// * advice
// * ext
// * id
// * timestamp
subscribe: function(channel, callback, context) {
if (channel instanceof Array)
return array.map(channel, function(c) {
return this.subscribe(c, callback, context);
}, this);
var subscription = new Subscription(this, channel, callback, context),
force = (callback === true),
hasSubscribe = this._channels.hasSubscription(channel);
if (hasSubscribe && !force) {
this._channels.subscribe([channel], subscription);
subscription.setDeferredStatus('succeeded');
return subscription;
}
this.connect(function() {
this.info('Client ? attempting to subscribe to ?', this._dispatcher.clientId, channel);
if (!force) this._channels.subscribe([channel], subscription);
this._sendMessage({
channel: Channel.SUBSCRIBE,
clientId: this._dispatcher.clientId,
subscription: channel
}, {}, function(response) {
if (!response.successful) {
subscription.setDeferredStatus('failed', Error.parse(response.error));
return this._channels.unsubscribe(channel, subscription);
}
var channels = [].concat(response.subscription);
this.info('Subscription acknowledged for ? to ?', this._dispatcher.clientId, channels);
subscription.setDeferredStatus('succeeded');
}, this);
}, this);
return subscription;
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// * subscription * clientId
// MAY include: * ext * subscription
// * id MAY include: * error
// * advice
// * ext
// * id
// * timestamp
unsubscribe: function(channel, subscription) {
if (channel instanceof Array)
return array.map(channel, function(c) {
return this.unsubscribe(c, subscription);
}, this);
var dead = this._channels.unsubscribe(channel, subscription);
if (!dead) return;
this.connect(function() {
this.info('Client ? attempting to unsubscribe from ?', this._dispatcher.clientId, channel);
this._sendMessage({
channel: Channel.UNSUBSCRIBE,
clientId: this._dispatcher.clientId,
subscription: channel
}, {}, function(response) {
if (!response.successful) return;
var channels = [].concat(response.subscription);
this.info('Unsubscription acknowledged for ? from ?', this._dispatcher.clientId, channels);
}, this);
}, this);
},
// Request Response
// MUST include: * channel MUST include: * channel
// * data * successful
// MAY include: * clientId MAY include: * id
// * id * error
// * ext * ext
publish: function(channel, data, options) {
validateOptions(options || {}, ['attempts', 'deadline']);
var publication = new Publication();
this.connect(function() {
this.info('Client ? queueing published message to ?: ?', this._dispatcher.clientId, channel, data);
this._sendMessage({
channel: channel,
data: data,
clientId: this._dispatcher.clientId
}, options, function(response) {
if (response.successful)
publication.setDeferredStatus('succeeded');
else
publication.setDeferredStatus('failed', Error.parse(response.error));
}, this);
}, this);
return publication;
},
_sendMessage: function(message, options, callback, context) {
message.id = this._generateMessageId();
var timeout = this._advice.timeout
? 1.2 * this._advice.timeout / 1000
: 1.2 * this._dispatcher.retry;
this.pipeThroughExtensions('outgoing', message, null, function(message) {
if (!message) return;
if (callback) this._responseCallbacks[message.id] = [callback, context];
this._dispatcher.sendMessage(message, timeout, options || {});
}, this);
},
_generateMessageId: function() {
this._messageId += 1;
if (this._messageId >= Math.pow(2,32)) this._messageId = 0;
return this._messageId.toString(36);
},
_receiveMessage: function(message) {
var id = message.id, callback;
if (message.successful !== undefined) {
callback = this._responseCallbacks[id];
delete this._responseCallbacks[id];
}
this.pipeThroughExtensions('incoming', message, null, function(message) {
if (!message) return;
if (message.advice) this._handleAdvice(message.advice);
this._deliverMessage(message);
if (callback) callback[0].call(callback[1], message);
}, this);
},
_handleAdvice: function(advice) {
extend(this._advice, advice);
this._dispatcher.timeout = this._advice.timeout / 1000;
if (this._advice.reconnect === this.HANDSHAKE && this._state !== this.DISCONNECTED) {
this._state = this.UNCONNECTED;
this._dispatcher.clientId = null;
this._cycleConnection();
}
},
_deliverMessage: function(message) {
if (!message.channel || message.data === undefined) return;
this.info('Client ? calling listeners for ? with ?', this._dispatcher.clientId, message.channel, message.data);
this._channels.distributeMessage(message);
},
_cycleConnection: function() {
if (this._connectRequest) {
this._connectRequest = null;
this.info('Closed connection for ?', this._dispatcher.clientId);
}
var self = this;
global.setTimeout(function() { self.connect() }, this._advice.interval);
}
});
extend(Client.prototype, Deferrable);
extend(Client.prototype, Publisher);
extend(Client.prototype, Logging);
extend(Client.prototype, Extensible);
module.exports = Client;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../mixins/deferrable":6,"../mixins/logging":7,"../mixins/publisher":8,"../util/array":26,"../util/browser":27,"../util/class":28,"../util/constants":29,"../util/extend":33,"../util/promise":34,"../util/uri":37,"../util/validate_options":38,"./channel":10,"./dispatcher":12,"./error":13,"./extensible":14,"./publication":16,"./subscription":18,"asap":3}],12:[function(require,module,exports){
(function (global){
'use strict';
var Class = require('../util/class'),
URI = require('../util/uri'),
cookies = require('../util/cookies'),
extend = require('../util/extend'),
Logging = require('../mixins/logging'),
Publisher = require('../mixins/publisher'),
Transport = require('../transport'),
Scheduler = require('./scheduler');
var Dispatcher = Class({ className: 'Dispatcher',
MAX_REQUEST_SIZE: 2048,
DEFAULT_RETRY: 5,
UP: 1,
DOWN: 2,
initialize: function(client, endpoint, options) {
this._client = client;
this.endpoint = URI.parse(endpoint);
this._alternates = options.endpoints || {};
this.cookies = cookies.CookieJar && new cookies.CookieJar();
this._disabled = [];
this._envelopes = {};
this.headers = {};
this.retry = options.retry || this.DEFAULT_RETRY;
this._scheduler = options.scheduler || Scheduler;
this._state = 0;
this.transports = {};
this.wsExtensions = [];
this.proxy = options.proxy || {};
if (typeof this._proxy === 'string') this._proxy = {origin: this._proxy};
var exts = options.websocketExtensions;
if (exts) {
exts = [].concat(exts);
for (var i = 0, n = exts.length; i < n; i++)
this.addWebsocketExtension(exts[i]);
}
this.tls = options.tls || {};
this.tls.ca = this.tls.ca || options.ca;
for (var type in this._alternates)
this._alternates[type] = URI.parse(this._alternates[type]);
this.maxRequestSize = this.MAX_REQUEST_SIZE;
},
endpointFor: function(connectionType) {
return this._alternates[connectionType] || this.endpoint;
},
addWebsocketExtension: function(extension) {
this.wsExtensions.push(extension);
},
disable: function(feature) {
this._disabled.push(feature);
},
setHeader: function(name, value) {
this.headers[name] = value;
},
close: function() {
var transport = this._transport;
delete this._transport;
if (transport) transport.close();
},
getConnectionTypes: function() {
return Transport.getConnectionTypes();
},
selectTransport: function(transportTypes) {
Transport.get(this, transportTypes, this._disabled, function(transport) {
this.debug('Selected ? transport for ?', transport.connectionType, URI.stringify(transport.endpoint));
if (transport === this._transport) return;
if (this._transport) this._transport.close();
this._transport = transport;
this.connectionType = transport.connectionType;
}, this);
},
sendMessage: function(message, timeout, options) {
options = options || {};
var id = message.id,
attempts = options.attempts,
deadline = options.deadline && new Date().getTime() + (options.deadline * 1000),
envelope = this._envelopes[id],
scheduler;
if (!envelope) {
scheduler = new this._scheduler(message, {timeout: timeout, interval: this.retry, attempts: attempts, deadline: deadline});
envelope = this._envelopes[id] = {message: message, scheduler: scheduler};
}
this._sendEnvelope(envelope);
},
_sendEnvelope: function(envelope) {
if (!this._transport) return;
if (envelope.request || envelope.timer) return;
var message = envelope.message,
scheduler = envelope.scheduler,
self = this;
if (!scheduler.isDeliverable()) {
scheduler.abort();
delete this._envelopes[message.id];
return;
}
envelope.timer = global.setTimeout(function() {
self.handleError(message);
}, scheduler.getTimeout() * 1000);
scheduler.send();
envelope.request = this._transport.sendMessage(message);
},
handleResponse: function(reply) {
var envelope = this._envelopes[reply.id];
if (reply.successful !== undefined && envelope) {
envelope.scheduler.succeed();
delete this._envelopes[reply.id];
global.clearTimeout(envelope.timer);
}
this.trigger('message', reply);
if (this._state === this.UP) return;
this._state = this.UP;
this._client.trigger('transport:up');
},
handleError: function(message, immediate) {
var envelope = this._envelopes[message.id],
request = envelope && envelope.request,
self = this;
if (!request) return;
request.then(function(req) {
if (req && req.abort) req.abort();
});
var scheduler = envelope.scheduler;
scheduler.fail();
global.clearTimeout(envelope.timer);
envelope.request = envelope.timer = null;
if (immediate) {
this._sendEnvelope(envelope);
} else {
envelope.timer = global.setTimeout(function() {
envelope.timer = null;
self._sendEnvelope(envelope);
}, scheduler.getInterval() * 1000);
}
if (this._state === this.DOWN) return;
this._state = this.DOWN;
this._client.trigger('transport:down');
}
});
Dispatcher.create = function(client, endpoint, options) {
return new Dispatcher(client, endpoint, options);
};
extend(Disp