Git commit a8011c9cec02a1c056da46f0448c96362d838103 by Thomas Friedrichsmeier.
Committed on 07/05/2025 at 13:21.
Pushed by tfry into branch 'master'.

Add promise-based mechanism for running R commands from plugins

M  +32   -23   doc/rkwardplugins/index.docbook
M  +11   -14   rkward/plugins/data/level_select.xml
M  +24   -0    rkward/scriptbackends/rkcomponentscripting.cpp
M  +3    -1    rkward/scriptbackends/rkcomponentscripting.h
M  +92   -86   rkward/scriptbackends/rkcomponentscripting.js

https://invent.kde.org/education/rkward/-/commit/a8011c9cec02a1c056da46f0448c96362d838103

diff --git a/doc/rkwardplugins/index.docbook b/doc/rkwardplugins/index.docbook
index e31be3f11..885c8cac2 100644
--- a/doc/rkwardplugins/index.docbook
+++ b/doc/rkwardplugins/index.docbook
@@ -1654,38 +1654,44 @@ This chapter contains information on some topics that 
are useful only to certain
        <para>With this in mind, here is the general pattern. You will use this 
inside a <link linkend="logic_scripted">scripted UI logic</link> section:</para>
        <programlisting>
                &lt;script&gt;&lt;![CDATA[
-                               last_command_id = -1;
                                gui.addChangeCommand ("variable", "update ()");
                                update = function () {
                                        gui.setValue ("selector.enabled", 0);
                                        variable = gui.getValue ("variable");
                                        if (variable == "") return;
 
-                                       last_command_id = doRCommand ('levels 
(' + variable + ')', "commandFinished");
-                               }
-
-                               commandFinished = function (result, id) {
-                                       if (id != last_command_id) return;  // 
another result is about to arrive
-                                       if (typeof (result) == "undefined") {
-                                               gui.setListValue 
("selector.available", Array ("ERROR"));
-                                               return;
-                                       }
-                                       gui.setValue ("selector.enabled", 1);
-                                       gui.setListValue ("selector.available", 
result);
+                                       new RCommand('levels (' + variable + 
')', "myid").then(result => {
+                                               gui.setValue 
("selector.enabled", 1);
+                                               gui.setListValue 
("selector.available", result);
+                                       }).catch(msg => {
+                                               if (msg === "outdated") return; 
 // command was canceled, because new one is about to arrive -> benign
+                                               // possibly other error 
handling, msg carries the warnings and error messages produced,
+                                               // if the command failed e.g.:
+                                               gui.setListValue 
("selector.available", Array ("ERROR:", msg));
+                                       });
                                }
                ]]&gt;&lt;/script&gt;
        </programlisting>
        <para>Here, <parameter>variable</parameter> is a property holding an 
object name (&eg; inside a <command>&lt;varslot&gt;</command>). Whenever that 
changes, you will want to update the display
-             of levels inside the <command>&lt;valueselector&gt;</command>, 
named <parameter>selector</parameter>. The key function here is 
<command>doRCommand()</command>, taking as first parameter
-                 the command string to run, and as second parameter the name 
of a function to call, when the command has finished. Note that the command is 
running asynchronously, and this makes things
-                 a bit more complex. For one thing you want to make sure, that 
your <command>&lt;valueselector&gt;</command> remains disabled, while it does 
not contain up-to-date information. Another
-                 thing is that you could potentially have queued more than one 
command, before you get the first results. This is why every command is given 
an "id", and we store that in <parameter>last_command_id</parameter> for later 
reference.</para>
-         <para>When the command is done, the specified callback is called 
(<parameter>commandFinished</parameter>, in this case) with two parameters: The 
result itself, and the id of the corresponding
-                 command. The result will be of a type resembling the 
representation in &R;, &ie; a numeric Array, if the result is numeric, &etc; It 
can even be an &R; <command>list()</command>, but in this case
-                 it will be represented as a JS <command>Array()</command> 
without names.</para>
-         <para>Note that even this example is somewhat simplified. In reality 
you should take additional precautions, &eg; to avoid putting an extreme amount 
of levels into the selector. The good news
-                 is that probably you do not have to do all this yourself. The 
above example is taken from the <command>rkward::level_select</command> plugin, 
for instance, which you can simply <link linkend="embedding">embed</link> in 
your own
-                 plugin. This even allows you to specify a different 
expression to run in place of <command>levels()</command>.</para>
+       of levels inside the <command>&lt;valueselector&gt;</command>, named 
<parameter>selector</parameter>. The key function here is the constructor 
statement <command>new RCommand()</command>, taking as first parameter
+       the command string to run. Note that the command is running 
asynchronously, and this makes things
+       a bit more complex. For one thing you want to make sure, that your 
<command>&lt;valueselector&gt;</command> remains disabled, while it does not 
contain up-to-date information.
+       Secondly, as the user may make changes, quickly, more than one command 
may have been generated, before we receive any result. Thus, you'll need to 
make sure to act on the most recent
+       command, only.
+       </para>
[suppressed due to size limit]
+       be helpful, during development, to insert a 
<command>Sys.sleep(1);</command> into your R command, to see what happens, when 
a command does not complete, immediately.</para>
+       <para>Finally, to deal with multiple commands being generated, you can 
specify a second argument to <command>new RCommand()</command>, ("myid", in 
this example). Any comands with the same (freely chosen) identifier will be 
understood to belong to the same queue. RKWard will then make sure that only 
the latest command will actually trigger the <command>.then()</command> block, 
while any obsoleted commands will arrive in the
+       <command>.catch()</command> block. Here, obsoleted commands can be 
identified, as the string "outdated" is passed as their value, while for any 
other possible errors, the warnings, and error messages are passed along.
+       </para>
+       <para>Note that this example is somewhat simplified. In reality you 
should take additional precautions, &eg; to avoid putting an extreme amount of 
levels into the selector. The good news
+               is that probably you do not have to do all this yourself. The 
above example is taken from the <command>rkward::level_select</command> plugin, 
for instance, which you can simply <link linkend="embedding">embed</link> in 
your own
+               plugin. This even allows you to specify a different expression 
to run in place of <command>levels()</command>.</para>
+       <note>
+               <para>In earlier versions of RKWard R commands were run using a 
somewhat more complex <command>doRCommand()</command> function. You may still 
see that in some plugins, but it is not recommended to use it
+               in new code.</para>
+       </note>
+
 </sect1>
 
 <sect1 id="current_object">
@@ -4502,7 +4508,10 @@ different types, using modifiers may lead to errors. For 
<replaceable>fixed_valu
 <listitem><para><command>include(filename)</command> can be used to include a 
separate JS file.</para></listitem>
 </varlistentry>
 <varlistentry><term>doRCommand()-function</term>
-<listitem><para><command>doRCommand(command, callback)</command> can be used 
to query &R; for information. Please read the section on <link 
linkend="querying_r_for_info">querying &R; from inside a plugin</link> for 
details, and caveats.</para></listitem>
+<listitem><para><emphasis>Obsolete. Do not use in new plugins:</emphasis> 
<command>doRCommand(command, callback)</command>. Use <command>new 
RCommand()</command>, instead.</para></listitem>
+</varlistentry>
+<varlistentry><term>new RCommand()-function</term>
+<listitem><para><command>new RCommand(command, optional_id)</command> can be 
used to query &R; for information. Please read the section on <link 
linkend="querying_r_for_info">querying &R; from inside a plugin</link> for 
details, and caveats.</para></listitem>
 </varlistentry>
 </variablelist>
 </sect1>
diff --git a/rkward/plugins/data/level_select.xml 
b/rkward/plugins/data/level_select.xml
index 1a7651bd3..6741cb4d2 100644
--- a/rkward/plugins/data/level_select.xml
+++ b/rkward/plugins/data/level_select.xml
@@ -19,7 +19,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
                <external id="limit" default="100"/>
 
                <script><![CDATA[
-                               last_command_id = -1;
                                gui.setValue ("limitnote.visible", false);
 
                                gui.addChangeCommand ("variable", "update ()");
@@ -46,19 +45,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
                                        code += "\tx\n";
                                        code += "})";
 
-                                       last_command_id = doRCommand (code, 
"commandFinished");
-                               }
-
-                               commandFinished = function (result, id) {
-                                       if (id != last_command_id) return;  // 
another result is about to arrive
-                                       if (typeof (result) == "undefined") {
-                                               gui.setListValue 
("selector.available", Array ("ERROR"));
-                                               return;
-                                       }
-                                       gui.setValue ("selector.enabled", 1);
-                                       limit = gui.getValue ("limit");
-                                       gui.setValue ("limitnote.visible", 
result.length > limit ? 1 : 0);
-                                       gui.setListValue ("selector.available", 
result.slice (0, limit));
+                                       new RCommand(code, "getvals")
+                                               .then(result => {
+                                                       if (result === 
"outdated") return;  // another result is about to arrive
+                                                       
gui.setValue("selector.enabled", 1);
+                                                       limit = 
gui.getValue("limit");
+                                                       
gui.setValue("limitnote.visible", result.length > limit ? 1 : 0);
+                                                       
gui.setListValue("selector.available", result.slice(0, limit));
+                                               })
+                                               .catch(err => {
+                                                       
gui.setListValue("selector.available", Array("ERROR:", err));
+                                               });
                                }
                ]]></script>
        </logic>
