« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/file-size-preloader.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/file-size-preloader.js')
-rw-r--r--src/file-size-preloader.js104
1 files changed, 104 insertions, 0 deletions
diff --git a/src/file-size-preloader.js b/src/file-size-preloader.js
new file mode 100644
index 0000000..4eadde7
--- /dev/null
+++ b/src/file-size-preloader.js
@@ -0,0 +1,104 @@
+// Very simple, bare-bones file size loader which takes a bunch of file
+// paths, gets their filesizes, and resolves a promise when it's done.
+//
+// Once the size of a path has been loaded, it's available synchronously -
+// so this may be provided to code areas which don't support async code!
+//
+// This class also supports loading more paths after the initial batch is
+// done (it uses a queue system) - but make sure you pause any sync code
+// depending on the results until it's finished. waitUntilDoneLoading will
+// always hold until the queue is completely emptied, including waiting for
+// any entries to finish which were added after the wait function itself was
+// called. (Same if you decide to await loadPaths. Sorry that function won't
+// resolve as soon as just the paths it provided are finished - that's not
+// really a worthwhile feature to support for its complexity here, since
+// basically all this should process almost instantaneously anyway!)
+//
+// This only processes files one at a time because I'm lazy and stat calls
+// are very, very fast.
+
+import {stat} from 'node:fs/promises';
+
+import {logWarn} from '#cli';
+
+export default class FileSizePreloader {
+  #paths = [];
+  #sizes = [];
+  #loadedPathIndex = -1;
+
+  #loadingPromise = null;
+  #resolveLoadingPromise = null;
+
+  hadErrored = false;
+
+  loadPaths(...paths) {
+    this.#paths.push(...paths.filter((p) => !this.#paths.includes(p)));
+    return this.#startLoadingPaths();
+  }
+
+  waitUntilDoneLoading() {
+    return this.#loadingPromise ?? Promise.resolve();
+  }
+
+  #startLoadingPaths() {
+    if (this.#loadingPromise) {
+      return this.#loadingPromise;
+    }
+
+    this.#loadingPromise = new Promise((resolve) => {
+      this.#resolveLoadingPromise = resolve;
+    });
+
+    this.#loadNextPath();
+
+    return this.#loadingPromise;
+  }
+
+  async #loadNextPath() {
+    if (this.#loadedPathIndex === this.#paths.length - 1) {
+      return this.#doneLoadingPaths();
+    }
+
+    let size;
+
+    const path = this.#paths[this.#loadedPathIndex + 1];
+
+    try {
+      size = await this.readFileSize(path);
+    } catch (error) {
+      // Oops! Discard that path, and don't increment the index before
+      // moving on, since the next path will now be in its place.
+      this.#paths.splice(this.#loadedPathIndex + 1, 1);
+      this.hasErrored = true;
+      logWarn`Failed to process file size for ${path}: ${error.message}`;
+      return this.#loadNextPath();
+    }
+
+    this.#sizes.push(size);
+    this.#loadedPathIndex++;
+    return this.#loadNextPath();
+  }
+
+  #doneLoadingPaths() {
+    this.#resolveLoadingPromise();
+    this.#loadingPromise = null;
+    this.#resolveLoadingPromise = null;
+  }
+
+  // Override me if you want?
+  // The rest of the code here is literally just a queue system, so you could
+  // pretty much repurpose it for anything... but there are probably cleaner
+  // ways than making an instance or subclass of this and overriding this one
+  // method!
+  async readFileSize(path) {
+    const stats = await stat(path);
+    return stats.size;
+  }
+
+  getSizeOfPath(path) {
+    const index = this.#paths.indexOf(path);
+    if (index === -1) return null;
+    if (index > this.#loadedPathIndex) return null;
+    return this.#sizes[index];
+  }
+}