[education/rkward] /: Add promise-based mechanism for running R commands from plugins
Thomas Friedrichsmeier
null at kde.org
Wed May 7 14:21:36 BST 2025
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>
<script><![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));
+ });
}
]]></script>
</programlisting>
<para>Here, <parameter>variable</parameter> is a property holding an object name (⪚ inside a <command><varslot></command>). Whenever that changes, you will want to update the display
- of levels inside the <command><valueselector></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><valueselector></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, ⪚ 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><valueselector></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><valueselector></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, ⪚ 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));
More information about the kde-doc-english
mailing list