Skip to content

Mixed context in Adobe CC extensions with CEP

Rashid Ghassempouri edited this page Sep 5, 2018 · 4 revisions

This article part of the documentation of this Toolbox for building Adobe CC extensions with CEP.

If you're reading this, it probably means either that you've already made Adobe CC extensions with CEP or you're struggling making one. If you belong to the second groupe I can only urge you to get Davide Barranca's incredible book, courses and blog which are the most complete documentation you'll find out there:

http://www.ps-scripting.com

http://www.htmlpanelsbook.com

http://www.davidebarranca.com

And of course the CEP team github page:

https://github.com/Adobe-CEP/Getting-Started-guides

For the rest of you who already have a sens of what's going on I'm going to summarize what I mean by "Mixed context", the kind of problematic it brings and hopefully some solutions. I'm very interested in what more experienced developers especially in this niche field of Adobe CC extensions have to say about this. Let's begin.

Two contexts: so close, yet so far

As you know building Adobe CC extensions with CEP requires communication between two core technical layers:

  1. the "panel" side: chromium and node in javascript (JS)
  2. the "native" side: the targeted app API in extendscript (JSX)

It means that your extension is made of a bunch of JS files loaded in the first context and a bunch of JSX files loaded in the second context. If you want to initiate communication from JS context to JSX context you can use the CEP CSInterface evalScript method with some callback to handle the asynchronous result of your call.

/*################################################################
JS CONTEXT
in a .js file included in js context
################################################################*/

var csInterface = new CSInterface();
csInterface.evalScript(
  'someObject.doSomething('+someDataObject+')', // the expression string to be evaluated on JSX side
  function(result) { } // callback action which will receive the return value from the evaluated expression
);

/*################################################################
JSX CONTEXT
in a .jsx file included in jsx context
################################################################*/

someObject = function() {}
someObject.doSomething = function(inputDataObject) { 
  /* do what you have to do */ 
  return outputDataObject; 
}

If you want to initiate communicate from JSX context to JS context you can dispatch custom CSXSEvents which CSInterface can listen to and handle in JS context.

/*################################################################
JSX CONTEXT
in a .jsx file included in jsx context
after making sure PlugPlugExternalObject is around
################################################################*/

var event = new CSXSEvent(); // create the event
event.type = "someEventTypeName"; // set custom type
event.data = someDataObject; // set custom data
event.dispatch(); // dispatch event to JS side

/*################################################################
JS CONTEXT
in a .js file included in js context
################################################################*/

var csInterface = new CSInterface();
csInterface.addEventListener(
  "someEventTypeName", 
  function(event) {  } // event handler function
);

It all makes sens and works well but it's not always practical for a few reasons:

Code split between JS and JSX contexts

Of course we have two different contexts and API with two similar but still different languages that completely justifies this split. But must of the time the extendscript code and javascript code are compatible and the main reason you split your code is because you have to. If it was possible to call the app API directly from the JS context I'm not sure that you would bother the split. To be fair even if it was possible to access the app API in one unified context it would make sens to separate "panel" and "native" code for architectural pattern concerns. But it would at least be practical to have the choice of where and when to split.

Asymetrical communciation API

AS explained before the two way communication between those two contexts are different. One based on string expression evaluation with asynchronous callback. The other based on an Event model without firsthand callback capabilities.

What if it was possible to get closer to a unified context or at least make the split less visible and the communication easier and more harmonized ?

Mixed context script: Two-Face script

Let's say that instead of having a set of files for each contexts (JS and JSX) we can have files (JSM, M for Mixed) that can be loaded and evaluated in both contexts. For the sake of example let's say we want to code a Debugger with a log and alert method which can of course be very handy in both context.

/*################################################################
JSM MIXED CONTEXT
in a .jsm file included in both contexts
################################################################*/

var myLogger = new Debugger();
myLogger.alert("HELLO WORLD");

