import type { BrowserWindow, BrowserWindowConstructorOptions, LoadURLOptions } from 'electron'; import { promisify } from 'util'; import type { SessionHelperInterface } from '../session/session-helper-interface'; import { AppWindow } from './app-window'; import type { UrlAppWindowInterface } from './url-app-window-interface'; import { WindowClosedError } from './window-closed-error'; import Timeout = NodeJS.Timeout; export abstract class UrlAppWindow extends AppWindow implements UrlAppWindowInterface { protected loadOptions: LoadURLOptions; /** * the wait interval after a failed load to try again */ private waitInterval: number = Milliseconds.TWO_HUNDRED; private loadWaitTime: number = 0; private loadWaitTimeStep: number = 10; private loadWaitTimeStepResetTimeout: Timeout; private loadWaitTimeResetTimeoutTime: number = Milliseconds.ONE_MINUTE; /** * when this promise is resolved a safe load is allowed * * it resets which each load to resolve again after a wait time * * @see loadWaitTime */ private loadWait: Promise = Promise.resolve(); protected constructor( sessionHelper: SessionHelperInterface, uri: string, options: BrowserWindowConstructorOptions = {}, loadOptions: LoadURLOptions = {} ) { super(sessionHelper, uri, { ...options, ...{ webPreferences: { enableRemoteModule: false, nodeIntegration: false, contextIsolation: true, }, }, }); this.loadOptions = loadOptions; this.loadWaitTimeStepResetTimeout = setTimeout(() => {}, 0); } public downloadUrlSafe(url: string, savePath: string, options?: LoadURLOptions): Promise { return new Promise((resolve, reject) => { if (!this._window) { throw new WindowClosedError(); } this._window.webContents.session.once('will-download', (event, item) => { item.setSavePath(savePath); item.once('done', (doneEvent, state) => { switch (state) { case 'completed': resolve(); break; case 'cancelled': case 'interrupted': default: reject(new Error(state)); break; } }); item.on('updated', () => { item.resume(); }); }); void this.loadUrlSafe(url, options); }); } public async loadUrlSafe(url: string, options?: LoadURLOptions): Promise { return this.loadWait.then(async () => { let failedLoad = true; while (failedLoad) { await new Promise((resolve) => { if (!this._window) { throw new WindowClosedError(); } this._window.webContents.once('did-navigate', (event, navigationUrl, httpResponseCode) => { failedLoad = HttpCode.BAD_REQUEST <= httpResponseCode; if (HttpCode.TOO_MANY_REQUESTS === httpResponseCode) { // go slower this.loadWaitTime += this.loadWaitTimeStep; // but go faster again after a time clearTimeout(this.loadWaitTimeStepResetTimeout); this.loadWaitTimeStepResetTimeout = setTimeout(() => { this.loadWaitTime = 0; }, this.loadWaitTimeResetTimeoutTime); } resolve(); }); void this._window.loadURL(url, options); }); if (failedLoad) { await promisify(setTimeout)(this.waitInterval); } } this.loadWait = promisify(setTimeout)(this.loadWaitTime); }); } protected load(window: BrowserWindow): Promise { return window.loadURL(this.uri, this.loadOptions); } }