Merge pull request #4242 from crazyserver/MOBILE-4138

MOBILE-4138 core: Wait for ready on delegates
main
Dani Palou 2024-11-20 15:41:25 +01:00 committed by GitHub
commit bd96f1a2ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 112 additions and 72 deletions

View File

@ -16,7 +16,6 @@ import { BehaviorSubject, Subject } from 'rxjs';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreDelegate, CoreDelegateDisplayHandler, CoreDelegateToDisplay } from './delegate'; import { CoreDelegate, CoreDelegateDisplayHandler, CoreDelegateToDisplay } from './delegate';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CorePromisedValue } from '@classes/promised-value';
/** /**
* Superclass to help creating sorted delegates. * Superclass to help creating sorted delegates.
@ -26,7 +25,6 @@ export class CoreSortedDelegate<
HandlerType extends CoreDelegateDisplayHandler<DisplayType>> HandlerType extends CoreDelegateDisplayHandler<DisplayType>>
extends CoreDelegate<HandlerType> { extends CoreDelegate<HandlerType> {
protected loaded = false;
protected sortedHandlersRxJs: Subject<DisplayType[]> = new BehaviorSubject<DisplayType[]>([]); protected sortedHandlersRxJs: Subject<DisplayType[]> = new BehaviorSubject<DisplayType[]>([]);
protected sortedHandlers: DisplayType[] = []; protected sortedHandlers: DisplayType[] = [];
@ -53,14 +51,14 @@ export class CoreSortedDelegate<
* @returns True if handlers are loaded, false otherwise. * @returns True if handlers are loaded, false otherwise.
*/ */
areHandlersLoaded(): boolean { areHandlersLoaded(): boolean {
return this.loaded; return this.handlersLoaded;
} }
/** /**
* Clear current site handlers. Reserved for core use. * Clear current site handlers. Reserved for core use.
*/ */
protected clearSortedHandlers(): void { protected clearSortedHandlers(): void {
this.loaded = false; this.handlersLoaded = false;
this.sortedHandlersRxJs.next([]); this.sortedHandlersRxJs.next([]);
this.sortedHandlers = []; this.sortedHandlers = [];
} }
@ -74,6 +72,13 @@ export class CoreSortedDelegate<
return this.sortedHandlers; return this.sortedHandlers;
} }
/**
* @inheritdoc
*/
hasHandlers(enabled = false): boolean {
return enabled ? !!this.sortedHandlers.length : !!this.handlers.length;
}
/** /**
* Get the handlers for the current site. * Get the handlers for the current site.
* *
@ -89,27 +94,15 @@ export class CoreSortedDelegate<
* @returns Promise resolved with the handlers. * @returns Promise resolved with the handlers.
*/ */
async getHandlersWhenLoaded(): Promise<DisplayType[]> { async getHandlersWhenLoaded(): Promise<DisplayType[]> {
if (this.loaded) { await this.waitForReady();
return this.sortedHandlers;
}
const promisedHandlers = new CorePromisedValue<DisplayType[]>(); return this.sortedHandlers;
const subscription = this.getHandlersObservable().subscribe((handlers) => {
if (this.loaded) {
subscription?.unsubscribe();
// Return main handlers.
promisedHandlers.resolve(handlers);
}
});
return promisedHandlers;
} }
/** /**
* Update handlers Data. * Update handlers Data.
*/ */
updateData(): void { protected updateData(): void {
const displayData: DisplayType[] = []; const displayData: DisplayType[] = [];
for (const name in this.enabledHandlers) { for (const name in this.enabledHandlers) {
@ -125,7 +118,7 @@ export class CoreSortedDelegate<
// Sort them by priority. // Sort them by priority.
displayData.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); displayData.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
this.loaded = true; this.handlersLoaded = true;
this.sortedHandlersRxJs.next(displayData); this.sortedHandlersRxJs.next(displayData);
this.sortedHandlers = displayData; this.sortedHandlers = displayData;
} }

View File

@ -16,6 +16,8 @@ import { CoreSites } from '@services/sites';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreSite } from '@classes/sites/site'; import { CoreSite } from '@classes/sites/site';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { Subject, BehaviorSubject } from 'rxjs';
import { CorePromisedValue } from './promised-value';
/** /**
* Superclass to help creating delegates * Superclass to help creating delegates
@ -66,21 +68,14 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
protected updatePromises: {[siteId: string]: {[name: string]: Promise<void>}} = {}; protected updatePromises: {[siteId: string]: {[name: string]: Promise<void>}} = {};
/** /**
* Whether handlers have been initialized. * Subject to subscribe to handlers changes.
*/ */
protected handlersInitialized = false; protected handlersUpdated: Subject<void> = new BehaviorSubject<void>(undefined);
/** /**
* Promise to wait for handlers to be initialized. * Handlers loaded flag.
*
* @returns Promise resolved when handlers are enabled.
*/ */
protected handlersInitPromise: Promise<boolean>; protected handlersLoaded = false;
/**
* Function to resolve the handlers init promise.
*/
protected handlersInitResolve!: (enabled: boolean) => void;
/** /**
* Constructor of the Delegate. * Constructor of the Delegate.
@ -90,10 +85,6 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
constructor(delegateName: string) { constructor(delegateName: string) {
this.logger = CoreLogger.getInstance(delegateName); this.logger = CoreLogger.getInstance(delegateName);
this.handlersInitPromise = new Promise((resolve): void => {
this.handlersInitResolve = resolve;
});
// Update handlers on this cases. // Update handlers on this cases.
CoreEvents.on(CoreEvents.LOGIN, () => this.updateHandlers()); CoreEvents.on(CoreEvents.LOGIN, () => this.updateHandlers());
CoreEvents.on(CoreEvents.SITE_UPDATED, () => this.updateHandlers()); CoreEvents.on(CoreEvents.SITE_UPDATED, () => this.updateHandlers());
@ -120,6 +111,7 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
} }
/** /**
* Check if handlers are loaded.
* Execute a certain function in a enabled handler. * Execute a certain function in a enabled handler.
* If the handler isn't found or function isn't defined, call the same function in the default handler. * If the handler isn't found or function isn't defined, call the same function in the default handler.
* *
@ -216,12 +208,13 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
} }
/** /**
* Check if the delegate has at least 1 registered handler (not necessarily enabled). * Returns if the delegate has any handler.
* *
* @returns If there is at least 1 handler. * @param enabled Check only enabled handlers or all.
* @returns True if there's any registered handler, false otherwise.
*/ */
hasHandlers(): boolean { hasHandlers(enabled = false): boolean {
return Object.keys(this.handlers).length > 0; return enabled ? !!this.enabledHandlers.length : !!this.handlers.length;
} }
/** /**
@ -324,13 +317,16 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
* *
* @returns Resolved when done. * @returns Resolved when done.
*/ */
async updateHandlers(): Promise<void> { protected async updateHandlers(): Promise<void> {
this.handlersLoaded = false;
const enabled = await this.isEnabled(); const enabled = await this.isEnabled();
if (!enabled) { if (!enabled) {
this.logger.debug('Delegate not enabled.'); this.logger.debug('Delegate not enabled.');
this.handlersInitResolve(false); this.handlersLoaded = true;
this.handlersUpdated.next();
return; return;
} }
@ -355,10 +351,10 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
// Verify that this call is the last one that was started. // Verify that this call is the last one that was started.
if (this.isLastUpdateCall(now)) { if (this.isLastUpdateCall(now)) {
this.handlersInitialized = true;
this.handlersInitResolve(true);
this.updateData(); this.updateData();
this.handlersLoaded = true;
this.handlersUpdated.next();
} }
} }
@ -366,10 +362,34 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
* Update handlers Data. * Update handlers Data.
* Override this function to update handlers data. * Override this function to update handlers data.
*/ */
updateData(): void { protected updateData(): void {
// To be overridden. // To be overridden.
} }
/**
* Waits the handlers to be ready.
*
* @returns Resolved when the handlers are ready.
*/
async waitForReady(): Promise<void> {
if (this.handlersLoaded) {
return;
}
const promise = new CorePromisedValue<void>();
const subscription = this.handlersUpdated.subscribe(() => {
if (this.handlersLoaded) {
// Resolve.
promise.resolve();
subscription?.unsubscribe();
}
});
return promise;
}
} }
/** /**

View File

@ -212,7 +212,7 @@ export class CoreBlockDelegateService extends CoreDelegate<CoreBlockHandler> {
* Called when there are new block handlers available. Informs anyone who subscribed to the * Called when there are new block handlers available. Informs anyone who subscribed to the
* observable. * observable.
*/ */
updateData(): void { protected updateData(): void {
this.blocksUpdateObservable.next(); this.blocksUpdateObservable.next();
} }

