Skip to content

Commit

Permalink
Merge pull request #3804 from NoelDeMartin/MOBILE-4272
Browse files Browse the repository at this point in the history
MOBILE-4272: Rollback async handlers
  • Loading branch information
dpalou authored Oct 3, 2023
2 parents 056bb28 + 53e902c commit 9a6c1bb
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { CoreSharedModule } from '@/core/shared.module';
import { getAssessmentStrategyHandlerInstance } from '@addons/mod/workshop/assessment/accumulative/services/handler';
import {
AddonModWorkshopAssessmentStrategyAccumulativeHandler,
} from '@addons/mod/workshop/assessment/accumulative/services/handler-lazy';

@NgModule({
imports: [
Expand All @@ -26,7 +28,12 @@ import { getAssessmentStrategyHandlerInstance } from '@addons/mod/workshop/asses
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());

AddonWorkshopAssessmentStrategyDelegate.registerHandler(
AddonModWorkshopAssessmentStrategyAccumulativeHandler.instance,
);
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getAssessmentStrategyHandlerInstance } from './services/handler';
import { AddonModWorkshopAssessmentStrategyCommentsHandler } from '@addons/mod/workshop/assessment/comments/services/handler-lazy';

@NgModule({
imports: [
Expand All @@ -26,7 +26,10 @@ import { getAssessmentStrategyHandlerInstance } from './services/handler';
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());

AddonWorkshopAssessmentStrategyDelegate.registerHandler(AddonModWorkshopAssessmentStrategyCommentsHandler.instance);
},
},
],
Expand Down
11 changes: 9 additions & 2 deletions src/addons/mod/workshop/assessment/numerrors/numerrors.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import { CoreSharedModule } from '@/core/shared.module';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
import { getAssessmentStrategyHandlerInstance } from './services/handler';
import {
AddonModWorkshopAssessmentStrategyNumErrorsHandler,
} from '@addons/mod/workshop/assessment/numerrors/services/handler-lazy';

@NgModule({
imports: [
Expand All @@ -26,7 +28,12 @@ import { getAssessmentStrategyHandlerInstance } from './services/handler';
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());

AddonWorkshopAssessmentStrategyDelegate.registerHandler(
AddonModWorkshopAssessmentStrategyNumErrorsHandler.instance,
);
},
},
],
Expand Down
7 changes: 5 additions & 2 deletions src/addons/mod/workshop/assessment/rubric/rubric.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getAssessmentStrategyHandlerInstance } from './services/handler';
import { AddonModWorkshopAssessmentStrategyRubricHandler } from '@addons/mod/workshop/assessment/rubric/services/handler-lazy';

@NgModule({
imports: [
Expand All @@ -26,7 +26,10 @@ import { getAssessmentStrategyHandlerInstance } from './services/handler';
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());
// TODO use async instances
// AddonWorkshopAssessmentStrategyDelegate.registerHandler(getAssessmentStrategyHandlerInstance());

AddonWorkshopAssessmentStrategyDelegate.registerHandler(AddonModWorkshopAssessmentStrategyRubricHandler.instance);
},
},
],
Expand Down
12 changes: 8 additions & 4 deletions src/addons/mod/workshop/workshop.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getCronHandlerInstance } from '@addons/mod/workshop/services/handlers/sync-cron';
import { getPrefetchHandlerInstance } from '@addons/mod/workshop/services/handlers/prefetch';
import { AddonModWorkshopPrefetchHandler } from '@addons/mod/workshop/services/handlers/prefetch-lazy';
import { AddonModWorkshopSyncCronHandler } from '@addons/mod/workshop/services/handlers/sync-cron-lazy';

