laravel-echo
Version:
Laravel Echo library for beautiful Pusher and Socket.IO integration
1,020 lines (998 loc) • 28.6 kB
JavaScript
var Echo = (function () {
'use strict';
/**
* This class represents a basic channel.
*/
class Channel {
/**
* Listen for a whisper event on the channel instance.
*/
listenForWhisper(event, callback) {
return this.listen('.client-' + event, callback);
}
/**
* Listen for an event on the channel instance.
*/
notification(callback) {
return this.listen('.Illuminate\\Notifications\\Events\\BroadcastNotificationCreated', callback);
}
/**
* Stop listening for a whisper event on the channel instance.
*/
stopListeningForWhisper(event, callback) {
return this.stopListening('.client-' + event, callback);
}
}
/**
* Event name formatter
*/
class EventFormatter {
/**
* Create a new class instance.
*/
constructor(namespace) {
this.namespace = namespace;
//
}
/**
* Format the given event name.
*/
format(event) {
if (['.', '\\'].includes(event.charAt(0))) {
return event.substring(1);
} else if (this.namespace) {
event = this.namespace + '.' + event;
}
return event.replace(/\./g, '\\');
}
/**
* Set the event namespace.
*/
setNamespace(value) {
this.namespace = value;
}
}
function isConstructor(obj) {
try {
new obj();
} catch (err) {
if (err instanceof Error && err.message.includes('is not a constructor')) {
return false;
}
}
return true;
}
/**
* This class represents a Pusher channel.
*/
class PusherChannel extends Channel {
/**
* Create a new class instance.
*/
constructor(pusher, name, options) {
super();
this.name = name;
this.pusher = pusher;
this.options = options;
this.eventFormatter = new EventFormatter(this.options.namespace);
this.subscribe();
}
/**
* Subscribe to a Pusher channel.
*/
subscribe() {
this.subscription = this.pusher.subscribe(this.name);
}
/**
* Unsubscribe from a Pusher channel.
*/
unsubscribe() {
this.pusher.unsubscribe(this.name);
}
/**
* Listen for an event on the channel instance.
*/
listen(event, callback) {
this.on(this.eventFormatter.format(event), callback);
return this;
}
/**
* Listen for all events on the channel instance.
*/
listenToAll(callback) {
this.subscription.bind_global((event, data) => {
if (event.startsWith('pusher:')) {
return;
}
let namespace = String(this.options.namespace ?? '').replace(/\./g, '\\');
let formattedEvent = event.startsWith(namespace) ? event.substring(namespace.length + 1) : '.' + event;
callback(formattedEvent, data);
});
return this;
}
/**
* Stop listening for an event on the channel instance.
*/
stopListening(event, callback) {
if (callback) {
this.subscription.unbind(this.eventFormatter.format(event), callback);
} else {
this.subscription.unbind(this.eventFormatter.format(event));
}
return this;
}
/**
* Stop listening for all events on the channel instance.
*/
stopListeningToAll(callback) {
if (callback) {
this.subscription.unbind_global(callback);
} else {
this.subscription.unbind_global();
}
return this;
}
/**
* Register a callback to be called anytime a subscription succeeds.
*/
subscribed(callback) {
this.on('pusher:subscription_succeeded', () => {
callback();
});
return this;
}
/**
* Register a callback to be called anytime a subscription error occurs.
*/
error(callback) {
this.on('pusher:subscription_error', status => {
callback(status);
});
return this;
}
/**
* Bind a channel to an event.
*/
on(event, callback) {
this.subscription.bind(event, callback);
return this;
}
}
/**
* This class represents a Pusher private channel.
*/
class PusherPrivateChannel extends PusherChannel {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(eventName, data) {
this.pusher.channels.channels[this.name].trigger(`client-${eventName}`, data);
return this;
}
}
/**
* This class represents a Pusher private channel.
*/
class PusherEncryptedPrivateChannel extends PusherChannel {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(eventName, data) {
this.pusher.channels.channels[this.name].trigger(`client-${eventName}`, data);
return this;
}
}
/**
* This class represents a Pusher presence channel.
*/
class PusherPresenceChannel extends PusherPrivateChannel {
/**
* Register a callback to be called anytime the member list changes.
*/
here(callback) {
this.on('pusher:subscription_succeeded', data => {
callback(Object.keys(data.members).map(k => data.members[k]));
});
return this;
}
/**
* Listen for someone joining the channel.
*/
joining(callback) {
this.on('pusher:member_added', member => {
callback(member.info);
});
return this;
}
/**
* Send a whisper event to other clients in the channel.
*/
whisper(eventName, data) {
this.pusher.channels.channels[this.name].trigger(`client-${eventName}`, data);
return this;
}
/**
* Listen for someone leaving the channel.
*/
leaving(callback) {
this.on('pusher:member_removed', member => {
callback(member.info);
});
return this;
}
}
/**
* This class represents a Socket.io channel.
*/
class SocketIoChannel extends Channel {
/**
* Create a new class instance.
*/
constructor(socket, name, options) {
super();
/**
* The event callbacks applied to the socket.
*/
this.events = {};
/**
* User supplied callbacks for events on this channel.
*/
this.listeners = {};
this.name = name;
this.socket = socket;
this.options = options;
this.eventFormatter = new EventFormatter(this.options.namespace);
this.subscribe();
}
/**
* Subscribe to a Socket.io channel.
*/
subscribe() {
this.socket.emit('subscribe', {
channel: this.name,
auth: this.options.auth || {}
});
}
/**
* Unsubscribe from channel and ubind event callbacks.
*/
unsubscribe() {
this.unbind();
this.socket.emit('unsubscribe', {
channel: this.name,
auth: this.options.auth || {}
});
}
/**
* Listen for an event on the channel instance.
*/
listen(event, callback) {
this.on(this.eventFormatter.format(event), callback);
return this;
}
/**
* Stop listening for an event on the channel instance.
*/
stopListening(event, callback) {
this.unbindEvent(this.eventFormatter.format(event), callback);
return this;
}
/**
* Register a callback to be called anytime a subscription succeeds.
*/
subscribed(callback) {
this.on('connect', socket => {
callback(socket);
});
return this;
}
/**
* Register a callback to be called anytime an error occurs.
*/
error(_callback) {
return this;
}
/**
* Bind the channel's socket to an event and store the callback.
*/
on(event, callback) {
this.listeners[event] = this.listeners[event] || [];
if (!this.events[event]) {
this.events[event] = (channel, data) => {
if (this.name === channel && this.listeners[event]) {
this.listeners[event].forEach(cb => cb(data));
}
};
this.socket.on(event, this.events[event]);
}
this.listeners[event].push(callback);
return this;
}
/**
* Unbind the channel's socket from all stored event callbacks.
*/
unbind() {
Object.keys(this.events).forEach(event => {
this.unbindEvent(event);
});
}
/**
* Unbind the listeners for the given event.
*/
unbindEvent(event, callback) {
this.listeners[event] = this.listeners[event] || [];
if (callback) {
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
}
if (!callback || this.listeners[event].length === 0) {
if (this.events[event]) {
this.socket.removeListener(event, this.events[event]);
delete this.events[event];
}
delete this.listeners[event];
}
}
}
/**
* This class represents a Socket.io private channel.
*/
class SocketIoPrivateChannel extends SocketIoChannel {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(eventName, data) {
this.socket.emit('client event', {
channel: this.name,
event: `client-${eventName}`,
data: data
});
return this;
}
}
/**
* This class represents a Socket.io presence channel.
*/
class SocketIoPresenceChannel extends SocketIoPrivateChannel {
/**
* Register a callback to be called anytime the member list changes.
*/
here(callback) {
this.on('presence:subscribed', members => {
callback(members.map(m => m.user_info));
});
return this;
}
/**
* Listen for someone joining the channel.
*/
joining(callback) {
this.on('presence:joining', member => callback(member.user_info));
return this;
}
/**
* Send a whisper event to other clients in the channel.
*/
whisper(eventName, data) {
this.socket.emit('client event', {
channel: this.name,
event: `client-${eventName}`,
data: data
});
return this;
}
/**
* Listen for someone leaving the channel.
*/
leaving(callback) {
this.on('presence:leaving', member => callback(member.user_info));
return this;
}
}
/**
* This class represents a null channel.
*/
class NullChannel extends Channel {
/**
* Subscribe to a channel.
*/
subscribe() {
//
}
/**
* Unsubscribe from a channel.
*/
unsubscribe() {
//
}
/**
* Listen for an event on the channel instance.
*/
listen(_event, _callback) {
return this;
}
/**
* Listen for all events on the channel instance.
*/
listenToAll(_callback) {
return this;
}
/**
* Stop listening for an event on the channel instance.
*/
stopListening(_event, _callback) {
return this;
}
/**
* Register a callback to be called anytime a subscription succeeds.
*/
subscribed(_callback) {
return this;
}
/**
* Register a callback to be called anytime an error occurs.
*/
error(_callback) {
return this;
}
/**
* Bind a channel to an event.
*/
on(_event, _callback) {
return this;
}
}
/**
* This class represents a null private channel.
*/
class NullPrivateChannel extends NullChannel {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(_eventName, _data) {
return this;
}
}
/**
* This class represents a null private channel.
*/
class NullEncryptedPrivateChannel extends NullChannel {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(_eventName, _data) {
return this;
}
}
/**
* This class represents a null presence channel.
*/
class NullPresenceChannel extends NullPrivateChannel {
/**
* Register a callback to be called anytime the member list changes.
*/
here(_callback) {
return this;
}
/**
* Listen for someone joining the channel.
*/
joining(_callback) {
return this;
}
/**
* Send a whisper event to other clients in the channel.
*/
whisper(_eventName, _data) {
return this;
}
/**
* Listen for someone leaving the channel.
*/
leaving(_callback) {
return this;
}
}
class Connector {
/**
* Create a new class instance.
*/
constructor(options) {
this.setOptions(options);
this.connect();
}
/**
* Merge the custom options with the defaults.
*/
setOptions(options) {
this.options = {
...Connector._defaultOptions,
...options,
broadcaster: options.broadcaster
};
let token = this.csrfToken();
if (token) {
this.options.auth.headers['X-CSRF-TOKEN'] = token;
this.options.userAuthentication.headers['X-CSRF-TOKEN'] = token;
}
token = this.options.bearerToken;
if (token) {
this.options.auth.headers['Authorization'] = 'Bearer ' + token;
this.options.userAuthentication.headers['Authorization'] = 'Bearer ' + token;
}
}
/**
* Extract the CSRF token from the page.
*/
csrfToken() {
let selector;
if (typeof window !== 'undefined' && typeof window.Laravel !== 'undefined' && window.Laravel.csrfToken) {
return window.Laravel.csrfToken;
}
if (this.options.csrfToken) {
return this.options.csrfToken;
}
if (typeof document !== 'undefined' && typeof document.querySelector === 'function' && (selector = document.querySelector('meta[name="csrf-token"]'))) {
return selector.getAttribute('content');
}
return null;
}
}
/**
* Default connector options.
*/
Connector._defaultOptions = {
auth: {
headers: {}
},
authEndpoint: '/broadcasting/auth',
userAuthentication: {
endpoint: '/broadcasting/user-auth',
headers: {}
},
csrfToken: null,
bearerToken: null,
host: null,
key: null,
namespace: 'App.Events'
};
/**
* This class creates a connector to Pusher.
*/
class PusherConnector extends Connector {
constructor() {
super(...arguments);
/**
* All of the subscribed channel names.
*/
this.channels = {};
}
/**
* Create a fresh Pusher connection.
*/
connect() {
if (typeof this.options.client !== 'undefined') {
this.pusher = this.options.client;
} else if (this.options.Pusher) {
this.pusher = new this.options.Pusher(this.options.key, this.options);
} else if (typeof window !== 'undefined' && typeof window.Pusher !== 'undefined') {
this.pusher = new window.Pusher(this.options.key, this.options);
} else {
throw new Error('Pusher client not found. Should be globally available or passed via options.client');
}
}
/**
* Sign in the user via Pusher user authentication (https://pusher.com/docs/channels/using_channels/user-authentication/).
*/
signin() {
this.pusher.signin();
}
/**
* Listen for an event on a channel instance.
*/
listen(name, event, callback) {
return this.channel(name).listen(event, callback);
}
/**
* Get a channel instance by name.
*/
channel(name) {
if (!this.channels[name]) {
this.channels[name] = new PusherChannel(this.pusher, name, this.options);
}
return this.channels[name];
}
/**
* Get a private channel instance by name.
*/
privateChannel(name) {
if (!this.channels['private-' + name]) {
this.channels['private-' + name] = new PusherPrivateChannel(this.pusher, 'private-' + name, this.options);
}
return this.channels['private-' + name];
}
/**
* Get a private encrypted channel instance by name.
*/
encryptedPrivateChannel(name) {
if (!this.channels['private-encrypted-' + name]) {
this.channels['private-encrypted-' + name] = new PusherEncryptedPrivateChannel(this.pusher, 'private-encrypted-' + name, this.options);
}
return this.channels['private-encrypted-' + name];
}
/**
* Get a presence channel instance by name.
*/
presenceChannel(name) {
if (!this.channels['presence-' + name]) {
this.channels['presence-' + name] = new PusherPresenceChannel(this.pusher, 'presence-' + name, this.options);
}
return this.channels['presence-' + name];
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(name) {
let channels = [name, 'private-' + name, 'private-encrypted-' + name, 'presence-' + name];
channels.forEach(name => {
this.leaveChannel(name);
});
}
/**
* Leave the given channel.
*/
leaveChannel(name) {
if (this.channels[name]) {
this.channels[name].unsubscribe();
delete this.channels[name];
}
}
/**
* Get the socket ID for the connection.
*/
socketId() {
return this.pusher.connection.socket_id;
}
/**
* Disconnect Pusher connection.
*/
disconnect() {
this.pusher.disconnect();
}
}
/**
* This class creates a connector to a Socket.io server.
*/
class SocketIoConnector extends Connector {
constructor() {
super(...arguments);
/**
* All of the subscribed channel names.
*/
this.channels = {};
}
/**
* Create a fresh Socket.io connection.
*/
connect() {
let io = this.getSocketIO();
this.socket = io(this.options.host ?? undefined, this.options);
this.socket.on('reconnect', () => {
Object.values(this.channels).forEach(channel => {
channel.subscribe();
});
});
}
/**
* Get socket.io module from global scope or options.
*/
getSocketIO() {
if (typeof this.options.client !== 'undefined') {
return this.options.client;
}
if (typeof window !== 'undefined' && typeof window.io !== 'undefined') {
return window.io;
}
throw new Error('Socket.io client not found. Should be globally available or passed via options.client');
}
/**
* Listen for an event on a channel instance.
*/
listen(name, event, callback) {
return this.channel(name).listen(event, callback);
}
/**
* Get a channel instance by name.
*/
channel(name) {
if (!this.channels[name]) {
this.channels[name] = new SocketIoChannel(this.socket, name, this.options);
}
return this.channels[name];
}
/**
* Get a private channel instance by name.
*/
privateChannel(name) {
if (!this.channels['private-' + name]) {
this.channels['private-' + name] = new SocketIoPrivateChannel(this.socket, 'private-' + name, this.options);
}
return this.channels['private-' + name];
}
/**
* Get a presence channel instance by name.
*/
presenceChannel(name) {
if (!this.channels['presence-' + name]) {
this.channels['presence-' + name] = new SocketIoPresenceChannel(this.socket, 'presence-' + name, this.options);
}
return this.channels['presence-' + name];
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(name) {
let channels = [name, 'private-' + name, 'presence-' + name];
channels.forEach(name => {
this.leaveChannel(name);
});
}
/**
* Leave the given channel.
*/
leaveChannel(name) {
if (this.channels[name]) {
this.channels[name].unsubscribe();
delete this.channels[name];
}
}
/**
* Get the socket ID for the connection.
*/
socketId() {
return this.socket.id;
}
/**
* Disconnect Socketio connection.
*/
disconnect() {
this.socket.disconnect();
}
}
/**
* This class creates a null connector.
*/
class NullConnector extends Connector {
constructor() {
super(...arguments);
/**
* All of the subscribed channel names.
*/
this.channels = {};
}
/**
* Create a fresh connection.
*/
connect() {
//
}
/**
* Listen for an event on a channel instance.
*/
listen(_name, _event, _callback) {
return new NullChannel();
}
/**
* Get a channel instance by name.
*/
channel(_name) {
return new NullChannel();
}
/**
* Get a private channel instance by name.
*/
privateChannel(_name) {
return new NullPrivateChannel();
}
/**
* Get a private encrypted channel instance by name.
*/
encryptedPrivateChannel(_name) {
return new NullEncryptedPrivateChannel();
}
/**
* Get a presence channel instance by name.
*/
presenceChannel(_name) {
return new NullPresenceChannel();
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(_name) {
//
}
/**
* Leave the given channel.
*/
leaveChannel(_name) {
//
}
/**
* Get the socket ID for the connection.
*/
socketId() {
return 'fake-socket-id';
}
/**
* Disconnect the connection.
*/
disconnect() {
//
}
}
/**
* This class is the primary API for interacting with broadcasting.
*/
class Echo {
/**
* Create a new class instance.
*/
constructor(options) {
this.options = options;
this.connect();
if (!this.options.withoutInterceptors) {
this.registerInterceptors();
}
}
/**
* Get a channel instance by name.
*/
channel(channel) {
return this.connector.channel(channel);
}
/**
* Create a new connection.
*/
connect() {
if (this.options.broadcaster === 'reverb') {
this.connector = new PusherConnector({
...this.options,
cluster: ''
});
} else if (this.options.broadcaster === 'pusher') {
this.connector = new PusherConnector(this.options);
} else if (this.options.broadcaster === 'socket.io') {
this.connector = new SocketIoConnector(this.options);
} else if (this.options.broadcaster === 'null') {
this.connector = new NullConnector(this.options);
} else if (typeof this.options.broadcaster === 'function' && isConstructor(this.options.broadcaster)) {
this.connector = new this.options.broadcaster(this.options);
} else {
throw new Error(`Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} is not supported.`);
}
}
/**
* Disconnect from the Echo server.
*/
disconnect() {
this.connector.disconnect();
}
/**
* Get a presence channel instance by name.
*/
join(channel) {
return this.connector.presenceChannel(channel);
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(channel) {
this.connector.leave(channel);
}
/**
* Leave the given channel.
*/
leaveChannel(channel) {
this.connector.leaveChannel(channel);
}
/**
* Leave all channels.
*/
leaveAllChannels() {
for (const channel in this.connector.channels) {
this.leaveChannel(channel);
}
}
/**
* Listen for an event on a channel instance.
*/
listen(channel, event, callback) {
return this.connector.listen(channel, event, callback);
}
/**
* Get a private channel instance by name.
*/
private(channel) {
return this.connector.privateChannel(channel);
}
/**
* Get a private encrypted channel instance by name.
*/
encryptedPrivate(channel) {
if (this.connectorSupportsEncryptedPrivateChannels(this.connector)) {
return this.connector.encryptedPrivateChannel(channel);
}
throw new Error(`Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} does not support encrypted private channels.`);
}
connectorSupportsEncryptedPrivateChannels(connector) {
return connector instanceof PusherConnector || connector instanceof NullConnector;
}
/**
* Get the Socket ID for the connection.
*/
socketId() {
return this.connector.socketId();
}
/**
* Register 3rd party request interceptiors. These are used to automatically
* send a connections socket id to a Laravel app with a X-Socket-Id header.
*/
registerInterceptors() {
if (typeof Vue === 'function' && Vue.http) {
this.registerVueRequestInterceptor();
}
if (typeof axios === 'function') {
this.registerAxiosRequestInterceptor();
}
if (typeof jQuery === 'function') {
this.registerjQueryAjaxSetup();
}
if (typeof Turbo === 'object') {
this.registerTurboRequestInterceptor();
}
}
/**
* Register a Vue HTTP interceptor to add the X-Socket-ID header.
*/
registerVueRequestInterceptor() {
Vue.http.interceptors.push((request, next) => {
if (this.socketId()) {
request.headers.set('X-Socket-ID', this.socketId());
}
next();
});
}
/**
* Register an Axios HTTP interceptor to add the X-Socket-ID header.
*/
registerAxiosRequestInterceptor() {
axios.interceptors.request.use(config => {
if (this.socketId()) {
config.headers['X-Socket-Id'] = this.socketId();
}
return config;
});
}
/**
* Register jQuery AjaxPrefilter to add the X-Socket-ID header.
*/
registerjQueryAjaxSetup() {
if (typeof jQuery.ajax != 'undefined') {
jQuery.ajaxPrefilter((_options, _originalOptions, xhr) => {
if (this.socketId()) {
xhr.setRequestHeader('X-Socket-Id', this.socketId());
}
});
}
}
/**
* Register the Turbo Request interceptor to add the X-Socket-ID header.
*/
registerTurboRequestInterceptor() {
document.addEventListener('turbo:before-fetch-request', event => {
event.detail.fetchOptions.headers['X-Socket-Id'] = this.socketId();
});
}
}
return Echo;
})();