View File

@ -26,6 +26,8 @@ import {
import { CoreCourseAccessDataType } from '../constants'; import { CoreCourseAccessDataType } from '../constants';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { Subject } from 'rxjs/internal/Subject';
import { BehaviorSubject } from 'rxjs';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { CORE_COURSES_MY_COURSES_REFRESHED_EVENT } from '@features/courses/constants'; import { CORE_COURSES_MY_COURSES_REFRESHED_EVENT } from '@features/courses/constants';
@ -214,7 +216,8 @@ export interface CoreCourseOptionsMenuHandlerToDisplay {
@Injectable( { providedIn: 'root' }) @Injectable( { providedIn: 'root' })
export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOptionsHandler> { export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOptionsHandler> {
protected loaded: { [courseId: number]: boolean } = {}; protected courseHandlersUpdated: { [courseId: number]: Subject<void> } = {};
protected courseHandlersLoaded: { [courseId: number]: boolean } = {};
protected lastUpdateHandlersForCoursesStart: { protected lastUpdateHandlersForCoursesStart: {
[courseId: number]: number; [courseId: number]: number;
} = {}; } = {};
@ -224,7 +227,6 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
access: CoreCourseAccess; access: CoreCourseAccess;
navOptions?: CoreCourseUserAdminOrNavOptionIndexed; navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
admOptions?: CoreCourseUserAdminOrNavOptionIndexed; admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
deferred: CorePromisedValue<void>;
enabledHandlers: CoreCourseOptionsHandler[]; enabledHandlers: CoreCourseOptionsHandler[];
enabledMenuHandlers: CoreCourseOptionsMenuHandler[]; enabledMenuHandlers: CoreCourseOptionsMenuHandler[];
}; };
@ -247,7 +249,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
* @returns True if handlers are loaded, false otherwise. * @returns True if handlers are loaded, false otherwise.
*/ */
areHandlersLoaded(courseId: number): boolean { areHandlersLoaded(courseId: number): boolean {
return !!this.loaded[courseId]; return !!this.courseHandlersLoaded[courseId];
} }
/** /**
@ -257,12 +259,12 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
*/ */
protected clearCoursesHandlers(courseId?: number): void { protected clearCoursesHandlers(courseId?: number): void {
if (courseId) { if (courseId) {
if (!this.loaded[courseId]) { if (!this.courseHandlersLoaded[courseId]) {
// Don't clear if not loaded, it's probably an ongoing load and it could cause JS errors. // Don't clear if not loaded, it's probably an ongoing load and it could cause JS errors.
return; return;
} }
this.loaded[courseId] = false; this.courseHandlersLoaded[courseId] = false;
delete this.coursesHandlers[courseId]; delete this.coursesHandlers[courseId];
} else { } else {
for (const courseId in this.coursesHandlers) { for (const courseId in this.coursesHandlers) {
@ -321,7 +323,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
): Promise<void> { ): Promise<void> {
// If the handlers aren't loaded, do not refresh. // If the handlers aren't loaded, do not refresh.
if (!this.loaded[courseId]) { if (!this.courseHandlersLoaded[courseId]) {
refresh = false; refresh = false;
} }
@ -331,21 +333,20 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
access: accessData, access: accessData,
navOptions, navOptions,
admOptions, admOptions,
deferred: new CorePromisedValue(),
enabledHandlers: [], enabledHandlers: [],
enabledMenuHandlers: [], enabledMenuHandlers: [],
}; };
this.courseHandlersUpdated[courseId] = new BehaviorSubject<void>(undefined);
} else { } else {
this.coursesHandlers[courseId].access = accessData; this.coursesHandlers[courseId].access = accessData;
this.coursesHandlers[courseId].navOptions = navOptions; this.coursesHandlers[courseId].navOptions = navOptions;
this.coursesHandlers[courseId].admOptions = admOptions; this.coursesHandlers[courseId].admOptions = admOptions;
this.coursesHandlers[courseId].deferred = new CorePromisedValue();
} }
this.updateHandlersForCourse(courseId, accessData, navOptions, admOptions); this.updateHandlersForCourse(courseId, accessData, navOptions, admOptions);
} }
await this.coursesHandlers[courseId].deferred; await this.waitCourseHandlersForReady(courseId);
} }
/** /**
@ -612,7 +613,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
/** /**
* Update handlers for each course. * Update handlers for each course.
*/ */
updateData(): void { protected updateData(): void {
// Update handlers for all courses. // Update handlers for all courses.
for (const courseId in this.coursesHandlers) { for (const courseId in this.coursesHandlers) {
const handler = this.coursesHandlers[courseId]; const handler = this.coursesHandlers[courseId];
@ -635,6 +636,8 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
navOptions?: CoreCourseUserAdminOrNavOptionIndexed, navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
admOptions?: CoreCourseUserAdminOrNavOptionIndexed, admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<void> { ): Promise<void> {
this.courseHandlersLoaded[courseId] = false;
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
const enabledForCourse: CoreCourseOptionsHandler[] = []; const enabledForCourse: CoreCourseOptionsHandler[] = [];
const enabledForCourseMenu: CoreCourseOptionsMenuHandler[] = []; const enabledForCourseMenu: CoreCourseOptionsMenuHandler[] = [];
@ -675,13 +678,38 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
// Update the coursesHandlers array with the new enabled addons. // Update the coursesHandlers array with the new enabled addons.
this.coursesHandlers[courseId].enabledHandlers = enabledForCourse; this.coursesHandlers[courseId].enabledHandlers = enabledForCourse;
this.coursesHandlers[courseId].enabledMenuHandlers = enabledForCourseMenu; this.coursesHandlers[courseId].enabledMenuHandlers = enabledForCourseMenu;
this.loaded[courseId] = true;
// Resolve the promise. // Notify changes.
this.coursesHandlers[courseId].deferred.resolve(); this.courseHandlersLoaded[courseId] = true;
this.courseHandlersUpdated[courseId].next();
} }
} }
/**
* Waits the course handlers to be ready.
*
* @param courseId The course ID.
* @returns Promise resolved when the handlers are ready.
*/
async waitCourseHandlersForReady(courseId: number): Promise<void> {
if (this.courseHandlersLoaded[courseId]) {
return;
}
const promise = new CorePromisedValue<void>();
const subscription = this.courseHandlersUpdated[courseId].subscribe(() => {
if (this.courseHandlersLoaded[courseId]) {
// Resolve.
promise.resolve();
subscription?.unsubscribe();
}
});
return promise;
}
} }
export const CoreCourseOptionsDelegate = makeSingleton(CoreCourseOptionsDelegateService); export const CoreCourseOptionsDelegate = makeSingleton(CoreCourseOptionsDelegateService);

