« get me outta code hell

file-size-preloader.js « src - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/file-size-preloader.js
blob: 4eadde7bf0a9ae05da2f4acd2a2fac1799889bcd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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];
  }
}