0

I wrote a Google Apps Script for a document to repeatedly set the text of a paragraph and then retrieve it. If I do this within a script file, everything works fine (see the function 'test' below):

Code.gs:

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function test() {
  DocumentApp.getActiveDocument().getBody().insertParagraph(0, "");
  for(var i = 0; i < 10; i++) {
    Logger.log("Inserting " + i);
    setText(i);
    Logger.log("Received " + getText());
  }
}

function setText(text) {
  var paragraph = DocumentApp.getActiveDocument().getBody().getChild(0).asParagraph();
  paragraph.clear();
  paragraph.setText(text);
}

function getText() {
  var paragraph = DocumentApp.getActiveDocument().getBody().getChild(0).asParagraph();
  return paragraph.getText();
}

function onOpen() {
  test();
  DocumentApp.getUi()
      .createMenu('Test')
      .addItem('Open', 'openTest')
      .addToUi();
}

function openTest() {
  var html = HtmlService
      .createTemplateFromFile('index')
      .evaluate()
      .setSandboxMode(HtmlService.SandboxMode.IFRAME);
  DocumentApp.getUi().showSidebar(html);
}

i.e. the test function behaves as expected:

[16-03-02 06:06:58:652 PST] Inserting 0
[16-03-02 06:06:58:657 PST] Received 0
[16-03-02 06:06:58:658 PST] Inserting 1
[16-03-02 06:06:58:663 PST] Received 1
[16-03-02 06:06:58:664 PST] Inserting 2
[16-03-02 06:06:58:670 PST] Received 2
[16-03-02 06:06:58:670 PST] Inserting 3
[16-03-02 06:06:58:676 PST] Received 3
[16-03-02 06:06:58:677 PST] Inserting 4
[16-03-02 06:06:58:683 PST] Received 4
[16-03-02 06:06:58:684 PST] Inserting 5
[16-03-02 06:06:58:690 PST] Received 5
[16-03-02 06:06:58:690 PST] Inserting 6
[16-03-02 06:06:58:696 PST] Received 6
[16-03-02 06:06:58:697 PST] Inserting 7
[16-03-02 06:06:58:703 PST] Received 7
[16-03-02 06:06:58:703 PST] Inserting 8
[16-03-02 06:06:58:708 PST] Received 8
[16-03-02 06:06:58:709 PST] Inserting 9
[16-03-02 06:06:58:715 PST] Received 9

However, if I do the same thing but from an HTML page:

JavaScript:

<script>

var i = 0;

$(document).ready(insert);

function log(msg) {
   $('#log').append('<div></div>').append(document.createTextNode(msg));
}

function insert() {
    log("Inserting " + i);
    google.script.run.withSuccessHandler(receive).setText(i);
}

function receive() {
    google.script.run.withSuccessHandler(function(text) {
        log("Received " + text);
        i++;
        if(i < 10)
            insert();
    }).getText();
}

</script>

I get results like

Inserting 0
Received
Inserting 1
Received 1
Inserting 2
Received 2
Inserting 3
Received 3
Inserting 4
Received 3
Inserting 5
Received 5
Inserting 6
Received 6
Inserting 7
Received 7
Inserting 8
Received 8
Inserting 9
Received 9 

Some are in order but some get swapped. Why is this happening? Shouldn't my changes to the document be in effect when my withSuccess callback is called? Is there a different function I can pass a callback to?

Mogsdad
  • 44,709
  • 21
  • 151
  • 275

1 Answers1

0

You're seeing the results of a race condition caused by asynchronous execution, as the JavaScript code is not doing the same thing as your Google Apps Script test function, despite your effort to ensure each setText() and getText() function call is synchronous. (For anyone not familiar with these concepts, there's a great general explanation here: Asynchronous vs synchronous execution, what does it really mean?)

The thing is, there is another level of asynchronicity involved here, as each "user" of the Google Document you're accessing gets their own copy, which is (conceptually, at least) synchronized with a master copy.

The test() function runs in the context of a single Google Apps Script execution instance. That instance gets associated with one copy of the document, and all subsequent reads and writes are to that copy. The net effect is that your script sees the changes it makes immediately after they are made.

Contrast this with the behaviour of the JavaScript running in the client browser. There, each google.script.run call creates a new, unique Google Apps Script execution instance on Google's servers, with its own unique copy of the Google Document. When setText() gets invoked, it modifies its copy of the document, which then synchronizes with the master document. When getText() runs, it gets a copy of the current master document, and retrieves content from that. If the synchronization of the previous setText() change has completed, you get the output you expect. If that synchronization isn't complete - well, you get what you get.

What can you do about that? With minimal code change, you could just delay getText() calls from the JavaScript client, for as long as you think necessary to guarantee success. The caveat there is that a delay that is sufficient today may not be tomorrow, so it's unreliable.

function receive() {
    setTimeout(
        google.script.run.withSuccessHandler(function(text) {
            log("Received " + text);
            i++;
            if(i < 10)
                insert();
        }).getText(),
        2000         // 2 second delay before calling getText()
    );
}

To guarantee synchronicity, you would need to devise a more complex system, for example some "server" process that remains running always, with a single copy of the document to manipulate.

Community
  • 1
  • 1
Mogsdad
  • 44,709
  • 21
  • 151
  • 275