Bug 784717 - Asynchronous directory walk controller code. r=froydnj
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Wed, 26 Sep 2012 21:37:23 -0400
changeset 108340 90b62202f9e380005f190d9cff1c6e7d8127d45e
parent 108339 44c79a5f7131afd53808430e3d4ccd77bbe09b7e
child 108341 8332dd98c1cb2019a1b16b858baede06ec134e93
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersfroydnj
bugs784717
milestone18.0a1
Bug 784717 - Asynchronous directory walk controller code. r=froydnj
toolkit/components/osfile/osfile_async_front.jsm
--- a/toolkit/components/osfile/osfile_async_front.jsm
+++ b/toolkit/components/osfile/osfile_async_front.jsm
@@ -577,16 +577,183 @@ File.makeDir = function makeDir(path, op
  */
 File.Info = function Info(value) {
   return value;
 };
 File.Info.fromMsg = function fromMsg(value) {
   return new File.Info(value);
 };
 
+/**
+ * Iterate asynchronously through a directory
+ *
+ * @constructor
+ */
+let DirectoryIterator = function DirectoryIterator(path, options) {
+  /**
+   * Open the iterator on the worker thread
+   *
+   * @type {Promise}
+   * @resolves {*} A message accepted by the methods of DirectoryIterator
+   * in the worker thread
+   * @rejects {StopIteration} If all entries have already been visited
+   * or the iterator has been closed.
+   */
+  this._itmsg = Scheduler.post(
+    "new_DirectoryIterator", [Type.path.toMsg(path), options],
+    path
+  );
+  this._isClosed = false;
+};
+DirectoryIterator.prototype = {
+  /**
+   * Get the next entry in the directory.
+   *
+   * @return {Promise}
+   * @resolves {OS.File.Entry}
+   * @rejects {StopIteration} If all entries have already been visited.
+   */
+  next: function next() {
+    let self = this;
+    let promise = this._itmsg;
+
+    // Get the iterator, call _next
+    promise = promise.then(
+      function withIterator(iterator) {
+        return self._next(iterator);
+      });
+
+    return promise;
+  },
+  /**
+   * Get several entries at once.
+   *
+   * @param {number=} length If specified, the number of entries
+   * to return. If unspecified, return all remaining entries.
+   * @return {Promise}
+   * @resolves {Array} An array containing the |length| next entries.
+   */
+  nextBatch: function nextBatch(size) {
+    if (this._isClosed) {
+      return Promise.resolve([]);
+    }
+    let promise = this._itmsg;
+    promise = promise.then(
+      function withIterator(iterator) {
+        return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]);
+      });
+    promise = promise.then(
+      function withEntries(array) {
+        return array.map(DirectoryIterator.Entry.fromMsg);
+      });
+    return promise;
+  },
+  /**
+   * Apply a function to all elements of the directory sequentially.
+   *
+   * @param {Function} cb This function will be applied to all entries
+   * of the directory. It receives as arguments
+   *  - the OS.File.Entry corresponding to the entry;
+   *  - the index of the entry in the enumeration;
+   *  - the iterator itself - return |iterator.close()| to stop the loop.
+   *
+   * If the callback returns a promise, iteration waits until the
+   * promise is resolved before proceeding.
+   *
+   * @return {Promise} A promise resolved once the loop has reached
+   * its end.
+   */
+  forEach: function forEach(cb, options) {
+    if (this._isClosed) {
+      return Promise.resolve();
+    }
+
+    let self = this;
+    let position = 0;
+    let iterator;
+
+    // Grab iterator
+    let promise = this._itmsg.then(
+      function(aIterator) {
+        iterator = aIterator;
+      }
+    );
+
+    // Then iterate
+    let loop = function loop() {
+      if (self._isClosed) {
+        return Promise.resolve();
+      }
+      return self._next(iterator).then(
+        function onSuccess(value) {
+          return Promise.resolve(cb(value, position++, self)).then(loop);
+        },
+        function onFailure(reason) {
+          if (reason == StopIteration) {
+            return;
+          }
+          throw reason;
+        }
+      );
+    };
+
+    return promise.then(loop);
+  },
+  /**
+   * Auxiliary method: fetch the next item
+   *
+   * @rejects {StopIteration} If all entries have already been visited
+   * or the iterator has been closed.
+   */
+  _next: function _next(iterator) {
+    if (this._isClosed) {
+      LOG("DirectoryIterator._next", "closed");
+      return this._itmsg;
+    }
+    let self = this;
+    let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
+    promise = promise.then(
+      DirectoryIterator.Entry.fromMsg,
+      function onReject(reason) {
+        // If the exception is |StopIteration| (which we may determine only
+        // from its message...) we need to stop the iteration.
+        if (!(reason instanceof WorkerErrorEvent && reason.message == "uncaught exception: [object StopIteration]")) {
+          // Any exception other than StopIteration should be propagated as such
+          throw reason;
+        }
+        self.close();
+        throw StopIteration;
+      });
+    return promise;
+  },
+  /**
+   * Close the iterator
+   */
+  close: function close() {
+    if (this._isClosed) {
+      return;
+    }
+    this._isClosed = true;
+    let self = this;
+    this._itmsg.then(
+      function withIterator(iterator) {
+        self._itmsg = Promise.reject(StopIteration);
+        return Scheduler.post("DirectoryIterator_prototype_close", [iterator]);
+      }
+    );
+  }
+};
+
+DirectoryIterator.Entry = function Entry(value) {
+  return value;
+};
+DirectoryIterator.Entry.fromMsg = function fromMsg(value) {
+  return new DirectoryIterator.Entry(value);
+};
+
 // Constants
 Object.defineProperty(File, "POS_START", {value: OS.Shared.POS_START});
 Object.defineProperty(File, "POS_CURRENT", {value: OS.Shared.POS_CURRENT});
 Object.defineProperty(File, "POS_END", {value: OS.Shared.POS_END});
 
 OS.File = File;
 OS.File.Error = OSError;
-
+OS.File.DirectoryIterator = DirectoryIterator;