5

I am trying to write multiple csv files from a set of data that I have loaded using the sheet js library. My first attempt was like:

    for (let i = 0; i < dataSetDivided.length; i++) {
      let exportSet = dataSetDivided[i]
      console.log(exportSet)
      let ws = XLSX.utils.json_to_sheet(exportSet, {header: finalHeaders})
        let wb = XLSX.utils.book_new()
        XLSX.utils.book_append_sheet(wb, ws, "SheetJS")

      let todayDate = this.returnFormattedDate()

      let originalFileName = this.state.fileName
      let exportFileName = 'import_' + originalFileName + '_' + todayDate + '(part_' + (i + 1) + ').csv'

      XLSX.writeFile(wb, exportFileName)
}

With this code only some files are written I guess because the for loop doesn't wait for the file to be written before continuing.

So I am trying to write each file within a promise like below:

Promise.all(
  dataSetDivided.map((exportSet, i) => {
    return new Promise((resolve, reject) => {

      console.log(exportSet)
      let ws = XLSX.utils.json_to_sheet(exportSet, {header: finalHeaders})
      let wb = XLSX.utils.book_new()
      XLSX.utils.book_append_sheet(wb, ws, "SheetJS")

      let todayDate = this.returnFormattedDate()

      let originalFileName = this.state.fileName
      let exportFileName = 'import_' + originalFileName + '_' + todayDate + '(part_' + (i + 1) + ').csv'

      XLSX.writeFile(wb, exportFileName, (err) => {
        if (err) {
          console.log(err)
          reject(err)
        } else {
          console.log('Created ' + exportFileName)
          resolve()
        }
      })
    })
  })
)
.then(() => {
  console.log('Created multiple files successfully')
})
.catch((err) => {
  console.log('ERROR: ' + err)
})

But... this isn't working, again only some files are written and nothing is logged to the console. Can anyone give me any ideas how to make this work or a better way to achieve the goal of writing multiple files like this? There is a XLSX.writeFileAsync method but I can't find any examples of how it works and I'm not sure if that is what I need.

With thanks,

James

UPDATE:

I am now using setTimeout to delay the next writeFile call... this is working for my test cases but I am aware it isn't a good solution, would be much better to have a callback when the file is successfully written:

    writeFileToDisk(dataSetDivided, i) {

    if (dataSetDivided.length > 0) {

      let exportSet = dataSetDivided[0]
      let ws = XLSX.utils.json_to_sheet(exportSet, {header: finalHeaders})
        let wb = XLSX.utils.book_new()
        XLSX.utils.book_append_sheet(wb, ws, "SheetJS")

      let todayDate = this.returnFormattedDate()

      let originalFileName = this.state.fileName
      let exportFileName = 'import_' + originalFileName + '_' + todayDate + '(part_' + i + ').csv'

      XLSX.writeFile(wb, exportFileName)

      dataSetDivided.splice(0, 1)

      i += 1

      setTimeout(() => {this.writeFileToDisk(dataSetDivided, i)}, 2500)
    }
  }

  this.writeFileToDisk(dataSetDivided, 1)

Any suggestions how to get this working without simulating the file write time would be much appreciated.

