Skip to content

Instantly share code, notes, and snippets.

@br3nt
Forked from neilj/window-controller.js
Last active September 4, 2024 13:18
Show Gist options
  • Select an option

  • Save br3nt/c6aeec77fcef1a1f3907 to your computer and use it in GitHub Desktop.

Select an option

Save br3nt/c6aeec77fcef1a1f3907 to your computer and use it in GitHub Desktop.
Cross-tab window communication
function Dispatcher(channel_name, broadcast_to_self) {
this.channel_name = channel_name ? 'dispatcher:' + channel_name : 'dispatcher:default_channel';
this.id = Math.random();
this.isMaster = false;
this.others = {};
this.broadcast_to_self = broadcast_to_self === undefined ? true || !!broadcast_to_self;
this.callbacks = {};
window.addEventListener( 'storage', this, false );
window.addEventListener( 'unload', this, false );
this.broadcast( 'hello' );
var that = this;
var check = function check () {
that.check();
that._checkTimeout = setTimeout( check, 9000 );
};
var ping = function ping () {
that.sendPing();
that._pingTimeout = setTimeout( ping, 17000 );
};
this._checkTimeout = setTimeout( check, 500 );
this._pingTimeout = setTimeout( ping, 17000 );
}
Dispatcher.prototype.destroy = function () {
clearTimeout( this._pingTimeout );
clearTimeout( this._checkTimeout );
window.removeEventListener( 'storage', this, false );
window.removeEventListener( 'unload', this, false );
this.broadcast( 'bye' );
};
Dispatcher.prototype.handleEvent = function ( event ) {
if ( event.type === 'unload' ) {
this.destroy();
} else if ( event.key === this.channel_name ) {
var data = JSON.parse( event.newValue ), type = data.type;
switch ( type ) {
case 'ping':
case 'hello':
case 'bye':
this[ type ]( data );
break;
default:
try {
this.callbacks[ type ].call( this, data );
}
catch ( error ) {}
}
}
};
Dispatcher.prototype.sendPing = function () {
this.broadcast( 'ping' );
};
Dispatcher.prototype.hello = function ( event ) {
this.ping( event );
if ( event.id < this.id ) {
this.check();
} else {
this.sendPing();
}
};
Dispatcher.prototype.ping = function ( event ) {
this.others[ event.id ] = +new Date();
};
Dispatcher.prototype.bye = function ( event ) {
delete this.others[ event.id ];
this.check();
};
Dispatcher.prototype.check = function ( event ) {
var now = +new Date(),
takeMaster = true,
id;
for ( id in this.others ) {
if ( this.others[ id ] + 23000 < now ) {
delete this.others[ id ];
} else if ( id < this.id ) {
takeMaster = false;
}
}
if ( this.isMaster !== takeMaster ) {
this.isMaster = takeMaster;
this.masterDidChange( takeMaster );
}
};
Dispatcher.prototype.masterDidChange = function () {};
Dispatcher.prototype.broadcast = function ( type, data ) {
var event = {
id: this.id,
type: type
};
for ( var x in data ) {
event[x] = data[x];
}
try {
data = JSON.stringify( event );
localStorage.setItem( this.channel_name, data );
if (this.broadcast_to_self) this.callbacks[ type ].call( this, JSON.parse(data) );
} catch ( error ) {}
};
Dispatcher.prototype.on = function (event_name, callback) {
this.callbacks[event_name] = callback;
return this;
}
Dispatcher.prototype.off = function (event_name) {
delete this.callbacks[event_name];
}
@br3nt
Copy link
Author

br3nt commented Sep 4, 2024

Copilot

Great questions! Handling race conditions and tab crashes are crucial for ensuring the robustness of your solution. Here are some strategies to address these issues:
Preventing Race ConditionsTo prevent multiple tabs from simultaneously trying to become the controller, you can use a combination of localStorage and a timestamp-based locking mechanism. Here's how you can do it:

  • Locking Mechanism: Use localStorage to create a lock with a timestamp.
  • Atomic Operations: Ensure that the operations to check and set the controller are atomic.
    Example Implementation
const channel = new BroadcastChannel('tab-communication');
let isController = false;

function becomeController() {
  isController = true;
  const eventSource = new EventSource('/events');
  eventSource.onmessage = (event) => {
    channel.postMessage(event.data);
  };
  window.addEventListener('beforeunload', () => {
    localStorage.removeItem('controller');
    channel.postMessage('controller-closed');
  });
}

function checkController() {
  const now = Date.now();
  const controller = localStorage.getItem('controller');
  if (!controller || now - parseInt(controller) > 5000) { // 5 seconds timeout
    localStorage.setItem('controller', now.toString());
    becomeController();
  }
}

checkController();

channel.onmessage = (message) => {
  if (message.data === 'controller-closed' && !isController) {
    checkController();
  } else if (!isController) {
    // Handle the event data
    console.log('Received event:', message.data);
  }
};

Handling Tab CrashesTo handle situations where the controller tab crashes and the beforeunload event is not called, you can implement a heartbeat mechanism:

  • Heartbeat Mechanism: The controller tab periodically updates a timestamp in localStorage.
  • Timeout Detection: Other tabs check this timestamp to detect if the controller tab has crashed.
    Example Implementation
const HEARTBEAT_INTERVAL = 2000; // 2 seconds
const HEARTBEAT_TIMEOUT = 5000; // 5 seconds

function startHeartbeat() {
  setInterval(() => {
    if (isController) {
      localStorage.setItem('controller-heartbeat', Date.now().toString());
    }
  }, HEARTBEAT_INTERVAL);
}

function checkController() {
  const now = Date.now();
  const controller = localStorage.getItem('controller');
  const heartbeat = localStorage.getItem('controller-heartbeat');
  if (!controller || !heartbeat || now - parseInt(heartbeat) > HEARTBEAT_TIMEOUT) {
    localStorage.setItem('controller', now.toString());
    becomeController();
  }
}

startHeartbeat();
checkController();

channel.onmessage = (message) => {
  if (message.data === 'controller-closed' && !isController) {
    checkController();
  } else if (!isController) {
    // Handle the event data
    console.log('Received event:', message.data);
  }
};

Key Points- Locking Mechanism: Prevents race conditions by using a timestamp-based lock.

  • Heartbeat Mechanism: Ensures that a new controller is elected if the current one crashes.
  • Timeout Detection: Other tabs detect if the controller tab has stopped updating the heartbeat.
    These strategies should help you create a robust solution for managing multiple tabs with a single controller for server-sent events. If you have any more questions or need further assistance, feel free to ask!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment