Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clean up after generating function or async generating function

How do I clean up after a generating function has been exited. The problem I am having is I made a little utility to read-lines from a file using Async Iterators a stage 3 EcmaScript proposal and I want it to close the file it's reading after I exit a for-of loop. Currently since this feature is only stage 3, in order to get this to run you'll have to use babel to transpile it.

With the below code you can see the problem. If you pipe in an input file then it will read one line and print that the line reader is open still.

I would like to explicitly close the file inside the LineReader class when it's iterator is returned.

I know I can do this by not using a generating function but instead return an iterator object as outlined here, but is there any way I can keep the generating function and define a return method for it.

src/line-reader.js

function deferred() {
  const def = {}
  def.promise = new Promise((resolve, reject) => {
    def.resolve = resolve
    def.reject = reject
  })
  return def
}

/**
 * PromiseQueue from GTOR adapted for ES2015
 * https://github.com/kriskowal/gtor
 */
class PromiseQueue {
  constructor (values) {
    this.ends = deferred();
    if (values) {
      values.forEach(this.put, this);
    }
  }

  put(value) {
    const next = deferred();
    this.ends.resolve({
      head: value,
      tail: next.promise
    });
    this.ends.resolve = next.resolve;
  }

  get () {
    var result = this.ends.promise.then(node=>node.head);
    this.ends.promise = this.ends.promise.then(node=>node.tail)
    return result;
  };
}

class LineReader {
  constructor (input, output) {
    this.lineReader = require('readline').createInterface({ input, output });
    this.lineQueue = new PromiseQueue();
    this.isClosed = false;
    this.lineReader.on('line', (line) => {
      this.lineQueue.put(line);
      this.lineReader.pause();
    });
    this.lineReader.on('close', (line) => {
      this.isClosed = true;
      this.lineQueue.put(Promise.resolve({done: true}));
    });
    this.lineReader.on('SIGCONT', () => {
      // `prompt` will automatically resume the stream
      this.lineReader.prompt();
    });
  }

  readLine(){
    var result = this.lineQueue.get().then(function (data) {
      if(data && data.done) {
        throw new Error("EOF");
      }
      return data
    });
    this.lineReader.resume();
    return result;
  }

  close () {
    this.lineReader.close();
  }

  async * [Symbol.asyncIterator] () {
    try {
      while(!this.isClosed) {
        yield await this.readLine();
      }
    } catch (e) {
      this.close();
    }
  }
}

module.exports = LineReader;

test/test.js

var LineReader = require("../src/line-reader");

var lineReader = new LineReader(process.stdin);

(async ()=> {
  var i = 1;
  for await (var line of lineReader) {
    console.log(`${i++} ${line}`);
    break;
  }
  console.log(`The line-reader is ${lineReader.isClosed ? "closed" : "open" }.`);
  lineReader.close();
})().catch(e=> {
  console.error(e)
})
like image 398
John Avatar asked Oct 19 '25 18:10

John


1 Answers

Just add a finally to your try block. finally will execute even if the function has returned (which it returns when someone breaks out of a for of loop). This guarantees that you function will clean up and you don't have to modify your function much. I just found this out thanks to this article by Jake Archibald.

class LineReader {
  // ... elided code ...

  async * [Symbol.asyncIterator] () {
    try {
      while(!this.isClosed) {
        yield await this.readLine();
      }
    } finally {
      this.close();
    }
  }
}
like image 91
John Avatar answered Oct 21 '25 07:10

John