omniJS – limitation or bug in getting complex values from URL.tellScript?

Should we be able to pass a nested value between Omni apps using URL.tellScript(String).call(Function) ?

There is no comment on this in the current API docs, but the very useful omni-automation.com example passes a flat array of strings (top-level OO topics -> named OG canvases), and advises:

“It is important to note that the generated Omni Automation script URL should return some type [of?] data (like a string or an array) as its result” (my emphases)

Must the array contain only simple atomic values ? I have found that data transfer fails with an array of nested dictionaries, and I am not sure whether that is:

  1. An architectural constraint which could be usefully clarified in the docs (and perhaps on omni-automation.com) ?
  2. An unintended bug which might prove fixable ?

As an example, the following code does successfully import the full depth of an OmniOutliner outline as a nested tree diagram, but only by getting omniOutliner to return a JSON serialisation of the array of dictionaries (rather than the array itself), and using JSON.parse in OmniGraffle to deserialize the passed JSON string.

(If we try to pass a nested [Dict] array directly, rather than passing a JSON string, we seem to get an array of vanilla Objects stripped of further properties) (and no error message …)

Is this (known / inevitable / by design) or just a bug ?

(() => {
    'use strict';

    // Rob Trew 2017

    // OMNIGRAFFLE 7.5 - OMNIJS CODE TO:
    // 1. READ AN OMNIOUTLINER OUTLINE THRU URL.tellScript(s).call(f(v))
    // 2. CREATE A NESTED TREE DIAGRAM FROM THE OUTLINE

    // OMNI-OUTLINER OUTLINE-READING FUNCTION --------------------------------

    // docTextNests :: OO () ->
    //      [TextNest :: {text :: String, nest :: TextNest}]
    const docTextNests = () => {
        const tn = x => ({
            text: x.topic,
            nest: x.children.map(tn)
        });
        // IT SEEMS THAT WE HAVE TO STRINGIFY HERE ...
        // IF WE TRY TO PASS THE ARRAY ITSELF, OMNIGRAFFLE
        // GETS ONLY AN ARRAY OF EMPTY OBJECTS ....
        return JSON.stringify(document.outline.rootItem.children.map(tn));
    };

    // OMNIGRAFFLE DIAGRAM-BUILDING FUNCTION ---------------------------------

    // Blank Canvas -> Dictionary with text and list of child dictionaries
    //                   -> Root shape of new diagram
    // nestedDiagram :: OG Canvas ->
    //          TextNest :: {text :: String, nest :: TextNest} -> OG Shape
    const nestedDiagram = (cnv, dctNode) => {
        const shp = cnv.newShape();
        return (
            // Effect
            shp.geometry = new Rect(0, 0, 120, 120),
            shp.text = dctNode.text || '',
            dctNode.nest.map(x => {
                const child = nestedDiagram(cnv, x);
                return (cnv.connect(shp, child), child);
            }),
            // Value
            shp
        );
    };

    // EVALUATING OMNI-OUTLINER CODE (FROM INSIDE OMNIGRAFFLE) ---------------
    // TO A (STRINGIFIED) NEST OF TEXTS
    URL.tellScript('omnioutliner', '(' + docTextNests + ')()')
        .call(strJSON => {
            const cnv = addCanvas();
            document.windows[0].selection.view.canvas = cnv;
            cnv.layoutInfo.automaticLayout = true;

            // TextNest de-serialized
            JSON.parse(strJSON)

                // Tree diagram built
                .map(x => nestedDiagram(cnv, x));
        });
})();

FWIW an ES5 JS recompilation, though I think this should be irrelevant, as the base macOS for recent OO builds is now Sierra:

(function() {
  var d = function(b, a) {
    var c = b.newShape();
    return c.geometry = new Rect(0, 0, 120, 120), c.text = a.text || "", a.nest.map(function(a) {
      a = d(b, a);
      return b.connect(c, a), a;
    }), c;
  };
  URL.tellScript("omnioutliner", "(" + function() {
    var b = function(a) {
      return {text:a.topic, nest:a.children.map(b)};
    };
    return JSON.stringify(document.outline.rootItem.children.map(b));
  } + ")()").call(function(b) {
    var a = addCanvas();
    document.windows[0].selection.view.canvas = a;
    a.layoutInfo.automaticLayout = !0;
    JSON.parse(b).map(function(b) {
      return d(a, b);
    });
  });
})();

This is an architectural constraint which isn’t trivial to fix, but ought to be eventually fixable, and we decided to ship with this limitation for our first version. It should certainly be documented better.

The issue is that the JS proxy objects which represent our application model objects have raw pointer values back into our stuff, et cetera, and so a naive serialization of those proxies gives away runtime heap layout info that along with a theoretical buffer overflow or some such, could be a security problem. So in an abundance of caution, we don’t let those escape the local app, and the most straightforward way of doing that was to limit tellScript arguments to containing only non-object types.

It’s certainly possible in the future to complicate this code to allow ‘clean’ dictionary objects, or to be smarter than that, and allow any objects while stripping internal implementation details, it just hasn’t been a high priority yet.

1 Like

Thanks ! That makes a lot of sense.

My feeling is that the JSON.stringify -> JSON.parse approach is absolutely fine, and probably rather fast …

Perhaps just document it and treat it as a feature or best practice ?

1 Like

I agree, the JSON workaround is a good way to go, and we should (for the near future, at least) just document that as best practice. Thanks!

1 Like