 8f32242787
			
		
	
	8f32242787
	
	
	
		
			
			* Prep readme for Typedoc. Clean up metadata. * Add license. * Ignore generated readme. * Flesh out TypeDoc. * Flesh out copy, usage. * web: Update package-lock.
		
			
				
	
	
		
			222 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			222 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /// <reference types="./types.js" />
 | |
| 
 | |
| /**
 | |
|  * @file Client-side observer for ESBuild events.
 | |
|  *
 | |
|  * @import { Message as ESBuildMessage } from "esbuild";
 | |
|  */
 | |
| 
 | |
| const logPrefix = "authentik/dev/web: ";
 | |
| const log = console.debug.bind(console, logPrefix);
 | |
| 
 | |
| /**
 | |
|  * @template {unknown} [Data=unknown]
 | |
|  * @typedef {(event: MessageEvent) => void} BuildEventListener
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * A client-side watcher for ESBuild.
 | |
|  *
 | |
|  * Note that this should be conditionally imported in your code, so that
 | |
|  * ESBuild may tree-shake it out of production builds.
 | |
|  *
 | |
|  * ```ts
 | |
|  * if (process.env.NODE_ENV === "development") {
 | |
|  *   await import("@goauthentik/esbuild-plugin-live-reload/client")
 | |
|  *     .catch(() => console.warn("Failed to import watcher"))
 | |
|  * }
 | |
|  * ```
 | |
|  *
 | |
|  * @implements {Disposable}
 | |
|  * @category Plugin
 | |
|  * runtime browser
 | |
|  */
 | |
| export class ESBuildObserver extends EventSource {
 | |
|     /**
 | |
|      * Whether the watcher has a recent connection to the server.
 | |
|      */
 | |
|     alive = true;
 | |
| 
 | |
|     /**
 | |
|      * The number of errors that have occurred since the watcher started.
 | |
|      */
 | |
|     errorCount = 0;
 | |
| 
 | |
|     /**
 | |
|      * Whether a reload has been requested while offline.
 | |
|      */
 | |
|     deferredReload = false;
 | |
| 
 | |
|     /**
 | |
|      * The last time a message was received from the server.
 | |
|      */
 | |
|     lastUpdatedAt = Date.now();
 | |
| 
 | |
|     /**
 | |
|      * Whether the browser considers itself online.
 | |
|      */
 | |
|     online = true;
 | |
| 
 | |
|     /**
 | |
|      * The ID of the animation frame for the reload.
 | |
|      */
 | |
|     #reloadFrameID = -1;
 | |
| 
 | |
|     /**
 | |
|      * The interval for the keep-alive check.
 | |
|      * @type {ReturnType<typeof setInterval> | undefined}
 | |
|      */
 | |
|     #keepAliveInterval;
 | |
| 
 | |
|     #trackActivity = () => {
 | |
|         this.lastUpdatedAt = Date.now();
 | |
|         this.alive = true;
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * @type {BuildEventListener}
 | |
|      */
 | |
|     #startListener = () => {
 | |
|         this.#trackActivity();
 | |
|         log("⏰ Build started...");
 | |
|     };
 | |
| 
 | |
|     #internalErrorListener = () => {
 | |
|         this.errorCount += 1;
 | |
| 
 | |
|         if (this.errorCount > 100) {
 | |
|             clearTimeout(this.#keepAliveInterval);
 | |
| 
 | |
|             this.close();
 | |
|             log("⛔️ Closing connection");
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * @type {BuildEventListener<string>}
 | |
|      */
 | |
|     #errorListener = (event) => {
 | |
|         this.#trackActivity();
 | |
| 
 | |
|         console.group(logPrefix, "⛔️⛔️⛔️  Build error...");
 | |
| 
 | |
|         /**
 | |
|          * @type {ESBuildMessage[]}
 | |
|          */
 | |
|         const esbuildErrorMessages = JSON.parse(event.data);
 | |
| 
 | |
|         for (const error of esbuildErrorMessages) {
 | |
|             console.warn(error.text);
 | |
| 
 | |
|             if (error.location) {
 | |
|                 console.debug(
 | |
|                     `file://${error.location.file}:${error.location.line}:${error.location.column}`,
 | |
|                 );
 | |
|                 console.debug(error.location.lineText);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         console.groupEnd();
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * @type {BuildEventListener}
 | |
|      */
 | |
|     #endListener = () => {
 | |
|         cancelAnimationFrame(this.#reloadFrameID);
 | |
| 
 | |
|         this.#trackActivity();
 | |
| 
 | |
|         if (!this.online) {
 | |
|             log("🚫 Build finished while offline.");
 | |
|             this.deferredReload = true;
 | |
| 
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         log("🛎️ Build completed! Reloading...");
 | |
| 
 | |
|         // We use an animation frame to keep the reload from happening before the
 | |
|         // event loop has a chance to process the message.
 | |
|         this.#reloadFrameID = requestAnimationFrame(() => {
 | |
|             window.location.reload();
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * @type {BuildEventListener}
 | |
|      */
 | |
|     #keepAliveListener = () => {
 | |
|         this.#trackActivity();
 | |
|         log("🏓 Keep-alive");
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Initialize the ESBuild observer.
 | |
|      * This should be called once in your application.
 | |
|      *
 | |
|      * @param {string | URL} [url]
 | |
|      * @returns {ESBuildObserver}
 | |
|      */
 | |
|     static initialize = (url) => {
 | |
|         const esbuildObserver = new ESBuildObserver(url);
 | |
| 
 | |
|         return esbuildObserver;
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      *
 | |
|      * @param {string | URL} [url]
 | |
|      */
 | |
|     constructor(url) {
 | |
|         if (!url) {
 | |
|             throw new TypeError("ESBuildObserver: Cannot construct without a URL");
 | |
|         }
 | |
| 
 | |
|         super(url);
 | |
| 
 | |
|         this.addEventListener("esbuild:start", this.#startListener);
 | |
|         this.addEventListener("esbuild:end", this.#endListener);
 | |
|         this.addEventListener("esbuild:error", this.#errorListener);
 | |
|         this.addEventListener("esbuild:keep-alive", this.#keepAliveListener);
 | |
| 
 | |
|         this.addEventListener("error", this.#internalErrorListener);
 | |
| 
 | |
|         window.addEventListener("offline", () => {
 | |
|             this.online = false;
 | |
|         });
 | |
| 
 | |
|         window.addEventListener("online", () => {
 | |
|             this.online = true;
 | |
| 
 | |
|             if (!this.deferredReload) return;
 | |
| 
 | |
|             log("🛎️ Reloading after offline build...");
 | |
|             this.deferredReload = false;
 | |
| 
 | |
|             window.location.reload();
 | |
|         });
 | |
| 
 | |
|         log("🛎️ Listening for build changes...");
 | |
| 
 | |
|         this.#keepAliveInterval = setInterval(() => {
 | |
|             const now = Date.now();
 | |
| 
 | |
|             if (now - this.lastUpdatedAt < 10_000) return;
 | |
| 
 | |
|             this.alive = false;
 | |
|             log("👋 Waiting for build to start...");
 | |
|         }, 15_000);
 | |
|     }
 | |
| 
 | |
|     [Symbol.dispose]() {
 | |
|         return this.close();
 | |
|     }
 | |
| 
 | |
|     dispose() {
 | |
|         return this[Symbol.dispose]();
 | |
|     }
 | |
| }
 | |
| 
 | |
| export default ESBuildObserver;
 |