import { ModuleName } from "@readium/navigator-html-injectables"; import { Locator, Publication } from "@readium/shared"; import FrameBlobBuider from "./FrameBlobBuilder.ts"; import { FrameManager } from "./FrameManager.ts"; import { Injector } from "../../injection/Injector.ts"; import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts"; const UPPER_BOUNDARY = 5; const LOWER_BOUNDARY = 3; export class FramePoolManager { private readonly container: HTMLElement; private readonly positions: Locator[]; private _currentFrame: FrameManager | undefined; private currentCssProperties: { [key: string]: string } | undefined; private readonly pool: Map = new Map(); private readonly blobs: Map = new Map(); private readonly inprogress: Map> = new Map(); private pendingUpdates: Map = new Map(); private currentBaseURL: string | undefined; private readonly injector: Injector | null = null; private readonly contentProtectionConfig: IContentProtectionConfig; private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig; constructor( container: HTMLElement, positions: Locator[], cssProperties?: { [key: string]: string }, injector?: Injector | null, contentProtectionConfig?: IContentProtectionConfig, keyboardPeripheralsConfig?: IKeyboardPeripheralsConfig ) { this.container = container; this.positions = positions; this.currentCssProperties = cssProperties; this.injector = injector ?? null; this.contentProtectionConfig = contentProtectionConfig || {}; this.keyboardPeripheralsConfig = keyboardPeripheralsConfig || []; } async destroy() { // Wait for all in-progress loads to complete let iit = this.inprogress.values(); let inp = iit.next(); const inprogressPromises: Promise[] = []; while(inp.value) { inprogressPromises.push(inp.value); inp = iit.next(); } if(inprogressPromises.length > 0) { await Promise.allSettled(inprogressPromises); } this.inprogress.clear(); // Destroy all frames let fit = this.pool.values(); let frm = fit.next(); while(frm.value) { await (frm.value as FrameManager).destroy(); frm = fit.next(); } this.pool.clear(); // Revoke all blobs this.blobs.forEach(v => { this.injector?.releaseBlobUrl?.(v); URL.revokeObjectURL(v); }); // Clean up injector if it exists this.injector?.dispose(); // Empty container of elements this.container.childNodes.forEach(v => { if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove(); }) } async update(pub: Publication, locator: Locator, modules: ModuleName[], force=false) { let i = this.positions.findIndex(l => l.locations.position === locator.locations.position); if(i < 0) throw Error(`Locator not found in position list: ${locator.locations.position} > ${this.positions.reduce((acc, l) => l.locations.position || 0 > acc ? l.locations.position || 0 : acc, 0) }`); const newHref = this.positions[i].href; if(this.inprogress.has(newHref)) // If this same href is already being loaded, block until the other function // call has finished executing so we don't end up e.g. loading the blob twice. await this.inprogress.get(newHref); // Create a new progress that doesn't resolve until complete // loading of the resource and its dependencies has finished. const progressPromise = new Promise(async (resolve, reject) => { const disposal: string[] = []; const creation: string[] = []; this.positions.forEach((l, j) => { if(j > (i + UPPER_BOUNDARY) || j < (i - UPPER_BOUNDARY)) { if(!disposal.includes(l.href)) disposal.push(l.href); } if(j < (i + LOWER_BOUNDARY) && j > (i - LOWER_BOUNDARY)) { if(!creation.includes(l.href)) creation.push(l.href); } }); disposal.forEach(async href => { if(creation.includes(href)) return; if(!this.pool.has(href)) return; await this.pool.get(href)?.destroy(); this.pool.delete(href); if(this.pendingUpdates.has(href)) this.pendingUpdates.set(href, { inPool: false }); }); // Check if base URL of publication has changed if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) { // Revoke all blobs this.blobs.forEach(v => { this.injector?.releaseBlobUrl?.(v); URL.revokeObjectURL(v); }); this.blobs.clear(); } this.currentBaseURL = pub.baseURL; const creator = async (href: string) => { if(force) { // Revoke all blobs so that CSSProperties are not stale // When using force, we switch scroll/paginated // If this property is not up to date, it creates issues // when navigating backwards, where paginated will go the // start of the resource instead of the end due to the // corrupted width ColumnSnapper (injectables) gets on init this.blobs.forEach(v => { this.injector?.releaseBlobUrl?.(v); URL.revokeObjectURL(v); }); this.blobs.clear(); this.pendingUpdates.clear(); } if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) { const url = this.blobs.get(href); if(url) { this.injector?.releaseBlobUrl?.(url); URL.revokeObjectURL(url); this.blobs.delete(href); this.pendingUpdates.delete(href); } } if(this.pool.has(href)) { const fm = this.pool.get(href)!; if(!this.blobs.has(href)) { await fm.destroy(); this.pool.delete(href); this.pendingUpdates.delete(href); } else { await fm.load(modules); return; } } const itm = pub.readingOrder.findWithHref(href); if(!itm) return; // TODO throw? if(!this.blobs.has(href)) { const blobBuilder = new FrameBlobBuider( pub, this.currentBaseURL || "", itm, { cssProperties: this.currentCssProperties, injector: this.injector } ); const blobURL = await blobBuilder.build(); this.blobs.set(href, blobURL); } // Create