jsonangularelectronipcrenderer

How is the ipcRenderer argument JSON object losing data when passed from an Angular 7 service?


I am having a strange issue with an Angular 7.1.1 and Electron 4.1.4 project.

Data Flow:

  1. Angular Component "Report Builder" collects report configuration options from a FormGroup and FormControl validated form and sends data to docx-templater.service
    • User Button triggers createReport() function
    • When submitting options for a complete report, the createReport() function calls dataService's fnGetCompleteControlList() which returns properly configured JSON asynchronously.
    • with a .then() function after the async data retrieval, the createReport() function combines the output directory which is part of the configuration form and sends both to the docx-templater.service's createCompleteDocument() function. Once the promise is returned it updates the UI.
  2. Angular Service "docx-templater"'s createCompleteDocument function passes the data and folder values to the ipcRenderer.send for the electron "writeCompleteDocument" channel and returns a promise.
  3. In my main.ts, I have an ipcMain.on for the "writeCompleteDocument" channel that passes the data to a write-docx function for processing that data into a word document.

Problem: When the data gets to my write-docx function it is missing a sub array of objects that are essential to the export process.

I have verified that the data is perfect in the Chrome Developer Tools console of electron at the moment just before it sends the data to the docx-templater.service and just before that service sends it to the ipcRenderer (meaning my data service and Report Builder functions are working as designed). When I check the data in the main.ts by saving the data off to a JSON file it is missing the controls sub array within the second object of the JSON only. The controls sub array shows up in the first object as expected.

I will note that what is coming out of the ipcMain function is a properly formed JSON file so it has really just excluded the "controls" sub array and is not truncating due to memory or buffer limits or anything like that.

report-builder.component.ts

createReport() {
    if (this.reportBuilderFG.get('allControls').value) {
      this.db.fnGetCompleteControlList()
        .then((groups: Group[]) => {
          this.word.createCompleteDocument(groups, this.reportBuilderFG.get('folder').value + '\\filename.docx')
          .then(() => {
            this.openSnackBar(this.reportBuilderFG.get('folder').value + '\\filename.docx created successfully');
          });
        });
    } else {
      // Do other stuff
    }
docx-templater.service.ts

createCompleteDocument(data, folder: string): Promise<boolean> {
    return new Promise(resolve => {
      console.log(data) <=== Data is perfect here.
      ipcRenderer.send('writeCompleteDocument', {data: data, folder: folder});
      resolve();
    });
  }

main.ts
import { writeCompleteDocument } from './node_scripts/write-docx';

ipcMain.on('writeCompleteDocument', (event, arg) => {
  fs.writeFileSync("IPCdata.json", arg.data); // <==== Part of the data is missing here.
  writeCompleteDocument(arg.data, arg.folder);
});

Good Data Example (some keys and objects excluded for brevity)
[
  {
    "name": "General Security",
    "order": 1,
    "subgroups": [
      {
        "_id": "GOV",
        "name": "Governance",
        "order": 1,
        "controls": [
          {
            "group": "GS",
            "subgroup": "GOV",
            "active": true,
            "printOrder": 1,
            "name": "This is my GS control name",
            "requirements": [
              {
                "id": "SA01",
                "active": true,
                "order": 1,
                "type": "SA",
                "applicability": [
                  "ABC",
                  "DEF",
                  "GHI"
                ],
              },
              { ... 3 more  }
            ],
            "_id": "GSRA-03",
            "_rev": "1-0cbdefc93e56683bc98bae3a122f9783"
          },
          { ... 3 more }
    ],
    "_id": "GS",
    "_rev": "1-b94d1651589eefd5ef0a52360dac6f9d"
  },
  {
    "order": 2,
    "name": "IT Security",
    "subgroups": [
      {
        "_id": "PLCY",
        "order": 1,
        "name": "Policies",
        "controls": [ <==== This entire sub array is missing when exporting from IPC Main
          {
            "group": "IT",
            "subgroup": "PLCY",
            "active": true,
            "printOrder": 1,
            "name": "This is my IT control name",
            "requirements": [
              {
                "id": "SA01",
                "active": true,
                "order": 1,
                "type": "SA",
                "applicability": [
                  "ABC",
                  "DEF",
                  "GHI"
                ],
              }
            ],
            "_id": "GSRA-03",
            "_rev": "1-0cbdefc93e56683bc98bae3a122f9783"
          }
      }
    ],
    "_id": "IT",
    "_rev": "2-e6ff53456e85b45d9bafd791652a945c"
  }
]

I would have expected the ipcRenderer to pass a JSON exactly as it is to the ipcMain.on function, but somehow it is trimming part of the data. I have even tried strigifying the data before sending it to the renderer and then parsing it on the other side but that did nothing.

Could this be an async thing? I am at a loss of where to go next to debug and find what idiot mistake I made in the process.

Also, I realize that the above data flow seems overly complex for what I am doing, and that I can probably do it easier, but it makes sense (kinda) for the way the whole application is structured so I am going to go with it if I can squash this bug.


Solution

  • Looks like your createCompleteDocument() function is set up incorrectly. A quick search showed me that ipcRenderer is an async function, but you are responding to it (almost) synchronously.

    You have the following, which is (probably) incorrect (actually it's definitely incorrect, because you've typed typed the return as Promise<boolean> when it is Promise<void>):

    createCompleteDocument(data, folder: string): Promise<boolean> {
      return new Promise(resolve => {
        ipcRenderer.send('writeCompleteDocument', {data: data, folder: folder});
        resolve();
      });
    }
    

    ipcRenderer#send() is async but you are calling resolve() immediately afterwards without waiting for the function to resolve. This probably explains why adding the setTimeout() is fixing the problem for you. Looking at the ipcRenderer docs, the following probably does what you want:

    createCompleteDocument(data, folder: string): Promise<Event> {
      return new Promise(resolve => {
        ipcRenderer.once('writeCompleteDocument', resolve);
        ipcRenderer.send('writeCompleteDocument', {data: data, folder: folder});
      });
    }
    

    Looks like the callback is passed an Event object.

    Another option would be to simply replace ipcRenderer#send() with ipcRenderer#sendSync() in your original code, but as pointed out in that method's documentation :

    Sending a synchronous message will block the whole renderer process, unless you know what you are doing you should never use it.

    Making use of ipcRenderer#send() and ipcRenderer#once() is almost definitely the way to go.

    Seperately, you can clean up the code by switching to async/await functions. For example:

    async createReport(): Promise<void> {
      if (this.reportBuilderFG.get('allControls').value) {
        const groups: Group[] = await this.db.fnGetCompleteControlList();
    
        await this.word.createCompleteDocument(
          groups,
          this.reportBuilderFG.get('folder').value + '\\filename.docx'
        );
    
        // Unclear if this function is actually async 
        await this.openSnackBar(
          this.reportBuilderFG.get('folder').value +
            '\\filename.docx created successfully'
        );
      } else {
        // Do other stuff
      }
    }