// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { Subscription } from 'rxjs'; import { AsyncDirective } from './async-directive'; import { PageLoadsManager } from './page-loads-manager'; import { CorePromisedValue } from './promised-value'; import { WSObservable } from './site'; /** * Class to watch requests from a page load (including requests from page sub-components). */ export class PageLoadWatcher { protected hasChanges = false; protected ongoingRequests = 0; protected components = new Set(); protected loadedTimeout?: number; protected hasChangesPromises: Promise[] = []; constructor( protected loadsManager: PageLoadsManager, protected updateInBackground: boolean, ) { } /** * Whether this load watcher can update data in background. * * @returns Whether this load watcher can update data in background. */ canUpdateInBackground(): boolean { return this.updateInBackground; } /** * Whether this load watcher had meaningful changes received in background. * * @returns Whether this load watcher had meaningful changes received in background. */ hasMeaningfulChanges(): boolean { return this.hasChanges; } /** * Set has meaningful changes to true. */ markMeaningfulChanges(): void { this.hasChanges = true; } /** * Watch a component, waiting for it to be ready. * * @param component Component instance. */ async watchComponent(component: AsyncDirective): Promise { this.components.add(component); clearTimeout(this.loadedTimeout); try { await component.ready(); } finally { this.components.delete(component); this.checkHasLoaded(); } } /** * Get the reading strategy to use. * * @returns Reading strategy to use. */ getReadingStrategy(): CoreSitesReadingStrategy | undefined { return this.updateInBackground ? CoreSitesReadingStrategy.STALE_WHILE_REVALIDATE : undefined; } /** * Watch a WS request, handling the different values it can return, calling the hasMeaningfulChanges callback if needed to * detect if there are new meaningful changes in the page load, and completing the page load when all requests have * finished and all components are ready. * * @param observable Observable of the request. * @param hasMeaningfulChanges Callback to check if there are meaningful changes if data was updated in background. * @returns First value of the observable. */ watchRequest( observable: WSObservable, hasMeaningfulChanges?: (previousValue: T, newValue: T) => Promise, ): Promise { const promisedValue = new CorePromisedValue(); let subscription: Subscription | null = null; let firstValue: T | undefined; this.ongoingRequests++; clearTimeout(this.loadedTimeout); const complete = async () => { this.ongoingRequests--; this.checkHasLoaded(); // Subscription variable might not be set because the observable completed immediately. Wait for next tick. await CoreUtils.nextTick(); subscription?.unsubscribe(); }; subscription = observable.subscribe({ next: value => { if (!firstValue) { firstValue = value; promisedValue.resolve(value); return; } // Second value, it means data was updated in background. Compare data. if (!hasMeaningfulChanges) { return; } this.hasChangesPromises.push(CoreUtils.ignoreErrors(hasMeaningfulChanges(firstValue, value), false)); }, error: (error) => { promisedValue.reject(error); complete(); }, complete: () => complete(), }); return promisedValue; } /** * Check if the load has finished. */ protected checkHasLoaded(): void { if (this.ongoingRequests !== 0 || this.components.size !== 0) { // Load not finished. return; } // It seems load has finished. Wait to make sure no new component has been rendered and started loading. // If a new component or a new request starts the timeout will be cancelled, no need to double check it. clearTimeout(this.loadedTimeout); this.loadedTimeout = window.setTimeout(async () => { // Loading finished. Calculate has changes. const values = await Promise.all(this.hasChangesPromises); this.hasChanges = this.hasChanges || values.includes(true); this.loadsManager.onPageLoaded(this); }, 100); } }