Merge pull request #3950 from NoelDeMartin/MOBILE-4272

MOBILE-4272 workshop: Decouple from initial bundle
main
Dani Palou 2024-03-04 09:57:17 +01:00 committed by GitHub
commit cca9a3b784
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 191 additions and 42 deletions

View File

@ -15,9 +15,7 @@
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { CoreSharedModule } from '@/core/shared.module';
import {
AddonModWorkshopAssessmentStrategyAccumulativeHandler,
} from '@addons/mod/workshop/assessment/accumulative/services/handler-lazy';
import { getAssessmentStrategyHandlerInstance } from './services/handler';
@NgModule({
imports: [
@ -28,12 +26,7 @@ import {
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
AddonWorkshopAssessmentStrategyDelegate.registerHandler(
AddonModWorkshopAssessmentStrategyAccumulativeHandler.instance,
);
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
},
},
],

View File

@ -18,6 +18,7 @@ import {
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_ACCUMULATIVE_NAME,
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_ACCUMULATIVE_STRATEGY_NAME,
} from '@addons/mod/workshop/assessment/constants';
import type { AddonModWorkshopAssessmentStrategyAccumulativeHandlerLazyService } from './handler-lazy';
export class AddonModWorkshopAssessmentStrategyAccumulativeHandlerService {
@ -32,13 +33,23 @@ export class AddonModWorkshopAssessmentStrategyAccumulativeHandlerService {
* @returns Assessment strategy handler.
*/
export function getAssessmentStrategyHandlerInstance(): AddonWorkshopAssessmentStrategyHandler {
const lazyHandler = asyncInstance(async () => {
const lazyHandler = asyncInstance<
AddonModWorkshopAssessmentStrategyAccumulativeHandlerLazyService,
AddonModWorkshopAssessmentStrategyAccumulativeHandlerService
>(async () => {
const { AddonModWorkshopAssessmentStrategyAccumulativeHandler } = await import('./handler-lazy');
return AddonModWorkshopAssessmentStrategyAccumulativeHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopAssessmentStrategyAccumulativeHandlerService());
lazyHandler.setLazyInstanceMethods([
'isEnabled',
'getComponent',
'getOriginalValues',
'hasDataChanged',
'prepareAssessmentData',
]);
return lazyHandler;
}

View File

@ -15,7 +15,7 @@
import { CoreSharedModule } from '@/core/shared.module';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { AddonModWorkshopAssessmentStrategyCommentsHandler } from '@addons/mod/workshop/assessment/comments/services/handler-lazy';
import { getAssessmentStrategyHandlerInstance } from './services/handler';
@NgModule({
imports: [
@ -26,10 +26,7 @@ import { AddonModWorkshopAssessmentStrategyCommentsHandler } from '@addons/mod/w
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
AddonWorkshopAssessmentStrategyDelegate.registerHandler(AddonModWorkshopAssessmentStrategyCommentsHandler.instance);
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
},
},
],

View File

@ -18,6 +18,7 @@ import {
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_COMMENTS_NAME,
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_COMMENTS_STRATEGY_NAME,
} from '@addons/mod/workshop/assessment/constants';
import type { AddonModWorkshopAssessmentStrategyCommentsHandlerLazyService } from './handler-lazy';
export class AddonModWorkshopAssessmentStrategyCommentsHandlerService {
@ -32,13 +33,23 @@ export class AddonModWorkshopAssessmentStrategyCommentsHandlerService {
* @returns Assessment strategy handler.
*/
export function getAssessmentStrategyHandlerInstance(): AddonWorkshopAssessmentStrategyHandler {
const lazyHandler = asyncInstance(async () => {
const lazyHandler = asyncInstance<
AddonModWorkshopAssessmentStrategyCommentsHandlerLazyService,
AddonModWorkshopAssessmentStrategyCommentsHandlerService
>(async () => {
const { AddonModWorkshopAssessmentStrategyCommentsHandler } = await import('./handler-lazy');
return AddonModWorkshopAssessmentStrategyCommentsHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopAssessmentStrategyCommentsHandlerService());
lazyHandler.setLazyInstanceMethods([
'isEnabled',
'getComponent',
'getOriginalValues',
'hasDataChanged',
'prepareAssessmentData',
]);
return lazyHandler;
}

View File

@ -15,9 +15,7 @@
import { CoreSharedModule } from '@/core/shared.module';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import {
AddonModWorkshopAssessmentStrategyNumErrorsHandler,
} from '@addons/mod/workshop/assessment/numerrors/services/handler-lazy';
import { getAssessmentStrategyHandlerInstance } from './services/handler';
@NgModule({
imports: [
@ -28,12 +26,7 @@ import {
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
AddonWorkshopAssessmentStrategyDelegate.registerHandler(
AddonModWorkshopAssessmentStrategyNumErrorsHandler.instance,
);
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
},
},
],

View File

@ -18,6 +18,7 @@ import {
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_NUMERRORS_NAME,
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_NUMERRORS_STRATEGY_NAME,
} from '@addons/mod/workshop/assessment/constants';
import type { AddonModWorkshopAssessmentStrategyNumErrorsHandlerLazyService } from './handler-lazy';
export class AddonModWorkshopAssessmentStrategyNumErrorsHandlerService {
@ -32,13 +33,23 @@ export class AddonModWorkshopAssessmentStrategyNumErrorsHandlerService {
* @returns Assessment strategy handler.
*/
export function getAssessmentStrategyHandlerInstance(): AddonWorkshopAssessmentStrategyHandler {
const lazyHandler = asyncInstance(async () => {
const lazyHandler = asyncInstance<
AddonModWorkshopAssessmentStrategyNumErrorsHandlerLazyService,
AddonModWorkshopAssessmentStrategyNumErrorsHandlerService
>(async () => {
const { AddonModWorkshopAssessmentStrategyNumErrorsHandler } = await import('./handler-lazy');
return AddonModWorkshopAssessmentStrategyNumErrorsHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopAssessmentStrategyNumErrorsHandlerService());
lazyHandler.setLazyInstanceMethods([
'isEnabled',
'getComponent',
'getOriginalValues',
'hasDataChanged',
'prepareAssessmentData',
]);
return lazyHandler;
}

View File

@ -15,7 +15,7 @@
import { CoreSharedModule } from '@/core/shared.module';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { AddonModWorkshopAssessmentStrategyRubricHandler } from '@addons/mod/workshop/assessment/rubric/services/handler-lazy';
import { getAssessmentStrategyHandlerInstance } from './services/handler';
@NgModule({
imports: [
@ -26,10 +26,7 @@ import { AddonModWorkshopAssessmentStrategyRubricHandler } from '@addons/mod/wor
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
AddonWorkshopAssessmentStrategyDelegate.registerHandler(AddonModWorkshopAssessmentStrategyRubricHandler.instance);
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
},
},
],

View File

@ -18,6 +18,7 @@ import {
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_RUBRIC_NAME,
ADDON_MOD_WORKSHOP_ASSESSMENT_STRATEGY_RUBRIC_STRATEGY_NAME,
} from '@addons/mod/workshop/assessment/constants';
import type { AddonModWorkshopAssessmentStrategyRubricHandlerLazyService } from './handler-lazy';
export class AddonModWorkshopAssessmentStrategyRubricHandlerService {
@ -32,13 +33,23 @@ export class AddonModWorkshopAssessmentStrategyRubricHandlerService {
* @returns Assessment strategy handler.
*/
export function getAssessmentStrategyHandlerInstance(): AddonWorkshopAssessmentStrategyHandler {
const lazyHandler = asyncInstance(async () => {
const lazyHandler = asyncInstance<
AddonModWorkshopAssessmentStrategyRubricHandlerLazyService,
AddonModWorkshopAssessmentStrategyRubricHandlerService
>(async () => {
const { AddonModWorkshopAssessmentStrategyRubricHandler } = await import('./handler-lazy');
return AddonModWorkshopAssessmentStrategyRubricHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopAssessmentStrategyRubricHandlerService());
lazyHandler.setLazyInstanceMethods([
'isEnabled',
'getComponent',
'getOriginalValues',
'hasDataChanged',
'prepareAssessmentData',
]);
return lazyHandler;
}

View File

@ -21,6 +21,7 @@ import {
} from '@addons/mod/workshop/constants';
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
import { CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
import type { AddonModWorkshopPrefetchHandlerLazyService } from './prefetch-lazy';
export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
@ -37,13 +38,17 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
* @returns Prefetch handler.
*/
export function getPrefetchHandlerInstance(): CoreCourseModulePrefetchHandler {
const lazyHandler = asyncInstance(async () => {
const lazyHandler = asyncInstance<
AddonModWorkshopPrefetchHandlerLazyService,
AddonModWorkshopPrefetchHandlerService
>(async () => {
const { AddonModWorkshopPrefetchHandler } = await import('./prefetch-lazy');
return AddonModWorkshopPrefetchHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopPrefetchHandlerService());
lazyHandler.setLazyInstanceMethods(['sync']);
return lazyHandler;
}

View File

@ -15,6 +15,7 @@
import { asyncInstance } from '@/core/utils/async-instance';
import { ADDON_MOD_WORKSHOP_SYNC_CRON_NAME } from '@addons/mod/workshop/constants';
import { CoreCronHandler } from '@services/cron';
import type { AddonModWorkshopSyncCronHandlerLazyService } from './sync-cron-lazy';
export class AddonModWorkshopSyncCronHandlerService {
@ -28,13 +29,17 @@ export class AddonModWorkshopSyncCronHandlerService {
* @returns Cron handler.
*/
export function getCronHandlerInstance(): CoreCronHandler {
const lazyHandler = asyncInstance(async () => {
const lazyHandler = asyncInstance<
AddonModWorkshopSyncCronHandlerLazyService,
AddonModWorkshopSyncCronHandlerService
>(async () => {
const { AddonModWorkshopSyncCronHandler } = await import('./sync-cron-lazy');
return AddonModWorkshopSyncCronHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopSyncCronHandlerService());
lazyHandler.setLazyInstanceMethods(['execute', 'getInterval']);
return lazyHandler;
}

View File

@ -114,3 +114,11 @@ Feature: Test basic usage of workshop activity in app
And I pull to refresh in the app
Then I should find "Closed" in the app
And I should find "Conclusion 1" in the app
Scenario: Prefetch a workshop
Given I entered the workshop activity "workshop" on course "Course 1" as "teacher1" in the app
When I press "Information" in the app
And I press "Download" in the app
And I press "Close" in the app
And I press the back button in the app
Then I should find "Downloaded" in the app

View File

@ -27,8 +27,8 @@ import { AddonModWorkshopIndexLinkHandler } from './services/handlers/index-link
import { AddonModWorkshopListLinkHandler } from './services/handlers/list-link';
import { AddonModWorkshopModuleHandler } from './services/handlers/module';
import { ADDON_MOD_WORKSHOP_COMPONENT, ADDON_MOD_WORKSHOP_PAGE_NAME } from '@addons/mod/workshop/constants';
import { AddonModWorkshopPrefetchHandler } from '@addons/mod/workshop/services/handlers/prefetch-lazy';
import { AddonModWorkshopSyncCronHandler } from '@addons/mod/workshop/services/handlers/sync-cron-lazy';
import { getPrefetchHandlerInstance } from '@addons/mod/workshop/services/handlers/prefetch';
import { getCronHandlerInstance } from '@addons/mod/workshop/services/handlers/sync-cron';
/**
* Get workshop services.
@ -85,13 +85,10 @@ const routes: Routes = [
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
// TODO use async instances
// CoreCourseModulePrefetchDelegate.registerHandler(getPrefetchHandlerInstance());
// CoreCronDelegate.register(getCronHandlerInstance());
CoreCourseModulePrefetchDelegate.registerHandler(getPrefetchHandlerInstance());
CoreCronDelegate.register(getCronHandlerInstance());
CoreCourseModuleDelegate.registerHandler(AddonModWorkshopModuleHandler.instance);
CoreCourseModulePrefetchDelegate.registerHandler(AddonModWorkshopPrefetchHandler.instance);
CoreCronDelegate.register(AddonModWorkshopSyncCronHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModWorkshopIndexLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModWorkshopListLinkHandler.instance);

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TupleMatches } from '@/core/utils/types';
import { CorePromisedValue } from '@classes/promised-value';
/**
@ -27,12 +28,16 @@ function createAsyncInstanceWrapper<
lazyConstructor?: () => TLazyInstance | Promise<TLazyInstance>,
): AsyncInstanceWrapper<TLazyInstance, TEagerInstance> {
let promisedInstance: CorePromisedValue<TLazyInstance> | null = null;
let lazyInstanceMethods: Array<string | symbol>;
let eagerInstance: TEagerInstance;
return {
get instance() {
return promisedInstance?.value ?? undefined;
},
get lazyInstanceMethods() {
return lazyInstanceMethods;
},
get eagerInstance() {
return eagerInstance;
},
@ -63,6 +68,9 @@ function createAsyncInstanceWrapper<
promisedInstance.resolve(instance);
},
setLazyInstanceMethods(methods) {
lazyInstanceMethods = methods;
},
setEagerInstance(instance) {
eagerInstance = instance;
},
@ -108,10 +116,14 @@ export interface AsyncInstanceWrapper<
TEagerInstance extends AsyncObject = Partial<TLazyInstance>
> {
instance?: TLazyInstance;
lazyInstanceMethods?: Array<string | symbol>;
eagerInstance?: TEagerInstance;
getInstance(): Promise<TLazyInstance>;
getProperty<P extends keyof TLazyInstance>(property: P): Promise<TLazyInstance[P]>;
setInstance(instance: TLazyInstance): void;
setLazyInstanceMethods<const T extends Array<string | symbol>>(
methods: LazyMethodsGuard<T, TLazyInstance, TEagerInstance>,
): void;
setEagerInstance(eagerInstance: TEagerInstance): void;
setLazyConstructor(lazyConstructor: () => TLazyInstance | Promise<TLazyInstance>): void;
resetInstance(): void;
@ -141,6 +153,12 @@ export type AsyncInstance<TLazyInstance extends TEagerInstance, TEagerInstance e
[k in keyof TLazyInstance]: AsyncMethod<TLazyInstance[k]>;
};
/**
* Guard type to make sure that lazy methods match what the lazy class implements.
*/
export type LazyMethodsGuard<TMethods extends Array<string | symbol>, TLazyInstance, TEagerInstance> =
TupleMatches<TMethods, Exclude<keyof TLazyInstance, keyof TEagerInstance>> extends true ? TMethods : never;
/**
* Create an asynchronous instance proxy, where all methods will be callable directly but will become asynchronous. If the
* underlying instance hasn't been set, methods will be resolved once it is.
@ -171,6 +189,10 @@ export function asyncInstance<TLazyInstance extends TEagerInstance, TEagerInstan
return Reflect.get(wrapper.eagerInstance, property, receiver);
}
if (wrapper.lazyInstanceMethods && !wrapper.lazyInstanceMethods.includes(property)) {
return undefined;
}
return async (...args: unknown[]) => {
const instance = await wrapper.getInstance();
const method = Reflect.get(instance, property, receiver);

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance';
import { AsyncInstance, LazyMethodsGuard, asyncInstance } from '@/core/utils/async-instance';
import { expectAnyType, expectSameTypes } from '@/testing/utils';
describe('AsyncInstance', () => {
@ -33,8 +33,45 @@ describe('AsyncInstance', () => {
expect(asyncService.instance).toBeUndefined();
expect(asyncService.answer).toEqual(42);
expect(asyncService.instance).toBeUndefined();
expect(await asyncService.isEager()).toBe(true);
expect(await asyncService.hello()).toEqual('Hi there!');
expect(asyncService.instance).toBeInstanceOf(LazyService);
expect(await asyncService.isEager()).toBe(false);
});
it('does not return undefined methods when they are declared', async () => {
const asyncService = asyncInstance<LazyService, EagerService>(() => new LazyService());
asyncService.setEagerInstance(new EagerService());
asyncService.setLazyInstanceMethods(['hello', 'goodbye']);
expect(asyncService.hello).not.toBeUndefined();
expect(asyncService.goodbye).not.toBeUndefined();
expect(asyncService.isEager).not.toBeUndefined();
expect(asyncService.notImplemented).toBeUndefined();
});
it('guards against missing or invalid instance methods', () => {
// Define interfaces.
interface Eager {
lorem(): void;
ipsum(): void;
}
interface Lazy extends Eager {
foo(): void;
bar(): void;
}
// Test valid method tuples.
expectSameTypes<LazyMethodsGuard<['foo', 'bar'], Lazy, Eager>, ['foo', 'bar']>(true);
expectSameTypes<LazyMethodsGuard<['bar', 'foo'], Lazy, Eager>, ['bar', 'foo']>(true);
expectSameTypes<LazyMethodsGuard<['foo', 'foo', 'bar'], Lazy, Eager>, ['foo', 'foo', 'bar']>(true);
// Test invalid method tuples.
expectSameTypes<LazyMethodsGuard<['foo'], Lazy, Eager>, never>(true);
expectSameTypes<LazyMethodsGuard<['foo', 'bar', 'lorem'], Lazy, Eager>, never>(true);
expectSameTypes<LazyMethodsGuard<['foo', 'bar', 'baz'], Lazy, Eager>, never>(true);
});
it('preserves undefined properties after initialization', async () => {
@ -73,6 +110,12 @@ class EagerService {
answer = 42;
notImplemented?(): void;
async isEager(): Promise<boolean> {
return true;
}
}
class FakeEagerService {
@ -83,6 +126,10 @@ class FakeEagerService {
class LazyService extends EagerService {
async isEager(): Promise<boolean> {
return false;
}
hello(): string {
return 'Hi there!';
}

View File

@ -39,6 +39,47 @@ export type Pretty<T> = T extends infer U ? {[K in keyof U]: U[K]} : never;
*/
export type SubPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
/**
* Helper type to negate a boolean type.
*/
export type Not<T extends boolean> = IsTrue<T> extends true ? false : (IsFalse<T> extends true ? true : boolean);
/**
* Helper type to check whether a boolean type is exactly `true`.
*/
export type IsTrue<T extends boolean> = Exclude<T, true> extends never ? true : false;
/**
* Helper type to check whether a boolean type is exactly `false`.
*/
export type IsFalse<T extends boolean> = Exclude<T, false> extends never ? true : false;
/**
* Helper type to check whether the given tuple contains all the items in a union.
*/
export type TupleContainsAll<TTuple extends unknown[], TItems> = Exclude<
TItems,
TTuple[number]
> extends never ? true : false;
/**
* Helper type to check whether the given tuple contains any items outside of a union.
*/
export type TupleContainsOthers<TTuple extends unknown[], TItems> = Exclude<
TTuple[number],
TItems
> extends never ? false : true;
/**
* Helper type to check whether the given tuple matches the items in a union.
*
* This means that the tuple will have all the items from the union, but not any outside of it.
*/
export type TupleMatches<TTuple extends unknown[], TItems> = IsTrue<
TupleContainsAll<TTuple, TItems> |
Not<TupleContainsOthers<TTuple, TItems>>
>;
/**
* Helper type to omit union.
* You can use it if need to omit an element from types union.