diff --git a/rkward/scriptbackends/rkcomponentscripting.cpp 
b/rkward/scriptbackends/rkcomponentscripting.cpp
index 8911c3c60..7d71e65e6 100644
--- a/rkward/scriptbackends/rkcomponentscripting.cpp
+++ b/rkward/scriptbackends/rkcomponentscripting.cpp
@@ -157,6 +157,30 @@ static QJSValue marshall(QJSEngine *engine, const RData 
*data) {
        return QJSValue();
 }
 
+void RKComponentScriptingProxy::doRCommand2(const QString &command, const 
QString &id, const QJSValue resolve, const QJSValue reject) {
+       RK_TRACE(PHP);
+       auto c = new RCommand(command, RCommand::PriorityCommand | 
RCommand::GetStructuredData | RCommand::Plugin);
+       if (!id.isNull()) {
+               auto old_c = latest_commands.value(id);
+               if (old_c) RInterface::instance()->softCancelCommand(old_c);
+               latest_commands.insert(id, c);
+       }
+       c->whenFinished(this, [this, resolve, reject, id](RCommand *command) {
+               QJSValue res;
+               auto latest_c = id.isNull() ? nullptr : 
latest_commands.value(id);
+               if (latest_c && (latest_c != command)) {
+                       res = 
reject.call(QJSValueList{(QJSValue(u"outdated"_s))});
+               } else if (command->failed()) {
+                       res = 
reject.call(QJSValueList{(QJSValue(command->warnings() + command->error()))});
+               } else {
+                       res = resolve.call(QJSValueList({(marshall(&engine, 
command))}));
+               }
+               handleScriptError(res);
+               if (latest_c == command) latest_commands.remove(id);
+       });
+       RInterface::issueCommand(c);
+}
+
 void RKComponentScriptingProxy::scriptRCommandFinished(RCommand *command) {
        RK_TRACE(PHP);
 
diff --git a/rkward/scriptbackends/rkcomponentscripting.h 
b/rkward/scriptbackends/rkcomponentscripting.h
index 735b90c5e..015bd90b3 100644
--- a/rkward/scriptbackends/rkcomponentscripting.h
+++ b/rkward/scriptbackends/rkcomponentscripting.h
@@ -42,6 +42,7 @@ class RKComponentScriptingProxy : public QObject {
        Q_INVOKABLE void addChangeCommand(const QString &changed_id, const 
QString &command);
        /** @returns id of the command issued. */
        Q_INVOKABLE QVariant doRCommand(const QString &command, const QString 
&callback);
+       Q_INVOKABLE void doRCommand2(const QString &command, const QString &id, 
const QJSValue resolve, const QJSValue reject);
 
        Q_INVOKABLE QVariant getValue(const QString &id) const;
        Q_INVOKABLE QVariant getString(const QString &id) const;
@@ -66,8 +67,9 @@ class RKComponentScriptingProxy : public QObject {
                QString callback;
        };
        QList<OutstandingCommand> outstanding_commands;
+       QHash<QString, RCommand *> latest_commands;
        QString _scriptfile;
-       void evaluate(const QString &code, const QString &filename=QString());
+       void evaluate(const QString &code, const QString &filename = QString());
 
        void handleChange(RKComponentBase *changed);
        QHash<RKComponentBase *, QString> component_commands;
diff --git a/rkward/scriptbackends/rkcomponentscripting.js 
b/rkward/scriptbackends/rkcomponentscripting.js
index 0942394b0..720c63f83 100644
--- a/rkward/scriptbackends/rkcomponentscripting.js
+++ b/rkward/scriptbackends/rkcomponentscripting.js
@@ -62,7 +62,7 @@ function Component(id) {
 
        this.addChangeCommand = function(id, command) {
                _rkward.addChangeCommand(this.absoluteId(id), command);
-       }
+       };
 };
 
 makeComponent = function(id) {
@@ -73,91 +73,97 @@ gui = new Component("");
 
 doRCommand = function(command, callback) {
        return (_rkward.doRCommand(command, callback));
-}:
-
-    function RObject(objectname) {
-           this.objectname = objectname;
-
-           // for internal use
-           this.initialize = function() {
-                   info = _rkward.getObjectInfo(this.objectname);
-
-                   this._dimensions = info.shift();
-                   this._classes = info.shift();
-                   this._isDataFrame = info.shift();
-                   this._isMatrix = info.shift();
-                   this._isList = info.shift();
-                   this._isFunction = info.shift();
-                   this._isEnvironment = info.shift();
-                   this._datatype = info.shift();
-           };
-
-           this.initialize();
-
-           this.getName = function() {
-                   return (this.objectname);
-           };
-
-           this.exists = function() {
-                   return (typeof (this._dimensions) != "undefined");
-           };
-
-           this.dimensions = function() {
-                   return (this._dimensions);
-           };
-
-           this.classes = function() {
-                   return (this._classes);
-           };
-
-           this.isClass = function(classname) {
-                   return (this._classes.indexOf(classname) != -1);
-           };
-
-           this.isDataFrame = function() {
-                   return (this._isDataFrame);
-           };
-
-           this.isMatrix = function() {
-                   return (this._isMatrix);
-           };
-
-           this.isList = function() {
-                   return (this._isList);
-           };
-
-           this.isFunction = function() {
-                   return (this._isFunction);
-           };
-
-           this.isEnvironment = function() {
-                   return (this._isEnvironment);
-           };
-
-           this.isDataNumeric = function() {
-                   return (this._datatype == "numeric");
-           };
-
-           this.isDataFactor = function() {
-                   return (this._datatype == "factor");
-           };
-
-           this.isDataCharacter = function() {
-                   return (this._datatype == "character");
-           };
-
-           this.isDataLogical = function() {
-                   return (this._datatype == "logical");
-           };
-
-           this.parent = function() {
-                   return (new RObject(_rkward.getObjectParent(this._name)));
-           };
-
-           this.child = function(childname) {
-                   return (new RObject(_rkward.getObjectChild(this._name, 
childname)));
-           }
-    };
+};
+
+function RCommand(command, id = null) {
+       return new Promise(function(resolve, reject) {
+               _rkward.doRCommand2(command, id, resolve, reject);
+       });
+};
+
+function RObject(objectname) {
+       this.objectname = objectname;
+
+       // for internal use
+       this.initialize = function() {
+               info = _rkward.getObjectInfo(this.objectname);
+
+               this._dimensions = info.shift();
+               this._classes = info.shift();
+               this._isDataFrame = info.shift();
+               this._isMatrix = info.shift();
+               this._isList = info.shift();
+               this._isFunction = info.shift();
+               this._isEnvironment = info.shift();
+               this._datatype = info.shift();
+       };
+
+       this.initialize();
+
+       this.getName = function() {
+               return (this.objectname);
+       };
+
+       this.exists = function() {
+               return (typeof (this._dimensions) != "undefined");
+       };
+
+       this.dimensions = function() {
+               return (this._dimensions);
+       };
+
+       this.classes = function() {
+               return (this._classes);
+       };
+
+       this.isClass = function(classname) {
+               return (this._classes.indexOf(classname) != -1);
+       };
+
+       this.isDataFrame = function() {
+               return (this._isDataFrame);
+       };
+
+       this.isMatrix = function() {
+               return (this._isMatrix);
+       };
+
+       this.isList = function() {
+               return (this._isList);
+       };
+
+       this.isFunction = function() {
+               return (this._isFunction);
+       };
+
+       this.isEnvironment = function() {
+               return (this._isEnvironment);
+       };
+
+       this.isDataNumeric = function() {
+               return (this._datatype == "numeric");
+       };
+
+       this.isDataFactor = function() {
+               return (this._datatype == "factor");
+       };
+
+       this.isDataCharacter = function() {
+               return (this._datatype == "character");
+       };
+
+       this.isDataLogical = function() {
+               return (this._datatype == "logical");
+       };
+
+       this.parent = function() {
+               return (new RObject(_rkward.getObjectParent(this._name)));
+       };
+
+       this.child = function(childname) {
+               return (new RObject(_rkward.getObjectChild(this._name, 
childname)));
+       }
+};
 
 makeRObject = function(objectname) {
        return (new RObject(objectname));

Reply via email to