Debugger = function () { this.isMuted = false; } // SAME CODE IN BOTH CONTEXT

Debugger.prototype.mute(shouldBeMuted) {this.isMuted = shouldBeMuted; } // SAME CODE IN BOTH CONTEXT

Debugger.prototype.log(message) {
  if (this.isMuted) return;
  //IF JSX CONTEXT -> use $.writeln
  //IF JS CONTEXT -> use console.log
} 

Debugger.prototype.alert(message) {
  if (this.isMuted) return;
  //IF JSX CONTEXT -> use alert;
  //IF JS CONTEXT -> use... wait a minute I also want to use alert in native app context !
} 

To start dealing with this simple situation in a mixed context approach we first have to know in which context the code is evaluated and also making it easier to communicate between the "two sides" of this mixed context script.

JSXBridge: an easier ride between contexts

JSXBridge is a simple library that brings you multiple tools to handle mixed context scripts and ease mixed context communication. Creating an instance of JSXBridge for an object implements that object with some useful methods to:

  • figure out the context of the object
  • call methods of other "bridged objects" in both contexts / having other "bridged objects" from both contexts call the the current object's methods.
  • dispatch custom events to other "bridged objects" from both contexts / listen to custom events dispatched from other "bridged objects" from both contexts.

Let's see how it works in our Debugger case:

/*################################################################
JSM MIXED CONTEXT
in a .jsm file included in both contexts
################################################################*/

var myLogger = new Debugger();
myLogger.alert("HELLO WORLD");

Debugger = function () {
  this.isMuted = false;
  new JSXBridge(this,'debugger');  // creating a JSXBridge for our object
}

Debugger.prototype.mute(shouldBeMuted) {this.isMuted = shouldBeMuted; } // SAME CODE IN BOTH CONTEXT

Debugger.prototype.log(message) {
  if (this.isMuted) return;
  // creating the JSXBridge in the constructor implements our object with checkContext() method
  // which let us figure out in which context the code is evaluated
  if (this.checkContext('jsx')) {
      $.writeln(message);
  } else {
      console.log(message);
  }
}

