Bin
2025-12-17 262fecaa75b2909ad244f12c3b079ed3ff4ae329
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
type OnProgressCallback = (total: number, loaded: number, progress: number) => void;
 
/**
 * @class FileLoader
 * @description Allows to download any file from a given URL and provide a data URL for it
 */
export class FileLoader {
  private fileCache: Map<string, string> = new Map();
  private errorCache: Map<string, Error> = new Map();
 
  /**
   * @method download
   * @description Downloads a file from a given URL and returns a data URL for it
   * @description Progress event available to track download progress
   */
  download(url: string, onProgress?: OnProgressCallback) {
    if (!url) throw new Error("No URL provided for download");
 
    return new Promise((resolve, reject) => {
      if (this.fileCache.has(url)) {
        resolve(this.fileCache.get(url));
        return;
      }
      if (this.errorCache.has(url)) {
        reject(this.errorCache.get(url));
        return;
      }
 
      const xhr = new XMLHttpRequest();
 
      xhr.responseType = "blob";
 
      xhr.addEventListener("load", async () => {
        if (xhr.readyState === 4 && xhr.status === 200) {
          const localURL = this.createDataURL(xhr.response);
 
          this.fileCache.set(url, localURL);
 
          // in case we're dealing with an image, let's cache it using default browser mechanisms
          // this will allow instant rendering in the future
          if (xhr.getResponseHeader("content-type")?.match(/image/)) {
            try {
              await this.cacheImage(localURL);
            } catch (err) {
              reject(err);
              return;
            }
          }
 
          resolve(localURL);
        }
      });
 
      xhr.addEventListener("progress", (e) => {
        const { total, loaded } = e;
        const progress = loaded / total;
 
        onProgress?.(total, loaded, progress);
      });
 
      xhr.addEventListener("error", () => {
        const error = new Error("Network error");
 
        reject(error);
 
        this.errorCache.set(url, error);
      });
 
      xhr.open("GET", url);
      xhr.send();
    });
  }
 
  isPreloaded(url: string) {
    return this.fileCache.has(url);
  }
 
  isError(url: string) {
    return this.errorCache.has(url);
  }
 
  getPreloadedURL(url: string) {
    return this.fileCache.get(url);
  }
 
  getError(url: string) {
    return this.errorCache.get(url);
  }
 
  private createDataURL(response: any) {
    const dataURL = URL.createObjectURL(response);
 
    return dataURL;
  }
 
  private cacheImage(url: string) {
    return new Promise<void>((resolve, reject) => {
      const image = new Image();
 
      image.onload = () => {
        resolve();
      };
 
      image.onerror = () => {
        reject();
      };
 
      image.src = url;
    });
  }
}