-
-
Save br3nt/c6aeec77fcef1a1f3907 to your computer and use it in GitHub Desktop.
Cross-tab window communication
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]; | |
| } |
Author
br3nt
commented
Sep 4, 2024
Author
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 Implementationconst 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 Implementationconst 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