Debugger.prototype.alert(message) {
  if (this.isMuted) return;
  if (this.checkContext('jsx') {
      alert(message);
  } else {
      // creating the JSXBridge in the constructor implements our object with mirror() method
      // which let us call a method in the same object in the opposite context
      this.mirror('alert',message);
  }
}

In this example we can see in action 2 helper methods brought by JSXBridge:

  • checkContext() let us figure out in which context the code is evaluated
  • mirror() let us call a method in the same object in the opposite context

Notice that only the context specific part of the code has to be written in a "safe scope" checked against the context in which that part of the code will be evaluated.

JSXBridge Call Model

Now that we've seen a simple example of a mixed context script let's have a more detailed view of the JSXBridge mixed context function call capabilities. The mirror() method we used in the previous example is actually a shortcode for the more complete method bridgeCall(). mirror() can call a method in the same object in the opposite context. bridgeCall() can call a method in any client ("bridged" object) in a certain scope. We'll see what client and scope do really mean in a minute. For now let's see how it works in a simple example:

/*################################################################
JSX CONTEXT
in a .jsx file included in jsx context
################################################################*/

ClassX = function() { new JSXBridge(this,'class_x'); }

ClassX.prototype.doSomething(param) { /*optionaly return something the callback function */ }

var obj_x = new ClassX();

/*################################################################
JS CONTEXT
in a .js file included in js context
################################################################*/

ClassY = function () {new JSXBridge(this,'class_y');}

var obj_y = new ClassY();

obj_y.bridgeCall(
  "class_x", // the client id of the bridged object, a method of which we want to call
  "doSomething", // the name of the method we want to call
  "someParam", // some param object to pass to the method
  function(result){/*do something*/}, // callback function or (to be evaluated) expression to be executed with the called method return value
  "jsx" // the context(s) in which the method should be called
  );

An obj_x instance of classX ,which creates a JSXBridge with "class_x" id, exists in JSX context as its file is included in JSX context.

An obj_y instance of classY ,which also creates a JSXBridge, exists in JS context as its file is included in JS context.

obj_y from JS context is calling obj_x's doSomething() method in JSX context with the bridgeCall() method. 

This call path is resolved thanks to 3 arguments:

  • the client id of the instance we want to call a method from = "class_x"
  • the function name of the method we want to call = "doSomething"
  • the scope we want to call the method in = "jsx"

This is a very basic example of a JS object trying to call a JSX object method.  Let's see what more we can do by giving a more in depth explanation of client and scope.

JSXBridge clients

Creating a new JSXBridge requires 2 parameters:

  • a client_object
  • a client_id

So if you create a JSXBridge in a certain context it will create and register a bridge for the client_object identified with the client_id in that context. This bridge will help JSXBridge to easily call methods from the client_object which will be executed in the client_object context.

Consider the example below:

/*################################################################
JS CONTEXT
in a .js file included in js context
################################################################*/

ClassJS = function () { new JSXBridge(this,'class_js');}

var obj_js = new ClassJS();

/*################################################################
JSX CONTEXT
in a .jsx file included in jsx context
################################################################*/

ClassJSX = function () { new JSXBridge(this,'class_jsx');}

var obj_jsx = new ClassJSX();

/*################################################################
JSM MIXED CONTEXT
in a .jsm file included in both contexts
################################################################*/

ClassJSM = function () { new JSXBridge(this,'class_jsm');}

var obj_jsm = new ClassJSM();

JS context only:

obj_js instance of ClassJS, is in a file which is included only in JS context therefore it will only be registered by JSXBridge in JS context identified by "class_js" client id. This means that obj_js methods can be called from both contexts BUT can be executed only in JS context.

JSX context only:

obj_jsx instance of ClassJSX, is in a file which is included only in JSX context therefore it will only be registered by JSXBridge in JSX context identified by "class_jsx" client id. This means that obj_jsx methods can be called from both contexts BUT can be executed only in JSX context.

Mixed context (JSM):

obj_jsm instance of ClassJSM, is in a file which is included in both contexts therefore its JS instance will be registered by JSXBridge in JS context and its JSX instance will be registered by JSXBridge in JSX context. Both with the same "class_jsm" client id. This means that obj_jsm methods can be called from both contexts AND can be executed in both context.

JSXBridge scopes

JSXBridge scopes will help us define in which context we want the method targeted by the JSXBridge call to be executed. 

There are different 5 scope definitions:

  • "jsx" stands for JSX context
  • "js" stands for JS context
  • "current" stands for the current context in other words the same context than the context the call comes from.
  • "mirror" stands for the "opposite" context of the current context.
  • "both" stands for both JS and JSX contexts

Here is a simple example of scope usage:

/*################################################################
JSM MIXED CONTEXT
in a .jsm file included in both contexts
################################################################*/

ClassX = function() { new JSXBridge(this,'class_x'); }

ClassX.prototype.doSomething(param) {
  if (this.checkContext('jsx') {
      $.writleln("HELLO FROM JSX");
  } else {
      console.log("HELLO FROM JS");
  }
}

var obj_x = new ClassX();

/*################################################################
SOMEWHERE IN ANY CONTEXT
################################################################*/

ClassY = function () {new JSXBridge(this,'class_y');}

var obj_y = new ClassY();

obj_y.bridgeCall(
  "class_x",
  "doSomething",
  null,
  null,
  JSXBridgeEventScope.BOTH // "both" scope will push the call to execute the method in both contexts
  );

/*################################################################
RESULTS: doSomething method is called in both contexts 
> "HELLO FROM JSX" is printed in EXTENDSCRIPT TOOL console
> "HELLO FROM JS" is printed in debug CHROMIUM console
################################################################*/

In this example it's possible to make the call in "both" scope only since obj_x file has been included in both contexts. Notice that all scopes are although defined as values of static variables of a JSXBridgeEventScope class, as used in the previous example.

These scopes will also be very useful for our next subject: JSXBridge Event Model.

JSXBridge Event Model

As mentioned at the beginning of this note we don't have a symmetrical / harmonized communication API between our two contexts.

JSXBridge can help us with this matter too.

Every JSXBridge client is also implemented with 2 Observer methods:

  • listen() let's the client listen to custom JSXBridgeEvents dispatched from JSXBridge clients.
  • dispatch() let's the client dispatch custom JSXBridgeEvents to JSXBridge clients.

Notice that JSXBridgeEvents' dispatchs are also "scope aware" so you can decide in which scope (to which context(s)) you want to dispatch your event.

Lets see how it works :

/*################################################################
JS CONTEXT
in a .js file included in js context
################################################################*/

ClassJS = function () { new JSXBridge(this,'class_js');}

var obj_js = new ClassJS();

obj_js.listen(
  "SOME_EVENT_TYPE",
  function(event) { 
    console.log(
      "sent from " + event.context 
    + " | scope = " + event.scope 
    + " | data = " + event.data
    ); 
  }
);

//later on...

obj_js.dispatch( "SOME_EVENT_TYPE" , 1 , JSXBridgeEventScope.JS );
obj_js.dispatch( "SOME_EVENT_TYPE" , 2 , JSXBridgeEventScope.JSX );
obj_js.dispatch( "SOME_EVENT_TYPE" , 3 , JSXBridgeEventScope.CURRENT );
obj_js.dispatch( "SOME_EVENT_TYPE" , 4 , JSXBridgeEventScope.MIRROR );
obj_js.dispatch( "SOME_EVENT_TYPE" , 5 , JSXBridgeEventScope.BOTH );

/*################################################################
JSX CONTEXT
in a .jsx file included in jsx context
################################################################*/

ClassJSX = function () { new JSXBridge(this,'class_jsx');}

var obj_jsx = new ClassJSX();

obj_jsx.listen(
  "SOME_EVENT_TYPE",
  function(event) { 
    $.writeln(
      "sent from " + event.context 
    + " | scope = " + event.scope 
    + " | data = " + event.data
    ); 
  }
);

/*################################################################
RESULT logged by obj_js in JS conext:
> sent from js | scope = js | data = 1
> sent from js | scope = current | data = 3
> sent from js | scope = both | data = 5
-----------------------------------------------------------------
RESULT logged by obj_jsx in JSX conext:
> sent from js | scope = jsx | data = 2
> sent from js | scope = mirror | data = 4
> sent from js | scope = both | data = 5
################################################################*/

In this example we have 2 "bridged objects" : obj_js in JS context and obj_jsx in JSX context.

Both objects listen to "SOME_EVENT_TYPE" event and the event handler prints the context from which the recieved event has been sent, the scope of the event and its encapsulated data.

For the sake of the example only obj_js dispatches 5 events with the type "SOME_EVENT_TYPE" testing all of the 5 scopes with a different data (integer 1 to 5). As JSXBridge Event Model works both ways in both contexts we could have of course let obj_jsx dispatch some events but let's say that it was not its turn ^^

Both objects only receive the events that match their context :

  • event 1 with JS scope is only received by obj_js which is in JS context.
  • event 2 with JSX scope is only received by obj_jsx which is in JSX context.
  • event 3 with CURRENT scope is only sent to the current context of the dispatcher. As the dispatcher is obj_js in JS context it is sent to the JS scope and will only be received by obj_js which is in JS context.
  • event 4 with MIRROR scope is only sent to the opposite context of the dispatcher. As the dispatcher is obj_js in JS context it's sent to the JSX scope and is only received by obj_jsx which is in JSX context.
  • event 5 with BOTH scope is sent to both contexts. Therefore both obj_js in JS context and obj_jsx in JSX context receive it.