Jimme
  • 131
  • 1
  • 2
  • 6
  • I think you've misdiagnosed the problem with the first loop. Doc says that the write function is synchronous, so wrapping it in a promise will just confuse the matter. You should investigate why the first loop is writing *some, but not all* files. (what's different about those 'some' cases?, maybe log `exportFileName`?). Having figured that out, definitely, definitely use the `XLSX.writeFileAsync`. I can give you advice on how to wrap that with a promise, but first walk, then fly. – danh Apr 12 '18 at 14:56
  • The fact that some writes happen successfully means you're close. Now the task is just nose-to-the-grindstone debugging. – danh Apr 12 '18 at 14:57
  • Thanks for your reply. Is it not the case that the for loop will run and get to XLSX.writeFile(wb, exportFileName) and the command is sent for this file to be written. Then the loop continues and potentially before the last file is written the second call to XLSX.writeFile(wb, exportFileName) is made.. and this is causing the file writing to sometimes complete and sometimes not? So I need to have a callback that tells me when the file is written and then move on to the next one? – Jimme Apr 12 '18 at 15:26
  • I think the write is happening synchronously in your first loop. See if it returns something and log that. Also please log the filenames. Can you create a small version of the input, maybe one with just two or three exportSets that each have just two or three objects (rows)? Please post log results from that. I'm pretty confident you're not (yet) confronting any synch/async issue. (You'll need to get to that next). (Oh, also, can you pull react out of the mix? Lets just see the data get handled without any UI complications) – danh Apr 12 '18 at 15:39
  • The filenames all console.log correctly in the loop, like: example_part_1, example_part_2 etc... The files that get written often have example_part_1 as a greyed out file name in the downloads folder, but this only happens sometimes. And there are usually a couple of files missing, it is quite random which file it is that is missing, it could be example_part_3 or example_part_4. I might have misunderstood what you are asking me to console.log... how do I see if the write returns something? – Jimme Apr 12 '18 at 16:09
  • Something like `let result = XLSX.writeFile(wb, exportFileName); console.log(result);` or `console.log(JSON.stringify(result))` – danh Apr 12 '18 at 16:17
  • I see, yes it console logs:`blob:http://localhost:3000/751c886c-a10b-4aed-a93a-07e98ea543a1; blob:http://localhost:3000/18be0f1d-6e4b-49e8-9d81-5b67066a7a28; blob:http://localhost:3000/97236f28-9c24-48af-86c9-8afb92387473; blob:http://localhost:3000/27bb0718-e92b-4c98-8e7a-2ef818cc884f; blob:http://localhost:3000/a9cefe74-8bef-491d-a3f4-f0c8f7419b0a` – Jimme Apr 12 '18 at 16:21
  • So 5 blobs, but in this example only three got written as files – Jimme Apr 12 '18 at 16:23
  • And if you repeat that identical experiment, do the same three get written, do the same two not get written? – danh Apr 12 '18 at 16:29
  • I have repeated the test five times with the same data and settings, the files that get written each time are slightly different, here are the parts that get written in these five tests: Test 1: 1, 3, 5. Test 2: 1, 4, 5. Test 3: 2, 3, 5. Test 4: 1, 2, 3, 5. Test 5: 1, 2, 5. So part 5 (the last file) always gets written, but the rest are quite random. – Jimme Apr 13 '18 at 07:54

3 Answers3

7

I just tried this (first time) XLSX code and can confirm that it writes the expected workbooks and runs synchronously...

'use strict'

const XLSX = require('xlsx');

let finalHeaders = ['colA', 'colB', 'colC'];
let data = [
    [ { colA: 1, colB: 2, colC: 3 }, { colA: 4, colB: 5, colC: 6 }, { colA: 7, colB: 8, colC: 9 } ],
    [ { colA:11, colB:12, colC:13 }, { colA:14, colB:15, colC:16 }, { colA:17, colB:18, colC:19 } ],
    [ { colA:21, colB:22, colC:23 }, { colA:24, colB:25, colC:26 }, { colA:27, colB:28, colC:29 } ]
];

data.forEach((array, i) => {
    let ws = XLSX.utils.json_to_sheet(array, {header: finalHeaders});
    let wb = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(wb, ws, "SheetJS")
    let exportFileName = `workbook_${i}.xls`;
    XLSX.writeFile(wb, exportFileName)
});

Running this yields workbook_0.xls, workbook_1.xls, and workbook_2.xls, each with a single sheet entitled "SheetJS". They all look good in excel, for example, workbook_0 has...

enter image description here

I think you should do the writing asynchronously, and would suggest the following adaptation of the above ...

function writeFileQ(workbook, filename) {
    return new Promise((resolve, reject) => {
        // the interface wasn't clearly documented, but this reasonable guess worked...
        XLSX.writeFileAsync(filename, workbook, (error, result) => {
            (error)? reject(error) : resolve(result);
        })
    })
}


let promises = data.map((array, i) => {
    let ws = XLSX.utils.json_to_sheet(array, {header: finalHeaders});
    let wb = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(wb, ws, "SheetJS")
    let exportFileName = `workbook_${i}.xls`;
    return writeFileQ(wb, exportFileName)
});

Promise.all(promises).then(result => console.log(result)).catch(error => console.log(error));

Running this async code, I found that it produced the same expected results and did so asynchronously.

So your original loop looks right, and should work synchronously. The fact that you aren't getting expected results must be caused by something apart from timing (or maybe some timing issue induced by react?).

In any event, if you do want to use the async approach, which I highly recommend, I've shown how to do that (but I worry that might not fully solve the problem unless you sort out what's happening with your first attempt).

danh
  • 55,236
  • 10
  • 89
  • 124
  • 2
    Thanks a lot for this Danh. I have tried to implement the solution using promises... but get the TypeError: _fs.writeFile is not a function. I think that you were correct in an earlier comment that my setup using Node and React might be creating such problems. – Jimme Apr 16 '18 at 07:44
0

XLSX.writeFileAsync does have a callback with the following syntax.

xlsx.writeFileAsync(workbookName, workbook, (err) => {
   // It's a callback
});

But this will handle only writing one file asynchronously.

Your case is typical, if you want to do a series things in which each item is asynchronous, then you should not just use iterative methods like loops/map/forEach.

One best library I would suggest for this is 'async'. 'async.parallel' function which takes a array of functions which execute asynchronously and calls callback after all of them finished.

https://caolan.github.io/async/docs.html#parallel

Parameshwar Ande
  • 727
  • 7
  • 15
  • _'async.parallel' function which takes a array of functions which execute asynchronously and calls callback after all of them finished._ ...or just do `Promise.all([asyncFunctionA(), asyncFunctionB()]).then(...)` – Alexey Grinko Jul 19 '19 at 10:47
0

If the concern is to use the library asynchronously for not blocking the server, you should know that this library implementation seems to be synchronous and you should check out the library's server demos README since it has several proposals to workaround this problem: https://github.com/SheetJS/sheetjs/tree/master/demos/server

cancerbero
  • 5,662
  • 1
  • 26
  • 18