2020-06-02 16:51:13 -07:00
/ * T h i s S o u r c e C o d e F o r m i s s u b j e c t t o t h e t e r m s o f t h e M o z i l l a P u b l i c
* License , v . 2.0 . If a copy of the MPL was not distributed with this
* file , You can obtain one at http : //mozilla.org/MPL/2.0/. */
"use strict" ;
// Note: this file should be loadabale with eval() into worker environment.
// Avoid Components.*, ChromeUtils and global const variables.
const SIMPLE _CHANNEL _MESSAGE _NAME = 'juggler:simplechannel' ;
class SimpleChannel {
2023-09-11 18:16:33 -07:00
constructor ( name , uid ) {
2020-06-02 16:51:13 -07:00
this . _name = name ;
this . _messageId = 0 ;
this . _connectorId = 0 ;
this . _pendingMessages = new Map ( ) ;
this . _handlers = new Map ( ) ;
2020-11-02 16:21:34 -08:00
this . _bufferedIncomingMessages = [ ] ;
2020-06-02 16:51:13 -07:00
this . transport = {
sendMessage : null ,
2023-01-23 11:29:48 -08:00
dispose : ( ) => { } ,
2020-06-02 16:51:13 -07:00
} ;
2020-11-02 16:21:34 -08:00
this . _ready = false ;
2023-03-21 01:23:12 +00:00
this . _paused = false ;
2020-06-02 16:51:13 -07:00
this . _disposed = false ;
2023-09-11 18:16:33 -07:00
this . _bufferedResponses = new Map ( ) ;
// This is a "unique" identifier of this end of the channel. Two SimpleChannel instances
// on the same end of the channel (e.g. two content processes) must not have the same id.
// This way, the other end can distinguish between the old peer with a new transport and a new peer.
this . _uid = uid ;
this . _connectedToUID = undefined ;
2020-06-02 16:51:13 -07:00
}
2023-01-23 11:29:48 -08:00
bindToActor ( actor ) {
this . resetTransport ( ) ;
this . _name = actor . actorName ;
const oldReceiveMessage = actor . receiveMessage ;
actor . receiveMessage = message => this . _onMessage ( message . data ) ;
this . setTransport ( {
sendMessage : obj => actor . sendAsyncMessage ( SIMPLE _CHANNEL _MESSAGE _NAME , obj ) ,
dispose : ( ) => actor . receiveMessage = oldReceiveMessage ,
} ) ;
}
resetTransport ( ) {
this . transport . dispose ( ) ;
this . transport = {
sendMessage : null ,
dispose : ( ) => { } ,
} ;
this . _ready = false ;
}
2020-11-02 16:21:34 -08:00
setTransport ( transport ) {
this . transport = transport ;
// connection handshake:
// 1. There are two channel ends in different processes.
// 2. Both ends start in the `ready = false` state, meaning that they will
// not send any messages over transport.
2023-09-11 18:16:33 -07:00
// 3. Once channel end is created, it sends { ack: `READY` } message to the other end.
// 4. Eventually, at least one of the ends receives { ack: `READY` } message and responds with
// { ack: `READY_ACK` }. We assume at least one of the ends will receive { ack: "READY" } event from the other, since
2020-11-02 16:21:34 -08:00
// channel ends have a "parent-child" relation, i.e. one end is always created before the other one.
2023-09-11 18:16:33 -07:00
// 5. Once channel end receives either { ack: `READY` } or { ack: `READY_ACK` }, it transitions to `ready` state.
this . transport . sendMessage ( { ack : 'READY' , uid : this . _uid } ) ;
2020-11-02 16:21:34 -08:00
}
2023-03-21 01:23:12 +00:00
pause ( ) {
this . _paused = true ;
}
resumeSoon ( ) {
if ( ! this . _paused )
return ;
this . _paused = false ;
this . _setTimeout ( ( ) => this . _deliverBufferedIncomingMessages ( ) , 0 ) ;
}
_setTimeout ( cb , timeout ) {
// Lazy load on first call.
2025-06-04 13:50:57 +01:00
this . _setTimeout = ChromeUtils . importESModule ( 'resource://gre/modules/Timer.sys.mjs' ) . setTimeout ;
2023-03-21 01:23:12 +00:00
this . _setTimeout ( cb , timeout ) ;
}
2020-11-02 16:21:34 -08:00
_markAsReady ( ) {
if ( this . _ready )
return ;
this . _ready = true ;
2023-01-23 11:29:48 -08:00
for ( const { message } of this . _pendingMessages . values ( ) )
this . transport . sendMessage ( message ) ;
2020-11-02 16:21:34 -08:00
}
2020-06-02 16:51:13 -07:00
dispose ( ) {
if ( this . _disposed )
return ;
this . _disposed = true ;
for ( const { resolve , reject , methodName } of this . _pendingMessages . values ( ) )
reject ( new Error ( ` Failed " ${ methodName } ": ${ this . _name } is disposed. ` ) ) ;
this . _pendingMessages . clear ( ) ;
this . _handlers . clear ( ) ;
this . transport . dispose ( ) ;
}
_rejectCallbacksFromConnector ( connectorId ) {
for ( const [ messageId , callback ] of this . _pendingMessages ) {
if ( callback . connectorId === connectorId ) {
callback . reject ( new Error ( ` Failed " ${ callback . methodName } ": connector for namespace " ${ callback . namespace } " in channel " ${ this . _name } " is disposed. ` ) ) ;
this . _pendingMessages . delete ( messageId ) ;
}
}
}
connect ( namespace ) {
const connectorId = ++ this . _connectorId ;
return {
send : ( ... args ) => this . _send ( namespace , connectorId , ... args ) ,
emit : ( ... args ) => void this . _send ( namespace , connectorId , ... args ) . catch ( e => { } ) ,
dispose : ( ) => this . _rejectCallbacksFromConnector ( connectorId ) ,
} ;
}
register ( namespace , handler ) {
if ( this . _handlers . has ( namespace ) )
throw new Error ( 'ERROR: double-register for namespace ' + namespace ) ;
this . _handlers . set ( namespace , handler ) ;
2023-03-21 01:23:12 +00:00
this . _deliverBufferedIncomingMessages ( ) ;
return ( ) => this . unregister ( namespace ) ;
}
_deliverBufferedIncomingMessages ( ) {
2020-11-02 16:21:34 -08:00
const bufferedRequests = this . _bufferedIncomingMessages ;
this . _bufferedIncomingMessages = [ ] ;
2020-10-06 01:53:25 -07:00
for ( const data of bufferedRequests ) {
this . _onMessage ( data ) ;
}
2020-06-02 16:51:13 -07:00
}
unregister ( namespace ) {
this . _handlers . delete ( namespace ) ;
}
/ * *
* @ param { string } namespace
* @ param { number } connectorId
* @ param { string } methodName
* @ param { ... * } params
* @ return { ! Promise < * > }
* /
async _send ( namespace , connectorId , methodName , ... params ) {
if ( this . _disposed )
throw new Error ( ` ERROR: channel ${ this . _name } is already disposed! Cannot send " ${ methodName } " to " ${ namespace } " ` ) ;
const id = ++ this . _messageId ;
2023-01-23 11:29:48 -08:00
const message = { requestId : id , methodName , params , namespace } ;
2020-06-02 16:51:13 -07:00
const promise = new Promise ( ( resolve , reject ) => {
2023-01-23 11:29:48 -08:00
this . _pendingMessages . set ( id , { connectorId , resolve , reject , methodName , namespace , message } ) ;
2020-06-02 16:51:13 -07:00
} ) ;
2020-11-02 16:21:34 -08:00
if ( this . _ready )
this . transport . sendMessage ( message ) ;
2020-06-02 16:51:13 -07:00
return promise ;
}
2023-03-21 01:23:12 +00:00
_onMessage ( data ) {
2023-09-11 18:16:33 -07:00
if ( data ? . ack === 'READY' ) {
// The "READY" and "READY_ACK" messages are a part of initialization sequence.
// This sequence happens when:
// 1. A new SimpleChannel instance is getting initialized on the other end.
// In this case, it will have a different UID and we must clear
// `this._bufferedResponses` since they are no longer relevant.
// 2. A new transport is assigned to communicate between 2 SimpleChannel instances.
// In this case, we MUST NOT clear `this._bufferedResponses` since they are used
// to address the double-dispatch issue.
if ( this . _connectedToUID !== data . uid )
this . _bufferedResponses . clear ( ) ;
this . _connectedToUID = data . uid ;
this . transport . sendMessage ( { ack : 'READY_ACK' , uid : this . _uid } ) ;
2020-11-02 16:21:34 -08:00
this . _markAsReady ( ) ;
return ;
}
2023-09-11 18:16:33 -07:00
if ( data ? . ack === 'READY_ACK' ) {
if ( this . _connectedToUID !== data . uid )
this . _bufferedResponses . clear ( ) ;
this . _connectedToUID = data . uid ;
2020-11-02 16:21:34 -08:00
this . _markAsReady ( ) ;
return ;
}
2023-09-11 18:16:33 -07:00
if ( data ? . ack === 'RESPONSE_ACK' ) {
this . _bufferedResponses . delete ( data . responseId ) ;
return ;
}
2023-03-21 01:23:12 +00:00
if ( this . _paused )
this . _bufferedIncomingMessages . push ( data ) ;
else
this . _onMessageInternal ( data ) ;
}
async _onMessageInternal ( data ) {
2020-06-02 16:51:13 -07:00
if ( data . responseId ) {
2023-09-11 18:16:33 -07:00
this . transport . sendMessage ( { ack : 'RESPONSE_ACK' , responseId : data . responseId } ) ;
2023-01-23 11:29:48 -08:00
const message = this . _pendingMessages . get ( data . responseId ) ;
if ( ! message ) {
2023-09-11 18:16:33 -07:00
// During cross-process navigation, we might receive a response for
2023-01-23 11:29:48 -08:00
// the message sent by another process.
return ;
}
2020-06-02 16:51:13 -07:00
this . _pendingMessages . delete ( data . responseId ) ;
if ( data . error )
2023-01-23 11:29:48 -08:00
message . reject ( new Error ( data . error ) ) ;
2020-06-02 16:51:13 -07:00
else
2023-01-23 11:29:48 -08:00
message . resolve ( data . result ) ;
2020-06-02 16:51:13 -07:00
} else if ( data . requestId ) {
2023-09-11 18:16:33 -07:00
// When the underlying transport gets replaced, some responses might
// not get delivered. As a result, sender will repeat the same request once
// a new transport gets set.
//
// If this request was already processed, we can fulfill it with the cached response
// and fast-return.
if ( this . _bufferedResponses . has ( data . requestId ) ) {
this . transport . sendMessage ( this . _bufferedResponses . get ( data . requestId ) ) ;
return ;
}
2020-06-02 16:51:13 -07:00
const namespace = data . namespace ;
const handler = this . _handlers . get ( namespace ) ;
if ( ! handler ) {
2020-11-02 16:21:34 -08:00
this . _bufferedIncomingMessages . push ( data ) ;
2020-06-02 16:51:13 -07:00
return ;
}
const method = handler [ data . methodName ] ;
if ( ! method ) {
this . transport . sendMessage ( { responseId : data . requestId , error : ` error in channel " ${ this . _name } ": No method " ${ data . methodName } " in namespace " ${ namespace } " ` } ) ;
return ;
}
2023-09-11 18:16:33 -07:00
let response ;
const connectedToUID = this . _connectedToUID ;
2020-06-02 16:51:13 -07:00
try {
const result = await method . call ( handler , ... data . params ) ;
2023-09-11 18:16:33 -07:00
response = { responseId : data . requestId , result } ;
2020-06-02 16:51:13 -07:00
} catch ( error ) {
2023-09-11 18:16:33 -07:00
response = { responseId : data . requestId , error : ` error in channel " ${ this . _name } ": exception while running method " ${ data . methodName } " in namespace " ${ namespace } ": ${ error . message } ${ error . stack } ` } ;
}
// The connection might have changed during the ASYNCHRONOUS handler execution.
// We only need to buffer & send response if we are connected to the same
// end.
if ( connectedToUID === this . _connectedToUID ) {
this . _bufferedResponses . set ( data . requestId , response ) ;
this . transport . sendMessage ( response ) ;
2020-06-02 16:51:13 -07:00
}
} else {
2023-01-23 11:29:48 -08:00
dump ( ` WARNING: unknown message in channel " ${ this . _name } ": ${ JSON . stringify ( data ) } \n ` ) ;
2020-06-02 16:51:13 -07:00
}
}
}