« 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: 5eff243a64707b6389902e1426eae6509c44d3b9 (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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// 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 {relative, resolve} from 'node:path';

import {logWarn} from '#cli';
import {transposeArrays} from '#sugar';

export default class FileSizePreloader {
  #paths = [];
  #sizes = [];
  #loadedPathIndex = -1;

  #loadingPromise = null;
  #resolveLoadingPromise = null;

  hadErrored = false;

  constructor({prefix = ''} = {}) {
    this.prefix = prefix;
  }

  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;
    }

    ({promise: this.#loadingPromise,
      resolve: this.#resolveLoadingPromise} =
        Promise.withResolvers());

    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) {
    let size = this.#getSizeOfPath(path);
    if (size || !this.prefix) return size;
    const path2 = resolve(this.prefix, path);
    if (path2 === path) return null;
    return this.#getSizeOfPath(path2);
  }

  #getSizeOfPath(path) {
    const index = this.#paths.indexOf(path);
    if (index === -1) return null;
    if (index > this.#loadedPathIndex) return null;
    return this.#sizes[index];
  }

  saveAsCache() {
    const entries =
      transposeArrays([
        this.#paths.slice(0, this.#loadedPathIndex)
          .map(path => relative(this.prefix, path)),

        this.#sizes.slice(0, this.#loadedPathIndex),
      ]);

    return Object.fromEntries(entries);
  }

  loadFromCache(cache) {
    const entries =
      Object.entries(cache)
        .filter(([p]) => !this.#paths.includes(p));

    let [newPaths, newSizes] = transposeArrays(entries);
    newPaths = newPaths.map(p => resolve(this.prefix, p));

    this.#paths.splice(this.#loadedPathIndex + 1, 0, ...newPaths);
    this.#sizes.splice(this.#loadedPathIndex + 1, 0, ...newSizes);
    this.#loadedPathIndex += entries.length;
  }
}