View File

@ -112,9 +112,9 @@ export class CoreFilterDelegateService extends CoreDelegate<CoreFilterHandler> {
skipFilters?: string[], skipFilters?: string[],
siteId?: string, siteId?: string,
): Promise<string> { ): Promise<string> {
// Wait for filters to be initialized. // Wait for filters to be initialized.
const enabled = await this.handlersInitPromise; await this.waitForReady();
const enabled = this.hasHandlers(true);
if (!enabled) { if (!enabled) {
// No enabled filters, return the text. // No enabled filters, return the text.
return text; return text;
@ -201,7 +201,8 @@ export class CoreFilterDelegateService extends CoreDelegate<CoreFilterHandler> {
): Promise<void> { ): Promise<void> {
// Wait for filters to be initialized. // Wait for filters to be initialized.
const enabled = await this.handlersInitPromise; await this.waitForReady();
const enabled = this.hasHandlers(true);
if (!enabled) { if (!enabled) {
return; return;
} }
@ -276,7 +277,8 @@ export class CoreFilterDelegateService extends CoreDelegate<CoreFilterHandler> {
*/ */
async shouldBeApplied(filters: CoreFilterFilter[], options: CoreFilterFormatTextOptions, site?: CoreSite): Promise<boolean> { async shouldBeApplied(filters: CoreFilterFilter[], options: CoreFilterFormatTextOptions, site?: CoreSite): Promise<boolean> {
// Wait for filters to be initialized. // Wait for filters to be initialized.
const enabled = await this.handlersInitPromise; await this.waitForReady();
const enabled = this.hasHandlers(true);
if (!enabled) { if (!enabled) {
return false; return false;
} }

View File

@ -13,10 +13,8 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreDashboardHomeHandler } from '@features/courses/services/handlers/dashboard-home';
import { CoreSiteHomeHomeHandler } from '@features/sitehome/services/handlers/sitehome-home';
import { CoreSites } from '@services/sites';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreMainMenuHomeDelegate } from '../home-delegate';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../mainmenu-delegate'; import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../mainmenu-delegate';
/** /**
@ -34,12 +32,9 @@ export class CoreMainMenuHomeHandlerService implements CoreMainMenuHandler {
* @inheritdoc * @inheritdoc
*/ */
async isEnabled(): Promise<boolean> { async isEnabled(): Promise<boolean> {
const siteId = CoreSites.getCurrentSiteId(); await CoreMainMenuHomeDelegate.waitForReady();
const dashboardEnabled = await CoreDashboardHomeHandler.isEnabledForSite(siteId); return CoreMainMenuHomeDelegate.hasHandlers(true);
const siteHomeEnabled = await CoreSiteHomeHomeHandler.isEnabledForSite(siteId);
return dashboardEnabled || siteHomeEnabled;
} }
/** /**

View File

@ -16,6 +16,7 @@ import { mock, mockSingleton } from '@/testing/utils';
import { CoreSite } from '@classes/sites/site'; import { CoreSite } from '@classes/sites/site';
import { CorePluginFileDelegateService, CorePluginFileHandler } from '@services/plugin-file-delegate'; import { CorePluginFileDelegateService, CorePluginFileHandler } from '@services/plugin-file-delegate';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreEvents } from '@singletons/events';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
describe('CorePluginFileDelegate', () => { describe('CorePluginFileDelegate', () => {
@ -32,7 +33,8 @@ describe('CorePluginFileDelegate', () => {
pluginFileDelegate = new CorePluginFileDelegateService(); pluginFileDelegate = new CorePluginFileDelegateService();
pluginFileDelegate.registerHandler(new ModFooRevisionHandler()); pluginFileDelegate.registerHandler(new ModFooRevisionHandler());
await pluginFileDelegate.updateHandlers(); CoreEvents.trigger(CoreEvents.LOGIN, { siteId: '42' }, '42');
await pluginFileDelegate.waitForReady();
}); });
it('removes revision from a URL', () => { it('removes revision from a URL', () => {