/**
* Get workshop services.
Expand Down Expand Up @@ -85,9 +85,13 @@ const routes: Routes = [
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
// TODO use async instances
// CoreCourseModulePrefetchDelegate.registerHandler(getPrefetchHandlerInstance());
// CoreCronDelegate.register(getCronHandlerInstance());

CoreCourseModuleDelegate.registerHandler(AddonModWorkshopModuleHandler.instance);
CoreCourseModulePrefetchDelegate.registerHandler(getPrefetchHandlerInstance());
CoreCronDelegate.register(getCronHandlerInstance());
CoreCourseModulePrefetchDelegate.registerHandler(AddonModWorkshopPrefetchHandler.instance);
CoreCronDelegate.register(AddonModWorkshopSyncCronHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModWorkshopIndexLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModWorkshopListLinkHandler.instance);

Expand Down
4 changes: 2 additions & 2 deletions src/core/features/native/services/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
import { Injectable } from '@angular/core';
import { makeSingleton } from '@singletons';
import { CorePlatform } from '@services/platform';
import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance';
import { AsyncInstance, AsyncObject, asyncInstance } from '@/core/utils/async-instance';

/**
* Native plugin manager.
*/
@Injectable({ providedIn: 'root' })
export class CoreNativeService {

private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance>> = {};
private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance<AsyncObject>>> = {};
private mocks: Partial<Record<keyof MoodleAppPlugins, MoodleAppPlugins[keyof MoodleAppPlugins]>> = {};

/**
Expand Down
73 changes: 49 additions & 24 deletions src/core/utils/async-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@

import { CorePromisedValue } from '@classes/promised-value';

// eslint-disable-next-line @typescript-eslint/ban-types
type AsyncObject = object;

/**
* Create a wrapper to hold an asynchronous instance.
*
* @param lazyConstructor Constructor to use the first time the instance is needed.
* @returns Asynchronous instance wrapper.
*/
function createAsyncInstanceWrapper<TEagerInstance extends AsyncObject, TInstance extends TEagerInstance>(
lazyConstructor?: () => TInstance | Promise<TInstance>,
): AsyncInstanceWrapper<TEagerInstance, TInstance> {
let promisedInstance: CorePromisedValue<TInstance> | null = null;
function createAsyncInstanceWrapper<
TLazyInstance extends TEagerInstance,
TEagerInstance extends AsyncObject = Partial<TLazyInstance>
>(
lazyConstructor?: () => TLazyInstance | Promise<TLazyInstance>,
): AsyncInstanceWrapper<TLazyInstance, TEagerInstance> {
let promisedInstance: CorePromisedValue<TLazyInstance> | null = null;
let eagerInstance: TEagerInstance;

return {
Expand Down Expand Up @@ -90,20 +90,36 @@ function createAsyncInstanceWrapper<TEagerInstance extends AsyncObject, TInstanc
};
}

/**
* Check whether the given value is a method.
*
* @param value Value.
* @returns Whether the given value is a method.
*/
function isMethod(value: unknown): value is (...args: unknown[]) => unknown {
return typeof value === 'function';
}

/**
* Asynchronous instance wrapper.
*/
export interface AsyncInstanceWrapper<TEagerInstance extends AsyncObject, TInstance extends TEagerInstance> {
instance?: TInstance;
export interface AsyncInstanceWrapper<
TLazyInstance extends TEagerInstance,
TEagerInstance extends AsyncObject = Partial<TLazyInstance>
> {
instance?: TLazyInstance;
eagerInstance?: TEagerInstance;
getInstance(): Promise<TInstance>;
getProperty<P extends keyof TInstance>(property: P): Promise<TInstance[P]>;
setInstance(instance: TInstance): void;
getInstance(): Promise<TLazyInstance>;
getProperty<P extends keyof TLazyInstance>(property: P): Promise<TLazyInstance[P]>;
setInstance(instance: TLazyInstance): void;
setEagerInstance(eagerInstance: TEagerInstance): void;
setLazyConstructor(lazyConstructor: () => TInstance | Promise<TInstance>): void;
setLazyConstructor(lazyConstructor: () => TLazyInstance | Promise<TLazyInstance>): void;
resetInstance(): void;
}

// eslint-disable-next-line @typescript-eslint/ban-types
export type AsyncObject = object;

/**
* Asynchronous version of a method.
*/
Expand All @@ -121,9 +137,9 @@ export type AsyncMethod<T> =
* All methods are converted to their asynchronous version, and properties are available asynchronously using
* the getProperty method.
*/
export type AsyncInstance<TEagerInstance extends AsyncObject = AsyncObject, TInstance extends TEagerInstance = TEagerInstance> =
AsyncInstanceWrapper<TEagerInstance, TInstance> & TEagerInstance & {
[k in keyof TInstance]: AsyncMethod<TInstance[k]>;
export type AsyncInstance<TLazyInstance extends TEagerInstance, TEagerInstance extends AsyncObject = Partial<TLazyInstance>> =
AsyncInstanceWrapper<TLazyInstance, TEagerInstance> & {
[k in keyof TLazyInstance]: AsyncMethod<TLazyInstance[k]>;
};

/**
Expand All @@ -133,19 +149,23 @@ export type AsyncInstance<TEagerInstance extends AsyncObject = AsyncObject, TIns
* @param lazyConstructor Constructor to use the first time the instance is needed.
* @returns Asynchronous instance.
*/
export function asyncInstance<TEagerInstance extends AsyncObject, TInstance extends TEagerInstance = TEagerInstance>(
lazyConstructor?: () => TInstance | Promise<TInstance>,
): AsyncInstance<TEagerInstance, TInstance> {
const wrapper = createAsyncInstanceWrapper<TEagerInstance, TInstance>(lazyConstructor);
export function asyncInstance<TLazyInstance extends TEagerInstance, TEagerInstance extends AsyncObject = Partial<TLazyInstance>>(
lazyConstructor?: () => TLazyInstance | Promise<TLazyInstance>,
): AsyncInstance<TLazyInstance, TEagerInstance> {
const wrapper = createAsyncInstanceWrapper<TLazyInstance, TEagerInstance>(lazyConstructor);

return new Proxy(wrapper, {
get: (target, property, receiver) => {
if (property in target) {
return Reflect.get(target, property, receiver);
}

if (wrapper.instance && property in wrapper.instance) {
return Reflect.get(wrapper.instance, property, receiver);
if (wrapper.instance) {
const value = Reflect.get(wrapper.instance, property, receiver);

return isMethod(value)
? async (...args: unknown[]) => value.call(wrapper.instance, ...args)
: value;
}

if (wrapper.eagerInstance && property in wrapper.eagerInstance) {
Expand All @@ -154,9 +174,14 @@ export function asyncInstance<TEagerInstance extends AsyncObject, TInstance exte

return async (...args: unknown[]) => {
const instance = await wrapper.getInstance();
const method = Reflect.get(instance, property, receiver);

if (!isMethod(method)) {
throw new Error(`'${property.toString()}' is not a function`);
}

return instance[property](...args);
return method.call(instance, ...args);
};
},
}) as AsyncInstance<TEagerInstance, TInstance>;
}) as AsyncInstance<TLazyInstance, TEagerInstance>;
}
94 changes: 94 additions & 0 deletions src/core/utils/tests/async-instance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// (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 { AsyncInstance, asyncInstance } from '@/core/utils/async-instance';
import { expectAnyType, expectSameTypes } from '@/testing/utils';

describe('AsyncInstance', () => {

it('initializes instances lazily', async () => {
const asyncService = asyncInstance(() => new LazyService());

expect(asyncService.instance).toBe(undefined);
expect(await asyncService.hello()).toEqual('Hi there!');
expect(asyncService.instance).toBeInstanceOf(LazyService);
});

it('does not initialize instance for eager properties', async () => {
const asyncService = asyncInstance(() => new LazyService());

asyncService.setEagerInstance(new EagerService());

expect(asyncService.instance).toBeUndefined();
expect(asyncService.answer).toEqual(42);
expect(asyncService.instance).toBeUndefined();
expect(await asyncService.hello()).toEqual('Hi there!');
expect(asyncService.instance).toBeInstanceOf(LazyService);
});

it('preserves undefined properties after initialization', async () => {
const asyncService = asyncInstance(() => new LazyService()) as { thisDoesNotExist?: () => Promise<void>};

await expect(asyncService.thisDoesNotExist?.()).rejects.toBeInstanceOf(Error);

expect(asyncService.thisDoesNotExist).toBeUndefined();
});

it('restricts types hierarchy', () => {
type GetInstances<T> = T extends AsyncInstance<infer TLazyInstance, infer TEagerInstance>
? { eager: TEagerInstance; lazy: TLazyInstance }
: never;
type GetEagerInstance<T> = GetInstances<T>['eager'];
type GetLazyInstance<T> = GetInstances<T>['lazy'];

expectSameTypes<GetLazyInstance<AsyncInstance<LazyService>>, LazyService>(true);
expectSameTypes<GetEagerInstance<AsyncInstance<LazyService>>, Partial<LazyService>>(true);

expectSameTypes<GetLazyInstance<AsyncInstance<LazyService, EagerService>>, LazyService>(true);
expectSameTypes<GetEagerInstance<AsyncInstance<LazyService, EagerService>>, EagerService>(true);

// @ts-expect-error LazyService should extend FakeEagerService.
expectAnyType<AsyncInstance<LazyService, FakeEagerService>>();
});

it('makes methods asynchronous', () => {
expectSameTypes<AsyncInstance<LazyService>['hello'], () => Promise<string>>(true);
expectSameTypes<AsyncInstance<LazyService>['goodbye'], () => Promise<string>>(true);
});

});

class EagerService {

answer = 42;

}

class FakeEagerService {

answer = '42';

}

class LazyService extends EagerService {

hello(): string {
return 'Hi there!';
}

async goodbye(): Promise<string> {
return 'Sayonara!';
}

}
5 changes: 5 additions & 0 deletions src/core/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructor<T> = { new(...args: any[]): T };

/**
* Helper type to infer whether two types are exactly the same.
*/
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;

/**
* Helper type to flatten complex types.
*/
Expand Down
10 changes: 10 additions & 0 deletions src/testing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { CoreIonLoadingElement } from '@classes/ion-loading';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DefaultUrlSerializer, UrlSerializer } from '@angular/router';
import { CoreUtils, CoreUtilsProvider } from '@services/utils/utils';
import { Equal } from '@/core/utils/types';

abstract class WrapperComponent<U> {

Expand Down Expand Up @@ -421,3 +422,12 @@ export function mockTranslate(translations: Record<string, string> = {}): void {
},
});
}

export function expectSameTypes<A, B>(equal: Equal<A, B>): () => void {
return () => expect(equal).toBe(true);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function expectAnyType<T>(): () => void {
return () => expect(true).toBe(true);
}

0 comments on commit 9a6c1bb

Please sign in to comment.